Redux 原始碼解讀 —— 從原始碼開始學 Redux

Russ_Zhong發表於2018-12-06

已經快一年沒有碰過 React 全家桶了,最近換了個專案組要用到 React 技術棧,所以最近又複習了一下;撿起舊知識的同時又有了一些新的收穫,在這裡作文以記之。

在閱讀文章之前,最好已經知道如何使用 Redux(不是 React-Redux)。

一、準備環境

為了更好的解讀原始碼,我們可以把原始碼拷貝到本地,然後搭建一個開發環境。Redux 的使用不依賴於 React,所以你完全可以在一個極為簡單的 JavaScript 專案中使用它。這裡不再贅述開發環境的搭建過程,需要的同學可以直接拷貝我的程式碼到本地,然後安裝依賴,執行專案。

$ git clone https://github.com/zhongdeming428/redux && cd redux

$ npm i

$ npm run dev
複製程式碼

二、閱讀原始碼

(1)原始碼結構

忽略專案中的那些說明文件什麼的,只看 src 這個原始檔目錄,其結構如下:

src
├── applyMiddleware.js  // 應用中介軟體的 API
├── bindActionCreators.js   // 轉換 actionCreators 的 API
├── combineReducers.js  // 組合轉換 reducer 的 API
├── compose.js  // 工具函式,用於巢狀呼叫中介軟體
├── createStore.js  // 入口函式,建立 store 的 API
├── index.js    // redux 專案的入口檔案,用於統一暴露所有 API
├── test
│   └── index.js    // 我所建立的用於除錯的指令碼
└── utils   // 專門放工具函式的目錄
    ├── actionTypes.js  // 定義了一些 redux 預留的 action type
    ├── isPlainObject.js  // 用於判斷是否是純物件 
    └── warning.js  // 用於丟擲合適的警告資訊

複製程式碼

可以看出來 redux 的原始碼結構簡單清晰明瞭,幾個主要的(也是僅有的) API 被儘可能的分散到了單個的檔案模組中,我們只需要挨個的看就行了。

(2)index.js

上一小節說到 index.js 是 redux 專案的入口檔案,用於暴露所有的 API,所以我們來看看程式碼:

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'
// 不同的 API 寫在不同的 js 檔案中,最後通過 index.js 統一匯出。

// 這個函式用於判斷當前程式碼是否已經被打包工具(比如 Webpack)壓縮過,如果被壓縮過的話,
// isCrushed 函式的名稱會被替換掉。如果被替換了函式名但是 process.env.NODE_ENV 又不等於 production
// 的時候,提醒使用者使用生產環境下的精簡程式碼。
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.'
  )
}

// 匯出主要的 API。
export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
  __DO_NOT_USE__ActionTypes
}
複製程式碼

我刪除了所有的英文註釋以減小篇幅,如果大家想看原來的註釋,可以去 redux 的專案檢視原始碼。

可以看到在程式的頭部引入了所有的 API 模組以及工具函式,然後在底部統一匯出了。這一部分比較簡單,主要是 isCrushed 函式有點意思。作者用這個函式來判斷程式碼是否被壓縮過(判斷函式名是否被替換掉了)。

這一部分也引用到了工具函式,由於這幾個函式比較簡單,所以可以先看看它們是幹嘛的。

(3)工具函式

除了 compose 函式以外,所有的工具函式都被放在了 utils 目錄下。

actionTypes.js

這個工具模組定義了幾種 redux 預留的 action type,包括 reducer 替換型別、reducer 初始化型別和隨機型別。下面上原始碼:

// 定義了一些 redux 保留的 action type。
// 隨機字串確保唯一性。
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
複製程式碼

可以看出就是返回了一個 ActionTypes 物件,裡面包含三種型別:INIT、REPLACE 和 PROBE_UNKNOW_ACTION。分別對應之前所說的幾種型別,為了防止和使用者自定義的 action type 相沖突,刻意在 type 裡面加入了隨機值。在後面的使用中,通過引入 ActionType 物件來進行對比。

isPlainObject.js

這個函式用於判斷傳入的物件是否是純物件,因為 redux 要求 action 和 state 是一個純物件,所以這個函式誕生了。

上原始碼:

/**
 * 判斷一個引數是否是純物件,純物件的定義就是它的建構函式為 Object。
 * 比如: { name: 'isPlainObject', type: 'funciton' }。
 * 而 isPlainObject 這個函式就不是純物件,因為它的建構函式是 Function。
 * @param {any} obj 要檢查的物件。
 * @returns {boolean} 返回的檢查結果,true 代表是純物件。
 */
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
}
複製程式碼

warning.js

這個函式用於丟擲適當的警告,沒啥好說的。

/**
 * Prints a warning in the console if it exists.
 *
 * @param {String} message The warning message.
 * @returns {void}
 */
export default function warning(message) {
  /* eslint-disable no-console */
  if (typeof console !== 'undefined' && typeof console.error === 'function') {
    console.error(message)
  }
  /* eslint-enable no-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.
    throw new Error(message)
  } catch (e) {} // eslint-disable-line no-empty
}
複製程式碼

compose.js

這個函式用於巢狀呼叫中介軟體(middleware),進行初始化。

/**
 * 傳入一系列的單引數函式作為引數(funcs 陣列),返回一個新的函式,這個函式可以接受
 * 多個引數,執行時會將 funcs 陣列中的函式從右至左進行呼叫。
 * @param {...Function} funcs 一系列中介軟體。
 * @returns {Function} 返回的結果函式。
 * 從右至左呼叫,比如: compose(f, g, h) 將會返回一個新函式
 * (...args) => f(g(h(...args))).
 */
export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }
  // 通過 reduce 方法迭代。
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
複製程式碼

(4)createStore.js

看完了工具函式和入口函式,接下來就要正式步入主題了。我們使用 redux 的重要一步就是通過 createStore 方法建立 store。那麼接下來看看這個方法是怎麼建立 store 的,store 又是個什麼東西呢?

我們看原始碼:

import $$observable from 'symbol-observable'
// 後面會講。
import ActionTypes from './utils/actionTypes'
// 引入一些預定義的保留的 action type。
import isPlainObject from './utils/isPlainObject'
// 判斷一個物件是否是純物件。

// 使用 redux 最主要的 API,就是這個 createStore,它用於建立一個 redux store,為你提供狀態管理。
// 它接受三個引數(第二三個可選),第一個是 reducer,用於改變 redux store 的狀態;第二個是初始化的 store,
// 即最開始時候 store 的快照;第三個引數是由 applyMiddleware 函式返回的 enhancer 物件,使用中介軟體必須
// 提供的引數。
export default function createStore(reducer, preloadedState, enhancer) {
  // 下面這一段基本可以不看,它們是對引數進行適配的。
  /*************************************引數適配****************************************/
  if (
    (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
    (typeof enhancer === 'function' && typeof arguments[3] === 'function')
  ) {
    // 如果傳遞了多個 enhancer,丟擲錯誤。
    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'
    )
  }
  // 如果沒有傳遞預設的 state(preloadedState 為函式型別,enhancer 為未定義型別),那麼傳遞的
  // preloadedState 即為 enhancer。
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      // 如果 enhancer 為不為空且非函式型別,報錯。
      throw new Error('Expected the enhancer to be a function.')
    }
    // 使用 enhancer 對 createStore 進行處理,引入中介軟體。注意此處沒有再傳遞 enhancer 作為引數。實際上 enhancer 會對 createStore 進行處理,然後返回一個實際意義上的 createStore 用於建立 store 物件,參考 applyMiddleware.js。
    return enhancer(createStore)(reducer, preloadedState)
  }
  // 如果 reducer 不是函式型別,報錯。
  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }
  /*********************************************************************************/

  // 在函式內部定義一系列區域性變數,用於儲存資料。
  let currentReducer = reducer  // 儲存當前的 reducer。
  let currentState = preloadedState // 用於儲存當前的 store,即為 state。  
  let currentListeners = [] // 用於儲存通過 store.subscribe 註冊的當前的所有訂閱者。
  let nextListeners = currentListeners  // 新的 listeners 陣列,確保不直接修改 listeners。
  let isDispatching = false // 當前狀態,防止 reducer 巢狀呼叫。

  // 顧名思義,確保 nextListeners 可以被修改,當 nextListeners 與 currentListeners 指向同一個陣列的時候
  // 讓 nextListeners 成為 currentListeners 的副本。防止修改 nextListeners 導致 currentListeners 發生變化。
  // 一開始我也不是很明白為什麼會存在 nextListeners,因為後面 dispatch 函式中還是直接把 nextListeners 賦值給了 currentListeners。
  // 直接使用 currentListeners 也是可以的。後來去 redux 的 repo 搜了搜,發現了一個 issue(https://github.com/reduxjs/redux/issues/2157) 講述了這個做法的理由。
  // 提交這段程式碼的作者的解釋(https://github.com/reduxjs/redux/commit/c031c0a8d900e0e95a4915ecc0f96c6fe2d6e92b)是防止 Array.slice 的濫用,只有在必要的時候呼叫 Array.slice 方法來複制 listeners。
  // 以前的做法是每次 dispatch 都要 slice 一次,導致了效能的降低吧。
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  // 返回 currentState,即 store 的快照。
  function getState() {
    // 防止在 reducer 中呼叫該方法,reducer 會接受 state 引數。
    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
  }

  // store.subscribe 函式,訂閱 dispatch。
  function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }
    // 不允許在 reducer 中進行訂閱。
    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.'
      )
    }

    let isSubscribed = true
    // 每次操作 nextListeners 之前先確保可以修改。
    ensureCanMutateNextListeners()
    // 儲存訂閱者的註冊方法。
    nextListeners.push(listener)

    // 返回一個用於登出當前訂閱者的函式。
    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      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.'
        )
      }

      isSubscribed = false
      // 每次操作 nextListeners 之前先確保可以修改。
      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

  // store.dispatch 函式,用於觸發 reducer 修改 state。
  function dispatch(action) {
    if (!isPlainObject(action)) {
      // action 必須是純物件。
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }

    if (typeof action.type === 'undefined') {
      // 每個 action 必須包含一個 type 屬性,指定修改的型別。
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }
    // reducer 內部不允許派發 action。
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      // 呼叫 reducer 之前,先將標誌位置一。
      isDispatching = true
      // 呼叫 reducer,返回的值即為最新的 state。
      currentState = currentReducer(currentState, action)
    } finally {
      // 呼叫完之後將標誌位置 0,表示 dispatch 結束。
      isDispatching = false
    }

    // dispatch 結束之後,執行所有訂閱者的函式。
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    // 返回當前所使用的 action,這一步是中介軟體巢狀使用的關鍵,很重要。
    return action
  }

  // 一個比較新的 API,用於動態替換當前的 reducers。適用於按需載入,程式碼拆分等場景。
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }
    // 執行預設的 REPLACE 型別的 action。在 combineReducers 函式中有使用到這個型別。
    currentReducer = nextReducer
    dispatch({ type: ActionTypes.REPLACE })
  }

  // 這是為了適配 ECMA TC39 會議的一個有關 Observable 的提案(參考https://github.com/tc39/proposal-observable)所寫的一個函式。
  // 作用是訂閱 store 的變化,適用於所有實現了 Observable 的類庫(主要是適配 RxJS)。
  // 我找到了引入這個功能的那個 commit:https://github.com/reduxjs/redux/pull/1632。
  function observable() {
    // outerSubscribe 即為外部的 subscribe 函式。
    const outerSubscribe = subscribe
    // 返回一個純物件,包含 subscribe 方法。
    return {
      subscribe(observer) {
        if (typeof observer !== 'object' || observer === null) {
          throw new TypeError('Expected the observer to be an object.')
        }

        // 用於給 subscribe 註冊的函式,嚴格按照 Observable 的規範實現,observer 必須有一個 next 屬性。
        function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }

        observeState()
        const unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },

      // $$observable 即為 Symbol.observable,也屬於 Observable 的規範,返回自身。
      [$$observable]() {
        return this
      }
    }
  }
  // 初始化時 dispatch 一個 INIT 型別的 action,校驗各種情況。
  dispatch({ type: ActionTypes.INIT })

  // 返回一個 store 物件。
  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}
複製程式碼

不難發現,我們的 store 物件就是一個純 JavaScript 物件。包含幾個屬性 API,而我們的 state 就儲存在 createStore 這個方法內部,是一個區域性變數,只能通過 getState 方法訪問到。這裡實際上是對閉包的利用,所有我們操作的 state 都儲存在 getState 方法內部的一個變數裡面。直到我們的程式結束(或者說 store 被銷燬),createStore 方法才會被回收,裡面的變數才會被銷燬。

而 subscribe 方法就是對觀察者模式的利用(注意不是釋出訂閱模式,二者有區別,不要混淆),我們通過 subscribe 方法註冊我們的函式,我們的函式會給儲存到 createStore 方法的一個區域性變數當中,每次 dispatch 被呼叫之後,都會遍歷一遍 currentListeners,依次執行其中的方法,達到我們訂閱的要求。

(5)combineReducers.js

瞭解了 createStore 到底是怎麼一回事,我們再來看看 combineReducers 到底是怎麼建立 reducer 的。

我們寫 reducer 的時候,實際上是在寫一系列函式,然後整個到一個物件的屬性上,最後傳給 combineReducers 進行處理,處理之後就可以供 createStore 使用了。

例如:

// 建立我們的 reducers。
const _reducers = {
  items(items = [], { type, payload }) {
    if (type === 'ADD_ITEMS') items.push(payload);
    return items;
  },
  isLoading(isLoading = false, { type, payload }) {
    if (type === 'IS_LOADING') return true;
    return false;
  }
};
// 交給 combineReducers 處理,適配 createStore。
const reducers = combineReducers(_reducers);
// createStore 接受 reducers,建立我們需要的 store。
const store = createStore(reducers);
複製程式碼

那麼 combineReducers 對我們的 reducers 物件進行了哪些處理呢?

下面的程式碼比較長,希望大家能有耐心。

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

/**
 * 用於獲取錯誤資訊的工具函式,如果呼叫你所定義的某個 reducer 返回了 undefined,那麼就呼叫這個函式
 * 丟擲合適的錯誤資訊。
 * 
 * @param {String} key 你所定義的某個 reducer 的函式名,同時也是 state 的一個屬性名。
 * 
 * @param {Object} action 呼叫 reducer 時所使用的 action。
 */
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.`
  )
}

/**
 * 工具函式,用於校驗未知鍵,如果 state 中的某個屬性沒有對應的 reducer,那麼返回報錯資訊。
 * 對於 REPLACE 型別的 action type,則不進行校驗。
 * @param {Object} inputState 
 * @param {Object} reducers 
 * @param {Object} action 
 * @param {Object} unexpectedKeyCache 
 */
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'

  // 如果 reducers 長度為 0,返回對應錯誤資訊。
  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.'
    )
  }

  if (!isPlainObject(inputState)) {
    return (
      `The ${argumentName} has unexpected type of "` +
      {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +             // {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1]
      `". Expected argument to be an object with the following ` +          // 返回的是 inputState 的型別。
      `keys: "${reducerKeys.join('", "')}"`
    )
  }

  // 獲取所有 State 有而 reducers 沒有的屬性,加入到 unexpectedKeysCache。
  const unexpectedKeys = Object.keys(inputState).filter(
    key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
  )

  // 加入到 unexpectedKeyCache。
  unexpectedKeys.forEach(key => {
    unexpectedKeyCache[key] = true
  })

  // 如果是 REPLACE 型別的 action type,不再校驗未知鍵,因為按需載入的 reducers 不需要校驗未知鍵,現在不存在的 reducers 可能下次就加上了。
  if (action && action.type === ActionTypes.REPLACE) return

  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.`
    )
  }
}

/**
 * 用於校驗所有 reducer 的合理性:傳入任意值都不能返回 undefined。
 * @param {Object} reducers 你所定義的 reducers 物件。
 */
function assertReducerShape(reducers) {
  Object.keys(reducers).forEach(key => {
    const reducer = reducers[key]
    // 獲取初始化時的 state。
    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.`
      )
    }
    // 如果初始化校驗通過了,有可能是你定義了 ActionTypes.INIT 的操作。這時候重新用隨機值校驗。
    // 如果返回 undefined,說明使用者可能對 INIT type 做了對應處理,這是不允許的。
    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.`
      )
    }
  })
}

// 把你所定義的 reducers 物件轉化為一個龐大的彙總函式。
// 可以看出,combineReducers 接受一個 reducers 物件作為引數,
// 然後返回一個總的函式,作為最終的合法的 reducer,這個 reducer 
// 接受 action 作為引數,根據 action 的型別遍歷呼叫所有的 reducer。
export default function combineReducers(reducers) {
  // 獲取 reducers 所有的屬性名。
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  // 遍歷 reducers 的所有屬性,剔除所有不合法的 reducer。
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]

    if (process.env.NODE_ENV !== 'production') {
      if (typeof reducers[key] === 'undefined') {
        warning(`No reducer provided for key "${key}"`)
      }
    }

    if (typeof reducers[key] === 'function') {
      // 將 reducers 中的所有 reducer 拷貝到新的 finalReducers 物件上。
      finalReducers[key] = reducers[key]
    }
  }
  // finalReducers 是一個純淨的經過過濾的 reducers 了,重新獲取所有屬性名。
  const finalReducerKeys = Object.keys(finalReducers)

  let unexpectedKeyCache
  // unexpectedKeyCache 包含所有 state 中有但是 reducers 中沒有的屬性。
  if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {}
  }

  let shapeAssertionError
  try {
    // 校驗所有 reducer 的合理性,快取錯誤。
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  // 這就是返回的新的 reducer,一個純函式。每次 dispatch 一個 action,都要執行一遍 combination 函式,
  // 進而把你所定義的所有 reducer 都執行一遍。
  return function combination(state = {}, action) {
    if (shapeAssertionError) {
      // 如果有快取的錯誤,丟擲。
      throw shapeAssertionError
    }

    if (process.env.NODE_ENV !== 'production') {
      // 非生產環境下校驗 state 中的屬性是否都有對應的 reducer。
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      )
      if (warningMessage) {
        warning(warningMessage)
      }
    }

    let hasChanged = false
    // state 是否改變的標誌位。
    const nextState = {}
    // reducer 返回的新 state。
    for (let i = 0; i < finalReducerKeys.length; i++) {   // 遍歷所有的 reducer。
      const key = finalReducerKeys[i]  // 獲取 reducer 名稱。
      const reducer = finalReducers[key]  // 獲取 reducer。
      const previousStateForKey = state[key]  // 舊的 state 值。
      const nextStateForKey = reducer(previousStateForKey, action)  // 執行 reducer 返回的新的 state[key] 值。
      if (typeof nextStateForKey === 'undefined') {
        // 如果經過了那麼多校驗,你的 reducer 還是返回了 undefined,那麼就要丟擲錯誤資訊了。
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      // 把返回的新值新增到 nextState 物件上,這裡可以看出來,你所定義的 reducer 的名稱就是對應的 state 的屬性,所以 reducer 命名要規範!
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
      // 檢驗 state 是否發生了變化。
    }
    // 根據標誌位返回對應的 state。
    return hasChanged ? nextState : state
  }
}
複製程式碼

(6)applyMiddleware.js

combineReducers 方法程式碼比較多,但是實際邏輯還是很簡單的,接下來這個函式程式碼不多,但是邏輯要稍微複雜一點,它就是應用中介軟體的 applyMiddleware 函式。這個函式的思想比較巧妙,值得學習。

import compose from './compose'

//&emsp;用於應用中介軟體的函式,可以同時傳遞多箇中介軟體。中介軟體的標準形式為:
//  const middleware = store => next => action => { /*.....*/ return next(action); }
export default function applyMiddleware(...middlewares) {
  //&emsp;返回一個函式,接受&emsp;createStore&emsp;作為引數。args 引數即為 reducer 和 preloadedState。
  return createStore => (...args) => {
    // 在函式內部呼叫 createStore 建立一個&emsp;store 物件,這裡不會傳遞 enhancer,因為 applyMiddleware 本身就是在建立一個 enhancer,然後給 createStore 呼叫。
    // 這裡實際上是通過 applyMiddleware 把 store 的建立推遲了。為什麼要推遲呢?因為要利用 middleWares 做文章,先初始化中介軟體,重新定義 dispatch,然後再建立 store,這時候建立的 store 所包含的 dispatch 方法就區別於不傳遞 enhancer 時所建立的 dispatch 方法了,其中包含了中介軟體所定義的一些邏輯,這就是為什麼中介軟體可以干預 dispatch 的原因。
    const store = createStore(...args)
    // 這裡對 dispatch 進行了重新定義,不管傳入什麼引數,都會報錯,這樣做的目的是防止你的中介軟體在初始化的時候就
    // 呼叫 dispatch。
    let dispatch = () => {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args) // 注意最後 dispatch 的時候不會訪問上面報錯的那個 dispatch 函式了,因為那個函式被下面的 dispatch 覆蓋了。
    }
    // 對於每一個 middleware,都傳入 middlewareAPI 進行呼叫,這就是中介軟體的初始化。
    // 初始化後的中介軟體返回一個新的函式,這個函式接受 store.dispatch 作為引數,返回一個替換後的 dispatch,作為新的
    // store.dispatch。
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // compose 方法把所有中介軟體串聯起來呼叫。用最終結果替換 dispatch 函式,之後所使用的所有 store.dispatch 方法都已經是
    // 替換了的,加入了新的邏輯。
    dispatch = compose(...chain)(store.dispatch)
    // 初始化中介軟體以後,把報錯的 dispatch 函式覆蓋掉。

    /**
     * middle 的標準形式:
     * const middleware = ({ getState, dispatch }) => next => action => {
     *    // ....
     *    return next(action);
     * }
     * 這裡 next 是經過上一個 middleware 處理了的 dispatch 方法。
     * next(action) 返回的仍然是一個 dispatch 方法。
     */

    return {
      ...store,
      dispatch  // 全新的 dispatch。
    }
  }
}
複製程式碼

程式碼量真的很少,但是真的很巧妙,這裡有幾點很關鍵:

  • compose 方法利用 Array.prototype.reduce 實現中介軟體的巢狀呼叫,返回一個全新的函式,可以接受新的引數(上一個中介軟體處理過的 dispatch),最終返回一個全新的包含新邏輯的 dispatch 方法。你看 middleware 經過初始化後返回的函式的格式:

    next => action => {
      return next(action);
    }
    複製程式碼

    其中 next 可以看成 dispatch,這不就是接受一個 dispatch 作為引數,然後返回一個新的 dispatch 方法嗎?原因就是我們可以認為接受 action 作為引數,然後觸發 reducer 更改 state 的所有函式都是 dispatch 函式。

  • middleware 中介軟體經過初始化以後,返回一個新函式,它接受 dispatch 作為引數,然後返回一個新的 dispatch 又可以供下一個 middleware 呼叫,這就導致所有的 middleware 可以巢狀呼叫了!而且最終返回的結果也是一個 dispatch 函式。

    最終得到的 dispatch 方法,是把原始的 store.dispatch 方法傳遞給最後一個 middleware,然後層層巢狀處理,最後經過第一個 middleware 處理過以後所返回的方法。所以我們在呼叫應用了中介軟體的 dispatch 函式時,從左至右的經過了 applyMiddleware 方法的所有引數(middleware)的處理。這有點像是包裹和拆包裹的過程。

  • 為什麼 action 可以經過所有的中介軟體處理呢?我們再來看看中介軟體的基本結構:

    ({ dispatch, getState }) => next => action => {
      return next(action);
    }
    複製程式碼

    我們可以看到 action 進入函式以後,會經過 next 的處理,並且會返回結果。next 會返回什麼呢?因為第一個 next 的值就是 store.dispatch,所以看看 store.dispatch 的原始碼就知道了。

    function dispatch(action) {
      // 省略了一系列操作的程式碼……
    
      // 返回當前所使用的 action,這一步是中介軟體巢狀使用的關鍵。
      return action
    }
    複製程式碼

    沒錯,store.dispatch 最終返回了 action,由於中介軟體巢狀呼叫,所以每個 next 都返回 action,然後又可以供下一個 next 使用,環環相扣,十分巧妙。

這部分描述的有點拗口,語言捉急但又不想畫圖,各位還是自己多想想好了。

(7)bindActionCreators.js

這個方法沒有太多好說的,主要作用是減少大家 dispatch reducer 所要寫的程式碼,比如你原來有一個 action:

const addItems = item => ({
  type: 'ADD_ITEMS',
  payload: item
});
複製程式碼

然後你要呼叫它的時候:

store.dispatch(addItems('item value'));
複製程式碼

如果你使用 bindActionCreators:

const _addItems = bindActionCreators(addItems, store.dispatch);
複製程式碼

當你要 dispatch reducer 的時候:

_addItems('item value');
複製程式碼

這樣就減少了你要寫的重複程式碼,另外你還可以把所有的 action 寫在一個物件裡面傳遞給 bindActionCreators,就像傳遞給 combineReducers 的物件那樣。

下面看看原始碼:

/**
 * 該函式返回一個新的函式,呼叫新的函式會直接 dispatch ActionCreator 所返回的 action。
 * 這個函式是 bindActionCreators 函式的基礎,在 bindActionCreators 函式中會把 actionCreators 拆分成一個一個
 * 的 ActionCreator,然後呼叫 bindActionCreator 方法。
 * @param {Function} actionCreator 一個返回 action 純物件的函式。
 * @param {Function} dispatch store.dispatch 方法,用於觸發 reducer。
 */
function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

//&emsp;接受一個 actionCreator(或者一個&emsp;actionCreators 物件)和一個&emsp;dispatch&emsp;函式作為引數,
//  然後返回一個函式或者一個物件,直接執行這個函式或物件中的函式可以讓你不必再呼叫&emsp;dispatch。
export default function bindActionCreators(actionCreators, dispatch) {
  // 如果 actionCreators 是一個函式而非物件,那麼直接呼叫 bindActionCreators 方法進行轉換,此時返回
  // 結果也是一個函式,執行這個函式會直接 dispatch 對應的&emsp;action。
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  //&emsp;actionCreators&emsp;既不是函式也不是物件,或者為空時,丟擲錯誤。
  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"?`
    )
  }

  //&emsp;如果&emsp;actionCreators 是一個物件,那麼它的每一個屬性就應該是一個&emsp;actionCreator,遍歷每一個&emsp;actionCreator,
  //&emsp;使用 bindActionCreator 進行轉換。
  const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    //&emsp;把轉換結果繫結到&emsp;boundActionCreators 物件,最後會返回它。
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}
複製程式碼

這部分挺簡單的,主要作用在於把 action creator 轉化為可以直接使用的函式。

三、中介軟體

看了原始碼以後,覺得中介軟體並沒有想象中的那麼晦澀難懂了。就是一個基本的格式,然後你在你的中介軟體裡面可以為所欲為,最後呼叫固定的方法,返回固定的內容就完事了。這就是為什麼大多數 redux middleware 的原始碼都很短小精悍的原因。

看看 redux-thunk 的原始碼:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;
複製程式碼

是不是很短很小?那麼到底幹了什麼讓它這麼受歡迎呢?

實際上 redux-thunk 可以被認為就是:

// 這就是典型的 middleware 格式。
({ dispatch, getState }) => next => action => {
  // next 就是 dispatch 方法。註釋所在的函式就是返回的新的 dispatch。
  // 先判斷一下 action 是不是一個函式。
  if (typeof action === 'function') {
    // 如果是函式,呼叫它,傳遞 dispatch,getState 和 多餘的引數作為 aciton 的引數。
    return action(dispatch, getState, extraArgument);
  }
  // 如果 action 不是函式,直接 nextr呼叫 action,返回結果就完事兒了。
  return next(action);
};
複製程式碼

怎麼樣,是不是很簡單?它乾的事就是判斷了一下 action 的型別,如果是函式就呼叫,不是函式就用 dispatch 來呼叫,很簡單。

但是它實現的功能很實用,允許我們傳遞函式作為 store.dispatch 的引數,這個函式的引數應該是固定的,必須符合上面程式碼的要求,接受 dispatch、getState作為引數,然後這個函式應該返回實際的 action。

我們也可以寫一個自己的中介軟體了:

({ dispatch, getState }) => next => action => {
  return action.then ? action.then(next) : next(action);
}
複製程式碼

這個中介軟體允許我們傳遞一個 Promise 物件作為 action,然後會等 action 返回結果(一個真正的 action)之後,再進行 dispatch。

當然由於 action.then() 返回的不是實際上的 action(一個純物件),所以這個中介軟體可能沒法跟其他中介軟體一起使用,不然其他中介軟體接受不到 action 會出問題。這只是個示例,用於說明中介軟體沒那麼複雜,但是我們可以利用中介軟體做很多事情。

如果想要了解更加複雜的 redux 中介軟體,可以參考:

四、總結

  • Redux 精妙小巧,主要利用了閉包和觀察者模式,然後運用了職責鏈、介面卡等模式構建了一個 store 王國。store 擁有自己的領土,要想獲取或改變 store 裡面的內容,必須通過 store 的各個函式來實現。
  • Redux 相比於 Vuex 而言,程式碼量更小,定製化程度更低,這就導致易用性低於 Vuex,但是可定製性高於 Vuex。這也符合 Vue 和 React 的風格。
  • Redux 原始碼比較好懂,讀懂原始碼更易於掌握 Redux 的使用方法,不要被嚇倒。
  • Redux 中介軟體短小精悍,比較實用。如果從使用方法開始學中介軟體比較難懂的話,可以嘗試從原始碼學習中介軟體。

最後,時間緊迫,水平有限,難免存在紕漏或錯誤,請大家多多包涵、多多指教、共同進步。

歡迎來我的 GitHub 下載專案原始碼;或者 Follow me

相關文章