Redux 源碼拾遺

首先來一段 redux 結合 中間件 thunk、logger 的使用demo 瞭解一下應該如何使用html

const redux = require('redux')
const { 
  createStore,
  combineReducers,
  bindActionCreators,
  compose,
  applyMiddleware
} = redux

const simpleState = { num: 0 }
const complexState = { num: 1 }

// logger中間件負責打印日誌
function logger({dispatch, getState}) {
  return function(next) {
    return function(action) {
      console.log('logger start:' + action.type)
      const res = next(action)
      console.log('logger end:' + action.type)
      return res
    }
  }
}

// thunk中間件負責異步
function thunk({dispatch, getState}) {
  return function(next) {
    return function(action) {
      if (typeof action === 'function') {
        return action(dispatch)
      }
      return next(action)
    }
  }
}

// 派發的增長方法
function increment(num) {
  return {
    type: 'increment',
    payload: num
  }
}

// 派發的減小的方法
function decrement(num) {
  return {
    type: 'decrement',
    payload: num
  }
}

// 派發的乘法的方法
function multiply(num) {
  return {
    type: 'multiply',
    payload: num
  }
}

// 派發的除法的方法
function divide(num) {
  return {
    type: 'divide',
    payload: num
  }
}

// 派發異步事件方法
function syncDivide() {
  return (dispatch) => {
    setTimeout(() => {
      dispatch(divide(10))
    }, 2000)
  }
}

// 負責加減法的reducer
function simpleCalcReducer(state = simpleState, action) {
  switch (action.type) {
    case 'increment':
      return Object.assign({}, state, { num: state.num + action.payload })
    case 'decrement':
      return Object.assign({}, state, { num: state.num - action.payload })
    default: 
      return state
  }
}

// 負責乘除法的reducer
function complexCalcReducer(state = complexState, action) {
  switch (action.type) {
    case 'multiply':
      return Object.assign({}, state, { num: state.num * action.payload })
    case 'divide':
      return Object.assign({}, state, { num: state.num / action.payload })
    default: 
      return state
  }
}

const middleware = applyMiddleware(thunk, logger)
const allReducers = combineReducers({
  simpleCalcReducer,
  complexCalcReducer
})
const store = createStore(allReducers, middleware)

// 全部action
const allActions = {
  increment,
  decrement,
  multiply,
  divide,
  syncDivide
}
const actions = bindActionCreators(allActions, store.dispatch)

// 派發action
actions.increment(2);
actions.decrement(1);
actions.multiply(10);
actions.divide(5);
actions.syncDivide()
setTimeout(() => {
  console.log(store.getState())
}, 2500)

打印結果以下:node

clipboard.png

上面只是簡單使用了redux的部分api,來儘可能實現一個標準項目中所使用到的redux的基本操做流程,下面進入源碼的分析階段react

1.項目結構
第一張圖,先來看一下 redux 的項目結構目錄webpack

clipboard.png

---src
------utils // 工具函數庫
---------actionTypes.js // redux用來初始化state,自身調用的action
---------isPlainObject.js // 用來檢測action 是否爲一個明確的對象
---------warning.js // 用來進行警告的公共方法
------applyMiddleware.js // 中間件,負責對store.dispatch方法進行包裝
------bindActionCreators.js // 對派發action的語法進行優化,使之能夠更好的應用於react或其餘
------combineReducers.js // 將分離的reducer合併成一個對象,並返回一個執行這些reducer的函數
------compose.js // 合併函數, 將函數參數按照從左向右的順序進行合併,返回一個新函數
------createStore.js // 核心方法,返回一個store對象,用來獲取state、派發action、添加訂閱者subscribe 等
------index.js // 入口文件,對外導出的方法git

2.utils 工具方法的分析
將utils的三個方法放在最前面來分析,主要是爲了先對三個工具方法有個簡單概念和了解,這樣在閱讀下面的內容時,對於有用到工具方法的地方能夠直接理解,不用再打斷思緒去分析工具方法,影響總體的閱讀體驗github

① warning.js -- 控制檯打印異常日誌web

export default function warning(message) {
  if (typeof console !== 'undefined' && typeof console.error === 'function') {
    console.error(message)
  }
  try {
    throw new Error(message)
  } catch (e) {}
}

總結:
warining 函數很是簡單,當 console.error 方法存在時來打印 錯誤的 message,這裏的一層判斷是爲了兼容ie6789瀏覽器只有在開啓控制檯的狀況下,console對象纔會建立,不然會報console未定義而導出程序沒法進行。
至於下面的爲何要 try 裏面去拋出異常,自己這樣作對程序是沒有什麼影響的,這樣的意義又是什麼呢?源碼的註釋裏解釋道 「This error was thrown as a convenience so that if you enable 'break on all exceptions' in your console, it would pause the execution at this line 」,翻譯過來就是,拋出這個錯誤是爲了方便查看錯誤源。只要咱們開啓了斷點異常,那麼程序就會在拋出錯誤的那行代碼上打上斷點,來方便進行調試和追蹤。那麼在谷歌裏面這個異常怎麼開啓呢?F12打開谷歌的開發者工具,1點擊 Sources - 2點擊藍色的pause icon - 3勾選 Pause on caught exceptions,以下圖所示redux

clipboard.png

在控制檯裏測試以下代碼api

clipboard.png

鍵入回車後,瀏覽器出現斷點,跳轉至sources資源文件,並高亮了拋出錯誤的那行代碼,很是方便數組

clipboard.png

② isPlainObject.js -- 判斷目標是否爲明確的對象

export default function isPlainObject(obj) {
  if (typeof obj !== 'object' || obj === null) return false

  let proto = obj
  while (Object.getPrototypeOf(proto) !== null) {
    proto = Object.getPrototypeOf(proto)
  }

  return Object.getPrototypeOf(obj) === proto
}

總結:
Object.getPrototypeOf 方法用來返回對象的隱式原型,即構造這個對象的構造函數的原型。
使用 while 循環來查找目標對象的原型鏈頂層,由於 Object.prototype.__proto__ === null,因此Object.prototype 就是原型鏈的頂層,查找到最後一層時,proto 必定被賦值爲 Object.prototype。
這樣作的意義是什麼? Array數據類型 或者 dom的 document等數據類型的typeof 也是 object, 他們都不是直接由Object 函數來構建的對象,以 Array 數組的構造函數來舉例。

var ary = new Array()  
var pro = Object.getPrototypeOf(ary)
console.log(pro === ary.__proto__) // true
console.log(ary.__proto__ === Array.prototype)  // true

var pro2 = Object.getPrototypeOf(pro)
console.log(pro2 === Object.prototype)  // true
Object.prototype.__proto__ === null  // true

可見 ary 第一次獲取到的原型是 Array.prototype,而 Array.prototype 自己也是一個對象,必然由 Object 函數建立,因此 Array.prototype.__proto__ 又指向了 Object.prototype,到此循環結束。最終 pro = Object.prototype。這就形成了最終的 Object.getPrototypeOf(obj) 和 proto 是不等的。
因此這個方法的就很清晰了,只有直接使用 Object 函數建立的對象纔會被判斷爲真,由於只有它原型鏈存在一層

③ actionTypes.js -- redux 內部作初始化state的action

const randomString = () =>
  Math.random()
    .toString(36)
    .substring(7)
    .split('')
    .join('.')

const ActionTypes = {
  INIT: `@@redux/INIT${randomString()}`,
  REPLACE: `@@redux/REPLACE${randomString()}`,
  PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
}

export default ActionTypes

總結:
randomString() 方法隨機生成一個36進制的數字,而後切割拼接,最終生成 和"5.b.p.3.b.8" 格式同樣的字符串
這個方法 導出了一個對象,對象包含3個key: INIT、REPLACE、PROBE_UNKNOWN_ACTION,前兩個是字符串,後面一個是方法,方法也是返回一個拼接好的字符串,其實這三個都是redux內部用來派發action 的 type

3.模塊分析(去掉註釋)
① 入口文件 index.js**

import createStore from './createStore'
import combineReducers from './combineReducers'
import bindActionCreators from './bindActionCreators'
import applyMiddleware from './applyMiddleware'
import compose from './compose'
import warning from './utils/warning'
import __DO_NOT_USE__ActionTypes from './utils/actionTypes'

function isCrushed() {}

if (
  process.env.NODE_ENV !== 'production' &&
  typeof isCrushed.name === 'string' &&
  isCrushed.name !== 'isCrushed'
) {
  warning(
    'You are currently using minified code outside of NODE_ENV === "production". ' +
      'This means that you are running a slower development build of Redux. ' +
      'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' +
      'or setting mode to production in webpack (https://webpack.js.org/concepts/mode/) ' +
      'to ensure you have the correct code for your production build.'
  )
}

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
  __DO_NOT_USE__ActionTypes
}

總結:
入口文件做用就時將幾個模塊文件引入和導出,裏面有一個空的 isCrushed 函數,這個函數的意義就是判斷當前的構建工具是否爲 node 環境,若是是 node 環境而且非生產環境,那麼就要判斷當前的 redux 文件有沒有被壓縮。
判斷的目的就是但願開發者,在非生產環境下不要壓縮代碼,若是項目比較大,咱們只是修改了一個小小的樣式,這時候若是開啓代碼的混淆壓縮,那麼咱們項目的全部依賴的文件都會被混淆壓縮,項目越大被壓縮的內容越多耗時越長,從而致使調試的時間增長,下降開發效率。這也正是redux在 warning 警告裏提到的 'This means that you are running a slower development build of Redux',你正在一個緩慢的開發環境下使用 redux。

② 核心文件 creteStore.js
cretaeStore.js 是redux的核心文件,在這個方法裏,redux 向外提供了 dispatch、subscribe、getState、replaceReducer 這四個核心方法。此外還有一個 [$$observable] 方法,這個方法並非很好理解他的做用和意義,放在文章最後來講明。下面是移除了註釋的源代碼

import $$observable from 'symbol-observable'
import ActionTypes from './utils/actionTypes'
import isPlainObject from './utils/isPlainObject'

export default function createStore(reducer, preloadedState, enhancer) {
  // 判斷1
  if (
    (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
    (typeof enhancer === 'function' && typeof arguments[3] === 'function')
  ) {
    throw new Error(
      'It looks like you are passing several store enhancers to ' +
        'createStore(). This is not supported. Instead, compose them ' +
        'together to a single function.'
    )
  }

  // 判斷2
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  // 判斷3
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    return enhancer(createStore)(reducer, preloadedState)
  }

  // 判斷4
  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }

  // 內部變量
  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

  function ensureCanMutateNextListeners() {
    // ...
  }

  function getState() {
    // ...
  }

  function subscribe(listener) {
    // ...
  }

  function dispatch(action) {
    // ...
  }

  function replaceReducer(nextReducer) {
    // ...
  }

  function observable() {
    // ...
  }

  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}

createStore 方法接收3個參數,分別是 reducer,preloadedState,enhancer。
1.reducer 就是一個純函數用來返回 state
2.preloadedState 是初始化的state,在實際開發中,不多有傳遞這個這個參數。其實這個參數就是爲了初始化必須的原始數據。此外,若是使用了combineReducer這個方法來組合多個reducer,相應的preloadedState 裏的key 也必需要和 combineReducer 中的key相對應
3.enhancer 翻譯過來就是加強器,它是一個相似的高階函數用來包裝和加強 creteStore 內部的 dispatch、subscribe、getState 等方法,經過這個高階函數能夠實現 中間件、時間旅行、持久化state等,在redux內只實現了一個enhancer,它就是中間件 applyMIddleware,用來強化 dispatch方法。

講完三個參數,開始解釋代碼

if (
    (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
    (typeof enhancer === 'function' && typeof arguments[3] === 'function')
  ) {
    throw new Error(
      'It looks like you are passing several store enhancers to ' +
        'createStore(). This is not supported. Instead, compose them ' +
        'together to a single function.'
    )
  }

判斷1:提示很明確,當使用多個enhancer時,須要使用compose 方法將多個enhancer合併成一個單一的函數。

// 判斷2
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

判斷2:我的感受這裏的判斷就是爲了符合更大衆化的開發需求,preloadedState 這個參數在實際開發中,真的不多傳遞。因此在這裏作了一個判斷,若是開發中沒有用到這個初始的preloadedState,徹底能夠將它省略掉,直接傳遞最後一個enhancer函數,redux在內部幫開發者,完成了這部分參數轉換的處理。

// 判斷3
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    return enhancer(createStore)(reducer, preloadedState)
  }

判斷3:當enhancer 存在而且爲函數的時候,直接將當前creteStore方法做爲參數傳遞給 enhancer。而這個enhancer(createStore),返回的就是 createStore 的增強版能夠稱他爲 creteStore-X,至於如何加強先放到後面的applyMiddleware這個enhancer來講明。這裏先只須要知道,經過enhancer包裝事後的 createStore,內部的某些方法被增強了。

// 判斷4
  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }

判斷4:reducer 必須是一個函數 函數 函數

let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

內部變量:
這裏分別使用了,currentReducer 和 currentState 來接收初始傳遞的 reducer 和 preloadedState,從新賦值是由於 currentReducer 和 currentState 都是可變的,當他們被修改的時候不會影響初始的reducer 和 preloadedState。
currentListeners 是一個數組用來存儲訂閱的函數列表,爲何還要多定義一個 nextListeners = currentListeners 呢?這個問題放到後面看比較好理解,先掠過
isDispatching 比較好理解,用來判斷redux是否處於派發action的狀態中,便是否在執行reducer。

function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

ensureCanMutateNextListeners:返回currentListeners 的淺拷貝
這裏的意圖不是很明顯,當nextListeners 和 currentListeners 全等的時候,返回一個 currentListeners 的 淺拷貝賦值給 nextListenenrs,意義是什麼呢接着向下看。

function getState() {
    if (isDispatching) {
      throw new Error(
        'You may not call store.getState() while the reducer is executing. ' +
          'The reducer has already received the state as an argument. ' +
          'Pass it down from the top reducer instead of reading it from the store.'
      )
    }

    return currentState
  }

getState:返回當前的currentState
返回當前的currentState,當action還在 派發當中時,若是調用這個方法會拋出錯誤

function subscribe(listener) {
     // 被添加的訂閱者必須是一個函數
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }
    // 處於dispatch的時候,不容許添加訂閱者
    if (isDispatching) {
      throw new Error(
        'You may not call store.subscribe() while the reducer is executing. ' +
          'If you would like to be notified after the store has been updated, subscribe from a ' +
          'component and invoke store.getState() in the callback to access the latest state. ' +
          'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
      )
    }
    // 這裏是一個閉包,isSubscribed 用來表示當前的訂閱者已經成功的被
    // 添加到了訂閱的列表中
    let isSubscribed = true
    // 當nexListeners 和 currentListeners 全等時候,返回了一個新的
    // nextListeners, 而後將訂閱者添加到新的 nextListeners
    ensureCanMutateNextListeners() // ?1
    nextListeners.push(listener)
    // 返回一個 unsubscribe, 用來取消已經添加到訂閱列表中的訂閱者
    return function unsubscribe() {
        // 若是當前的訂閱者已經被取消了 直接返回
      if (!isSubscribed) {
        return
      }
      // 處於dispatch的時候,不容許取消訂閱者
      if (isDispatching) {
        throw new Error(
          'You may not unsubscribe from a store listener while the reducer is executing. ' +
            'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
        )
      }
      // 將訂閱者的狀態改成false
      isSubscribed = false
      // 繼續更新 nextListeners
      ensureCanMutateNextListeners()
      // 將取消的訂閱者從 nextListeners 中刪除
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

subscribe:用來添加和刪除訂閱者
這樣粗略看下來彷佛也沒太看清楚這個 ensureCanMutateNextListeners 方法有什麼卵用,繼續翻看redux 對ensureCanMutateNextListeners 方法的解釋,裏面提到
"This makes a shallow copy of currentListeners so we can use nexListeners as a temporary list while dispatching"
"This prevents any bugs around consumers calling subscribe/unsubscribe in the middle of a dispatch"
主要就是後面這一句,這個方法 能夠防止用戶在執行dispatch中調用訂閱或者取消訂閱時出現任何錯誤。仍是不明因此,繼續向下尋找答案

function dispatch(action) {
      // 判斷 action 是否爲明確的 object 對象
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }
     // action 必須有 type 這個字段類型
    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }
      // 不容許同時執行多個 dispatch
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }
      // 將執行狀態設置爲true, 開始執行reudcer, 並接收 currentState
    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
        // 不管成功失敗 都將執行狀態重置
      isDispatching = false
    }
      // reducer 執行完畢以後,開始循環執行 訂閱者數組列表
    const listeners = (currentListeners = nextListeners) // ?2
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i] // ?3
      listener()
    }

    return action
  }

dispatch:用來派發action,執行reducer
問題2,每次執行訂閱者列表的時候,爲何要新定義一個 listeners 來接收 nextListeners,直接 currentListeners = nextListeners 而後使用 currentListeners 來循環不行嗎?
問題3,循環裏面爲何不直接使用 listeners[i]() 來執行調用,而是定義一個變量來接收而後再執行?
再結合上面 subscribe 中 ensureCanMutateNextListeners 的到底有何意義?
其實,這一切的細節都是爲了解決一種特殊的應用場景,在訂閱事件內部 再次 添加訂閱或者取消訂閱。如
store.subscribe( () => { store.subscribe(...) })
若是是這種場景,由於是在循環中去觸發 listener,單純的使用 currentListeners 來存儲訂閱列表是沒法知足的。循環尚未結束,其中的某個 listener 對 currentListeners 進行了添加或者刪除,都會影響到這次循環的進行,帶來不可預期的錯誤。至此這些細節就變得清晰起來了。
引用官方的註釋更爲準確,每次dispatch後,循環使用的是當前nextListeners 的快照,一樣也就是 currentListeners,它在循環結束以前是不會被改變的。想象一下,假如在訂閱事件的內部繼續調用 store.subsrcibe 來添加訂閱者,那麼就會調用 ensureCanMutateNextListeners 這個方法,若是currentListeners 和 nextListeners 是徹底相等的說明nextListeners 還未被改變,此時淺拷貝一份 currentListenrs 的隊列賦值爲 nextListeners,nextListeners 就變成了一個全新的訂閱隊列,而後將 添加的訂閱者放到新的 nextListeners,這樣就徹底不會影響到以前已經開始的循環。當下次disptach 再次發起的時候,將 currentListeners 同步爲最新的 nextListeners 隊列。
問題 2 應該如何理解呢?找到了早期redux的提交記錄以下:

clipboard.png

這裏尚未對 currentListeners 和 nextListeners 作概念的區分,只是將每次listeners 淺拷貝了一層用來 安全的執行循環。因此 const listeners = (currentListeners = nextListeners) 中聲明的 listeners並非必須的,他的存在只是爲了在以後在循環中使用 listeners 代替 currentListeners 少打幾個字母而已
問題 2 應該如何理解呢?其實在這裏是爲了確保在 listener 當中的 this 與咱們對 js 當中的函數內部 this 指向誰的預期保持一致。這裏就是將 this 從新綁定爲默認的全局對象,若是直接使用 listeners[i]() 來調用,那麼其內部的this 變指向了listeners 這個數組自己。

function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }
    currentReducer = nextReducer
    dispatch({ type: ActionTypes.REPLACE })
  }

replaceReducer:替換reducer,而且從新 dispatch
replaceReducer 這個方法比較簡單,當咱們的項目啓用了模塊懶加載,咱們的最開始的reducer可能只是部分的模塊的reducer,這時候若是要引入新的模塊就須要可以動態的替換 reducer 來更新state。官方的註釋裏還寫道,若是在開發模式下啓用了熱更新,一樣須要這個函數來進行替換。

function observable() {
    const outerSubscribe = subscribe
    return {
      subscribe(observer) {
        if (typeof observer !== 'object' || observer === null) {
          throw new TypeError('Expected the observer to be an object.')
        }
        function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }
        observeState()
        const unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },
      [$$observable]() {
        return this
      }
    }
  }

observable:此方法返回一個 observable 對象,該對象擁有 subscribe 的方法來添加一個可觀測的對象。那麼這個方法到底時幹什麼用的呢?註釋裏有寫道 「For more information, see the observable proposal:https://github.com/tc39/propo...」,打開這個地址看到這個項目是將 可觀察類型引入到ECMAScript標準庫中,而redux 這裏就是實現了 observable 的觀察對象。這裏不是很清楚如何使用 ... 略過。

dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }

最後使用 dispatch({ type: ActionTypes.INIT }) 生成一個初始化的state,這也就是爲何preloadedState不須要傳遞,就能夠獲得初始化的state了。由於redux內部在執行 createStore 這個方法的時候,自動執行了一次 disptach。最後將衆方法返回

③ combineReducers -- 將多個reducer 合併成一個函數

import ActionTypes from './utils/actionTypes'
import warning from './utils/warning'
import isPlainObject from './utils/isPlainObject'

// 函數1
function getUndefinedStateErrorMessage(key, action) {
  // ...
}

// 函數2
function getUnexpectedStateShapeWarningMessage(
  inputState,
  reducers,
  action,
  unexpectedKeyCache
) {
  // ...
}

// 函數3
function assertReducerShape(reducers) {
  // ...
}

// 函數4 
export default function combineReducers(reducers) {
  // ...
}
function getUndefinedStateErrorMessage(key, action) {
  const actionType = action && action.type
  const actionDescription =
    (actionType && `action "${String(actionType)}"`) || 'an action'

  return (
    `Given ${actionDescription}, reducer "${key}" returned undefined. ` +
    `To ignore an action, you must explicitly return the previous state. ` +
    `If you want this reducer to hold no value, you can return null instead of undefined.`
  )
}

getUndefinedStateErrorMessage: 方法比較簡單,返回一段提示信息,提示 reducer不能返回undefined

function getUnexpectedStateShapeWarningMessage(
  inputState,
  reducers,
  action,
  unexpectedKeyCache
) {
  const reducerKeys = Object.keys(reducers)
  const argumentName =
    action && action.type === ActionTypes.INIT
      ? 'preloadedState argument passed to createStore'
      : 'previous state received by the reducer'
    // reducer 不存在時進行的提示
  if (reducerKeys.length === 0) {
    return (
      'Store does not have a valid reducer. Make sure the argument passed ' +
      'to combineReducers is an object whose values are reducers.'
    )
  }
      // state 爲非明確的對象的時候的提示
  if (!isPlainObject(inputState)) {
    return (
      `The ${argumentName} has unexpected type of "` +
      {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
      `". Expected argument to be an object with the following ` +
      `keys: "${reducerKeys.join('", "')}"`
    )
  }
  // 當state當中的某個key值沒法在reducers當中找到,而且還未被加入到unexpectedKeyCache
  // 那麼就把這個key篩選出來
  const unexpectedKeys = Object.keys(inputState).filter(
    key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
  )
  // 將上一步驟篩選出來的key,存儲到unexpectedKeyCache裏面
  unexpectedKeys.forEach(key => {
    unexpectedKeyCache[key] = true
  })
  // 若是是使用了 replaceReducer 替換了reducer,那麼就不須要進行提示了,由於以前
  // 的state 的數據可能和 新的reducer 不能保持一致
  if (action && action.type === ActionTypes.REPLACE) return
  // 將state裏面reducer不能操做的key打印出來
  if (unexpectedKeys.length > 0) {
    return (
      `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` +
      `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` +
      `Expected to find one of the known reducer keys instead: ` +
      `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
    )
  }
  }

getUnexpectedStateShapeWarningMessage: 在非生產環境對finalReducer進行的校驗,將state內異常的 key 拋出,進而提示開發者。

function assertReducerShape(reducers) {
    // 循環全部reducer, 並使用 ActionTypes.INIT 和 ActionTypes.PROBE_UNKNOWN_ACTION()
    // 兩個type類型的 action 校驗全部reducer 是否返回了符合規範的 state
  Object.keys(reducers).forEach(key => {
    const reducer = reducers[key]
    const initialState = reducer(undefined, { type: ActionTypes.INIT })

    if (typeof initialState === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined during initialization. ` +
          `If the state passed to the reducer is undefined, you must ` +
          `explicitly return the initial state. The initial state may ` +
          `not be undefined. If you don't want to set a value for this reducer, ` +
          `you can use null instead of undefined.`
      )
    }

    if (
      typeof reducer(undefined, {
        type: ActionTypes.PROBE_UNKNOWN_ACTION()
      }) === 'undefined'
    ) {
      throw new Error(
        `Reducer "${key}" returned undefined when probed with a random type. ` +
          `Don't try to handle ${
            ActionTypes.INIT
          } or other actions in "redux/*" ` +
          `namespace. They are considered private. Instead, you must return the ` +
          `current state for any unknown actions, unless it is undefined, ` +
          `in which case you must return the initial state, regardless of the ` +
          `action type. The initial state may not be undefined, but can be null.`
      )
    }
  })
}

assertReducerShape:用來斷言reducers返回的結果是否是符合要求的類型,若是不知足判斷它會拋出一個error
能夠看出在不使用 combineReducers 的時候,咱們編寫的惟一的reducer是不用這些校驗的,若是咱們在其中返回了 undefined 那麼必然最終的 currentState 就變成了 undefined。
那麼爲何 combineReducers 這個方法強制全部的 reduer 不能返回 undefined 呢?
找到了redux的中文文檔:http://cn.redux.js.org/docs/a...
裏面提到:」combineReducers 函數設計的時候有點偏主觀,就是爲了不新手犯一些常見錯誤。也因些咱們故意設定一些規則,但若是你本身手動編寫根 redcuer 時並不須要遵照這些規則「
這樣就很清楚了,原來多的這些校驗是爲了更好的提示新手用戶,reducer的正確使用規範。
這裏還有一個問題,使用 {type: ActionTypes.INIT} 來校驗 reducer 是否返回正確的state,ActionTypes.INIT 應該必然是 reducer 內部未知的 action 了。可是爲何下面還要用 { type: ActionTypes.PROBE_UNKNOWN_ACTION() } 在重複校驗一次嗎?難道說只是爲了說明兩次校驗的出發目的不同?可是這樣是否是多餘了

export default function combineReducers(reducers) {
  // 接收一個對象形式的 reducers 集合如 { reducer1: () => {}, reducer2: () => {}, }
  const reducerKeys = Object.keys(reducers)
  // 最終將要被執行的reducers 集合
  const finalReducers = {}
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]
     // 判斷當前若是是非生成環境 而且 reducers[key] 不存在時在控制檯打印警告信息
    if (process.env.NODE_ENV !== 'production') {
      if (typeof reducers[key] === 'undefined') {
        warning(`No reducer provided for key "${key}"`)
      }
    }
     // 只有當 reducers[key] 爲函數類型時 才添加到 finalReducers 當中
    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  // 獲取finalReducers 當中的 key
  const finalReducerKeys = Object.keys(finalReducers)
  // 用來存放state中不能被操做的 key
  let unexpectedKeyCache
  if (process.env.NODE_ENV !== 'production') {
      // 爲了優化性能只在非生產模式下進行校驗
    unexpectedKeyCache = {}
  }
  // 用來校驗finalReducers中每個reducer是否合乎規範
  let shapeAssertionError
  try {
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }
  return function combination(state = {}, action) {
      // 到這裏開始執行 reducer
    if (shapeAssertionError) {
        // 若是reducer返回了undefined直接拋出錯誤
      throw shapeAssertionError
    }
    // 非生產環境進行提示
    if (process.env.NODE_ENV !== 'production') {
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      )
      if (warningMessage) {
        warning(warningMessage)
      }
    }
    // 判斷 state 有沒用被更改
    let hasChanged = false
    // 從新生成的state
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      const previousStateForKey = state[key]
      const nextStateForKey = reducer(previousStateForKey, action)
      if (typeof nextStateForKey === 'undefined') {
          // 若是reducer 返回了undefined 拋出錯誤
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      // 若是state 內的某個key的數據已經被更改過 此處必然是 true
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // 根據state是否改動過 返回對應的state
    return hasChanged ? nextState : state
  }
}

combineReducers:這個方法就是將多個reducers循環執行後最終返回一個合併後的state,這個方法仍是比較簡單的。

④-- bindActionCreators,提供了一種調用dispatch的其餘方式,代碼自己比較簡單,這裏就不在贅述了

function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error(
      `bindActionCreators expected an object or a function, instead received ${
        actionCreators === null ? 'null' : typeof actionCreators
      }. ` +
        `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
    )
  }

  const boundActionCreators = {}
  for (const key in actionCreators) {
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

⑤ -- compose, 組合函數,將多個函數參數按照傳入的順序組合成一個函數,並返回。第二個參數做爲第一個函數的參數,第三個參數做爲第二個函數的參數,依次類推。返回的函數接收的參數,將做爲compose 初始參數的最後一個函數的參數,源碼以下

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }
   // 最終 compose 返回的是一個 (...args) => a(b(...args)) 這樣的函數
  // 那麼這個函數內部調用的順序也就清楚了,執行這個函數的時候,首先會執行 
  // var c = b(...args),  而後將c 做爲參數傳遞給 a,若是多個函數的話依次類推,
  // 最早執行最裏面的那個參數,由裏向外
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

問題1,先執行了b(...args)函數,不就直接開始從裏面執行了嗎?demo1

var funcs2 = [
  function a() {
    console.log('a')
  },
  function b() {
    console.log('b')
  },
  function c(arg) {
    console.log(arg)
    console.log('c')
  }
]
var t = compose(funcs2)
t('d')
// d c b a

說好的剝洋蔥呢,怎麼就成半個了?
其實compose這個方法只是提供了組合函數的功能,真正想要實現完整的從外到內這種完整的洋蔥模型,還須要對傳遞到compose參數的每一個函數作處理,它的每一個函數必定是返回了一個新的函數,這樣才能確保它不會被簡單的執行,思考下面的demo

var funcs = [
  function a(b) {
    return () => {
      console.log('a')
      return b();
    }
  },
  function b(c) {
    return () => {
      console.log('b');
      return c();
    }
  },
  function c(arg) {
    return () => {
      console.log('c');
      console.log(arg)
    }
  }
]
var f = compose(funcs)('d');
f()
// a b c d

這就和文章最開始實現的 logger 和 thunk 的中間件很類似了。a 裏執行 b , 而後 b 裏執行 c,一箇中間件的雛形已經出現了。

⑥ -- applyMiddleware,redux內惟一實現的enhancer,用來擴展dispatch方法,也是redux中最難理解的地方,一睹真容吧,爲了便於對執行過程的理解,這裏貼一下中間件redux-thunk源碼的簡化版

({ dispatch, getState }) => next => action => {
  if (typeof action === 'function') {
    return action(dispatch, getState);
  }
  return next(action);
}
import compose from './compose'
export default function applyMiddleware(...middlewares) {
   // 這裏 applyMiddleware 返回的函數接收的第一個參數是 creteStore,
   // 這就是以前 createStore內直接使用 enhancer(createStore)(reducer, preloadedState)
   // 的緣由了。 
  return createStore => (...args) => {
      // store 就是最原始的 store 對象,這裏還未對store.dispatch 進行擴展
    const store = createStore(...args)
      // 聲明一個dispatch的方法,注意這裏使用的時let,表示這個dispatch將要會被改變
      // 而改變事後的dispatch 就是加入了中間價加強版的 dispatch
    let dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      )
    }
    // middlewareAPI和字面意思同樣就是,中間件須要用到的原始的store對象的方法,,
    // 這裏提供了兩個方法,一個getState, 一個是dispatch。等等這裏好像有點不同的東西
    // 爲何這裏 要寫成 dispatch: (...args) => dispatch(...args),而不是直接
    // dispatch: dispatch  先留下繼續向下看。
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    // 這裏參考上面的redux-thunk的源碼 chain最終返回的結果就是
    /*
       chain = [ next => action => {
          if (typeof action === 'function') {
            return action(dispatch, getState);
            // 此處的dispatch 和 getState 即爲 middlewareAPI的dispatch 和 getState
          }
          return next(action);
        } ]
        */
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 結合以前已經分析過的compose,compose(...chain)最後一個參數來接收 store.dispatch
    // 由於這裏只傳遞了一個thunk中間件,因此,這裏的 dispatch 就變成了
    /* 
        dispatch = (action) => {
          if (typeof action === 'function') {
            return action(dispatch, getState);
            // 此處的dispatch 和 getState 依然爲 middlewareAPI的dispatch 和 getState
          }
          // 因爲next被賦值爲store.dispatch, 此處實際爲
          return store.dispatch(action);
        }
    */
    // 也由於這裏dispatch 被從新賦值,致使middlewareAPI內dispatch屬性
    // 裏使用到的 dispatch 也變了,再也不是拋出error錯誤的那個函數了。
    // 這就是爲何必定要寫成 dispatch: (...args) => dispatch(...args) 的緣由了
    // 若是寫成 dispatch: dispatch, 至關於只是初始聲明瞭這個方法,後續dispatch的修改就與它無關了
    dispatch = compose(...chain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
    // 返回的dispatch 替換掉了 store 內部初始的 dispatch, dispatch被擴展了
  }
}

總結:經過對源碼的翻閱,瞭解到了整個redux的執行流程和機制。經過createStore來初始化state,當須要使用異步action的時候,可使用 applyMiddleware 將redux-thunk redux-saga 等中間件對store.dispatch 進行擴展。每次調用 dispatch 都會執行全部的reducer,reducer執行完畢後,會更新全部的訂閱事件。

相關文章
相關標籤/搜索