redux源碼解析

背景

從flux再到mobx,最後到redux。到目前爲止,本人也算展轉了好幾個數據流管理工具了。通過不斷的實踐和思考,我發現對於中大型項目來講,redux無疑是當下綜合考量下最優秀的一個。因此我決定深刻它的源碼,一探究竟。javascript

探究主題

redux的源碼很少,可是都是濃縮的精華,能夠說字字珠璣。 咱們能夠簡單地將redux的源碼劃分爲三部分。html

  • redux這個類庫/包的入口。這裏就對應createStore.js的源碼。
  • state的combine機制。這裏對應於combineReducers.js。
  • 中間件的組合原理。這裏對應於applyMiddleware.js和compose.js。

之因此沒有提到utils文件夾和bindActionCreators.js,這是由於他們都是屬於工具類的代碼,不涉及到redux的核心功能,故略過不表。java

與以上所提到的三部分代碼相呼應的三個主題是:react

  • createStore.js到底作了什麼?
  • redux是如何將各個小的reducer combine上來,組建總的,新的state樹呢?
  • 中間件機制是如何工做的呢?它的原理又是什麼呢?

下面,讓咱們深刻到源碼中,圍繞着這三個主題,來揭開redux的神祕面紗吧!git

1、createStore.js到底作了什麼?

爲了易於理解,咱們不妨使用面向對象的思惟去表達。 一句話,createStore就是Store類的「構造函數」。github

Store類主要有私有屬性:express

  • currentReducer
  • currentState
  • currentListeners
  • isDispatching

公有方法:編程

  • dispatch
  • subscribe
  • getState
  • replaceReducer

而在這幾個公有方法裏面,getStatereplaceReducer無疑就是特權方法。由於經過getState能夠讀取私有屬性currentState的值,而經過replaceReducer能夠對私有屬性currentReducer進行寫操做。雖然咱們是使用普通的函數調用來使用createStore,可是它本質上能夠看做是一個構造函數調用。經過這個構造函數調用,咱們獲得的是Store類的一個實例。這個實例有哪些方法,咱們經過源碼能夠一目瞭然:redux

return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
複製代碼

官方推薦咱們全局只使用一個Store類的實例,咱們能夠將此看做是單例模式的踐行。 其實,咱們也能夠換取另一個角度去解讀createStore.js的源碼:模塊模式。 爲何可行呢?咱們能夠看看《你不知道的javascript》一文中如何定義javascript中的模塊模式的實現:api

  • 必須有外部的嵌套函數,該函數必須至少被調用一次(每次調用都會建立一個新的模塊實例)。
  • 嵌套函數必須返回至少一個內部函數,這樣內部函數才能在私有做用域中造成閉包,而且能夠訪問或者修改私有的狀態。

咱們再來看看createStore.js的源碼的骨架。首先,最外層的就是一個叫createStore的函數,而且它返回了一個字面量對象。這個字面量對象的屬性值保存着一些被嵌套函數(內部函數)的引用。因此咱們能夠說createStore函數實際上是返回了至少一個內部函數。咱們能夠經過返回的內部函數來訪問模塊的私有狀態(currentReducer,currentState)。而咱們在使用的時候,也是至少會調用一次createStore函數。綜上所述,咱們能夠把createStore.js的源碼看做是一次模塊模式的實現。

export default function createStore(reducer, preloadedState, enhancer) {
    //......
    //......
    return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}
複製代碼

其實不管是從類模式仍是模塊模式上解讀,createStore.js本質上就是基於閉包的原理實現了數據的封裝-經過私有變量來防止了全局污染和命名衝突,同時還容許經過特定的方法來訪問特定的私有變量。

2、redux是如何將各個小的reducer combine上來?state樹的初始化如何進行的?後面又是如何更新的?

首先,咱們得搞清楚什麼是reducer?接觸過函數式編程的人都知道,reducer是很純粹的函數式編程概念。簡單來講,它就是一個函數。只不過不一樣於普通函數(實現特定的功能,可重複使用的代碼塊)這個概念,它還多了一層概念-「純」。對,reducer必須是純函數。所謂的「純函數」是指,一樣的輸入會產生一樣的輸出。好比下面的add函數就是一個純函數:

function add(x,y){
    return x+y;
}
複製代碼

由於你不管第幾回調用add(1,2),結果都是返回3。好,解釋到這裏就差很少了。由於要深究函數式編程以及它的一些概念,那是說上幾天幾夜都說不完的。在這裏,咱們只須要明白,reducer是一個有返回值的函數,而且是純函數便可。爲何這麼說呢?由於整個應用的state樹正是由每一個reducer返回的值所組成的。咱們能夠說reducer的返回值是組成整顆state樹的基本單元。而咱們這個小節裏面要探究的combine機制本質上就是關於如何組織state樹-一個關於state樹的數據結構問題。 在深刻探究combine機制及其原理以前,咱們先來探討一下幾個關於「閉包」的概念。

什麼是「閉包」?固然,這裏的閉包是指咱們常說的函數閉包(除了函數閉包,還有對象閉包)。關於這個問題的答案可謂仁者見仁,智者見智。在這裏,我就不做過多的探討,只是說一下我對閉包的理解就好。由於我正是靠着這幾個對閉包的理解來解讀reducer的combine機制及其原理的。我對閉包的理解主要是環繞在如下概念的:

  1. 造成一個能夠觀察獲得的閉包(代碼書寫期)
  2. 產生某個閉包(代碼運行期)
  3. 進入某個閉包(代碼運行期)

首先,說說「造成一個能夠觀察獲得的閉包」。要想造成一個能夠觀察獲得的閉包,咱們必須知足兩個前提條件:

  • (詞法)做用域嵌套。
  • 內層做用域對外層做用域的標識符進行了訪問/引用。

其次,說說「產生某個閉包」。正如周愛民所說的,不少人沒有把閉包說清楚,那是由於他們忽略了一個事實:閉包是一個運行時的概念。對此,我十分認同。那麼怎樣才能產生一個閉包呢?這個問題的答案是在前面所說的那個前提條件再加上一個條件:

  • 經過調用嵌套函數,將被嵌套的函數實例引用傳遞到外層做用域(嵌套函數所在的詞法做用域)以外。

咱們能夠簡單地總結一下:當咱們對一個在內部實現中造成了一個能夠觀察獲得的閉包的函數進行調用操做的時候,咱們就說「產生某個閉包」。

最後,說說「進入某個閉包」。同理,「進入某個閉包」跟「產生某個閉包」同樣,都是一個運行時的概念,他們都是在函數被調用的時候所發生的。只不過,「進入某個閉包」所對應的函數調用是不同而已。當咱們對傳遞到嵌套做用域以外的被嵌套函數進行調用操做的時候,咱們能夠說「進入某個閉包」。

好了,通過一大堆的概念介紹後,你可能會有點雲裏霧裏的,不知道我在說什麼。下面咱們結合combineReducers.js的源碼和實際的使用例子來娓娓道來。

二話不說,咱們來看看combineReducers.js的總體骨架。

export default function combineReducers(reducers) {
  const finalReducers = {}
  //省略了不少代碼
  const finalReducerKeys = Object.keys(finalReducers)
  //省略了不少代碼
  
  return function combination(state = {}, action) {
    let hasChanged = false
    const nextState = {}
     for (let i = 0; i < finalReducerKeys.length; i++) {
       const key = finalReducerKeys[i]
       const reducer = finalReducers[key]
       //省略了不少代碼
    }
    return hasChanged ? nextState : state
  }
}
複製代碼

在這裏,咱們能夠看到combineReducers的詞法做用域裏面嵌套着combination的詞法做用域,而且combination的詞法做用域持有着它的上層做用域的好幾個變量。這裏,只是摘取了finalReducers和finalReducerKeys。因此,咱們說,在combineReducers函數的內部,有一個能夠被觀察獲得的閉包。而這事是redux類庫幫咱們作了。

const todoReducer=combineReducers({
    list:listReducer,
    visibility:visibilityReducer
})
複製代碼

當咱們像上面那樣調用combineReducers,在運行期,這裏就會產生一個閉包

const todoReducer=combineReducers({
    list:listReducer,
    visibility:visibilityReducer
})
const prevState = {
    list:[],
    visibility:'showAll'
}
const currAction = { 
    type:"ADD_TODO",
    payload:{
        id:1,
        text:'23:00 準時睡覺',
        isFinished: false
    }
}
todoReducer(prevState,currAction)
複製代碼

當咱們像上面那樣調用combineReducers()返回的todoReducer,在運行期,咱們就會開始進入一個閉包

這裏順便提一下,與combineReducers()函數的實現同樣。createStore()函數內部的實現也有一個「能夠觀察獲得的閉包」,在這裏就不贅言了。

其實提到閉包,這裏還得提到函數實例。函數狹義上來說應該是指代碼書寫期的腳本代碼,是一個靜態概念。而函數實例偏偏相反,是一個函數在代碼運行期的概念。函數實例與閉包存在對應關係。一個函數實例能夠對應一個閉包,多個函數實例能夠共享同一個閉包。因此,閉合也是一個運行期的概念。明白這一點,對於理解我後面所說相當重要。

在這個小節,咱們提出的疑問是「redux是如何將各個小的reducer combine上來,組建總的state樹?」。答案是經過combineReducers閉包的層層嵌套。經過查看源碼咱們知道,調用combineReducers,咱們能夠獲得一個combination函數的實例,而這個函數實例有一個對應的閉包。也就是說,在代碼書寫期,redux類庫[造成了一個能夠觀察獲得的閉包]。咱們的js代碼初始加載和執行的時候,經過調用combineReducers()函數,咱們獲得了一個函數實例,咱們把這個函數實例的引用保存在調用combineReducers函數時傳進去的對象屬性上。正如上面所說每一個函數實例都會有一個與之對應的閉包。所以咱們理解爲,咱們經過調用combineReducers(),事先(注意:這個「事先」是相對代碼正式能夠跟用戶交互階段而言)生成了一個閉包,等待進入。由於combineReducers的使用是層層嵌套的,因此這裏產生了一個閉包鏈。既然閉包鏈已經準備好了,那麼咱們何時進入這條閉包鏈呢?也許你會有個疑問:「進入閉包有什麼用?」。由於閉包的本質是將數據保存在內存當中,因此這裏的答案就是:「進入閉包,是爲了訪問該閉包在產生時保存在內存當中的數據」。在redux中,state被設計爲一個樹狀結構,而咱們的combine工做又是自底向上來進行的。能夠這麼講,在自底向上的combine工做中,當咱們完成最後一次調用combineReducers時,咱們的閉包鏈已經準備穩當了,等待咱們的進入。現實開發中,因爲都採用了模塊化的開發方式,combine工做的實現代碼都是分散在各個不一樣的文件裏面,最後是經過引用傳遞的方式來彙總在一塊。假如一開始咱們把它們寫在一塊兒,那麼咱們就很容易看到combine工做的全貌。就以下代碼所演示的那樣:

const finalReducer = combineReducers({
    page1:combineReducers({
        counter:counterReducer,
        todo:combineReducers({
            list:listReducer,
            visibility:visibilityReducer
        })
    })
})

複製代碼

咱們能夠說當代碼執行到[給finalReducer變量賦值]的時候,咱們的閉包鏈已經準備好了,等待咱們的進入。 接下來,咱們不由問:「那到底何時進入呢?」。答案是:「finalReducer被調用的時候,咱們就開始進入這個閉包鏈了。」。那麼finalReducer何時,在哪裏被調用呢?你們能夠回想如下,咱們最後一次消費finalReducer是否是就是調用createStore()時做爲第一個參數傳入給它?是的。因而乎,咱們就來到createStore.js的源碼中來看看:

import $$observable from 'symbol-observable'

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

/** * Creates a Redux store that holds the state tree. * The only way to change the data in the store is to call `dispatch()` on it. * * There should only be a single store in your app. To specify how different * parts of the state tree respond to actions, you may combine several reducers * into a single reducer function by using `combineReducers`. * * @param {Function} reducer A function that returns the next state tree, given * the current state tree and the action to handle. * * @param {any} [preloadedState] The initial state. You may optionally specify it * to hydrate the state from the server in universal apps, or to restore a * previously serialized user session. * If you use `combineReducers` to produce the root reducer function, this must be * an object with the same shape as `combineReducers` keys. * * @param {Function} [enhancer] The store enhancer. You may optionally specify it * to enhance the store with third-party capabilities such as middleware, * time travel, persistence, etc. The only store enhancer that ships with Redux * is `applyMiddleware()`. * * @returns {Store} A Redux store that lets you read the state, dispatch actions * and subscribe to changes. */
export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    return enhancer(createStore)(reducer, preloadedState)
  }

  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() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  /** * Reads the state tree managed by the store. * * @returns {any} The current state tree of your application. */
  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
  }

  /** * Adds a change listener. It will be called any time an action is dispatched, * and some part of the state tree may potentially have changed. You may then * call `getState()` to read the current state tree inside the callback. * * You may call `dispatch()` from a change listener, with the following * caveats: * * 1. The subscriptions are snapshotted just before every `dispatch()` call. * If you subscribe or unsubscribe while the listeners are being invoked, this * will not have any effect on the `dispatch()` that is currently in progress. * However, the next `dispatch()` call, whether nested or not, will use a more * recent snapshot of the subscription list. * * 2. The listener should not expect to see all state changes, as the state * might have been updated multiple times during a nested `dispatch()` before * the listener is called. It is, however, guaranteed that all subscribers * registered before the `dispatch()` started will be called with the latest * state by the time it exits. * * @param {Function} listener A callback to be invoked on every dispatch. * @returns {Function} A function to remove this change listener. */
  function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }

    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 http://redux.js.org/docs/api/Store.html#subscribe for more details.'
      )
    }

    let isSubscribed = true

    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 http://redux.js.org/docs/api/Store.html#subscribe for more details.'
        )
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

  /** * Dispatches an action. It is the only way to trigger a state change. * * The `reducer` function, used to create the store, will be called with the * current state tree and the given `action`. Its return value will * be considered the **next** state of the tree, and the change listeners * will be notified. * * The base implementation only supports plain object actions. If you want to * dispatch a Promise, an Observable, a thunk, or something else, you need to * wrap your store creating function into the corresponding middleware. For * example, see the documentation for the `redux-thunk` package. Even the * middleware will eventually dispatch plain object actions using this method. * * @param {Object} action A plain object representing 「what changed」. It is * a good idea to keep actions serializable so you can record and replay user * sessions, or use the time travelling `redux-devtools`. An action must have * a `type` property which may not be `undefined`. It is a good idea to use * string constants for action types. * * @returns {Object} For convenience, the same action object you dispatched. * * Note that, if you use a custom middleware, it may wrap `dispatch()` to * return something else (for example, a Promise you can await). */
  function dispatch(action) {
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }

    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }

    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }

    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

  /** * Replaces the reducer currently used by the store to calculate the state. * * You might need this if your app implements code splitting and you want to * load some of the reducers dynamically. You might also need this if you * implement a hot reloading mechanism for Redux. * * @param {Function} nextReducer The reducer for the store to use instead. * @returns {void} */
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }

    currentReducer = nextReducer
    dispatch({ type: ActionTypes.REPLACE })
  }

  /** * Interoperability point for observable/reactive libraries. * @returns {observable} A minimal observable of state changes. * For more information, see the observable proposal: * https://github.com/tc39/proposal-observable */
  function observable() {
    const outerSubscribe = subscribe
    return {
      /** * The minimal observable subscription method. * @param {Object} observer Any object that can be used as an observer. * The observer object should have a `next` method. * @returns {subscription} An object with an `unsubscribe` method that can * be used to unsubscribe the observable from the store, and prevent further * emission of values from the observable. */
      subscribe(observer) {
        if (typeof observer !== 'object') {
          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
      }
    }
  }

  // When a store is created, an "INIT" action is dispatched so that every
  // reducer returns their initial state. This effectively populates
  // the initial state tree.
  dispatch({ type: ActionTypes.INIT })

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

複製代碼

果不其然,在dispatch方法的內部實現,咱們看到了咱們傳入的finalReducer被調用了(咱們縮減了部分代碼來看):

export default function createStore(reducer, preloadedState, enhancer) {
    ...... // other code above
     let currentReducer = reducer
     ...... // other code above
    function dispatch(action) {
     ...... // other code above

    try {
      isDispatching = true
      // 注意看,就是在這裏調用了咱們傳入的「finalReducer」
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }
    ...... // other code rest
  }
}
複製代碼

也就是說,咱們在調用dispatch(action)的時候(準確地來講,第一次調用dispatch的並非咱們,而是redux類庫本身),咱們就開始進入早就準備好的combination閉包鏈了。 到這裏咱們算是回答了這個小節咱們本身給本身提出的第一個小問題:「redux是如何將各個小的reducer combine上來?」。下面,咱們一塊兒來總結如下:

  1. redux類庫在代碼書寫期就[造成了一個能夠觀察獲得的閉包]-combination閉包。
  2. (redux類庫的)用戶本身經過自底向上地調用combineReducers()來產生了一個層層嵌套的閉包鏈。
  3. 最終,在咱們以一個action去調用dispatch()方法的時候,咱們就會進入了這個閉包鏈。

既然combine工做的原理咱們已經搞清楚了,那麼第二個小問題和第三個小問題就迎刃而開了。

首先咱們來看看createStore的函數簽名:

createStore(finalreducer, preloadedState, enhancer) => store實例
複製代碼

state樹的初始值取決於咱們調用createStore時的傳參狀況。

  • 假如,咱們有傳遞preloadedState這個實參,那麼這個實參將做爲state樹的初始值。
  • 假如,咱們沒有傳遞preloadedState這個實參,那麼state樹的初始化將會是由redux類庫本身來完成。具體地說,redux在內部經過dispatch一個叫{ type: ActionTypes.INIT }的action來完成了state樹的初始化。在這裏,咱們得提到reducer的初始值。reducer的初始值指的是reducer在沒有匹配到action.type的狀況下返回的值。一個純正的reducer,咱們通常會這麼寫:
function counterReducer(state = 0, action){
    switch (action.type) {
        case "INCREMENT":
            return state + 1;
        case "DECREMENT":
            return state - 1;
        default:
            return state;
    }
}
複製代碼

在上面的代碼中,若是當前dispatch的action在counterReducer裏面沒有找到匹配的action.type,那麼就會走default分支。而default分支的返回值又等同具備默認值爲0的形參state。因此,咱們能夠說這個reducer的初始值爲0。注意,形參state的默認值並不必定就是reducer的初始值。考慮下面的寫法(這種寫法是不被推薦的):

function counterReducer(state = 0, action){
    switch (action.type) {
        case "INCREMENT":
            return state + 1;
        case "DECREMENT":
            return state - 1;
        default:
            return1;
    }
}
複製代碼

在switch語句的default條件分支裏面,咱們是return -1,而不是return state。在這種狀況下,reducer的初始值是-1,而不是0 。因此,咱們不能說reducer的初始值是等同於state形參的默認值了。

也許你會問:「爲何dispatch 一個叫{ type: ActionTypes.INIT }action就能將各個reducer的初始值收集上來呢?」。咱們先來看看ActionTypes.INIT究竟是什麼。

utils/actionTypes.js的源碼:

/** * These are private action types reserved by Redux. * For any unknown actions, you must return the current state. * If the current state is undefined, you must return the initial state. * Do not reference these action types directly in your code. */
const ActionTypes = {
  INIT:
    '@@redux/INIT' +
    Math.random()
      .toString(36)
      .substring(7)
      .split('')
      .join('.'),
  REPLACE:
    '@@redux/REPLACE' +
    Math.random()
      .toString(36)
      .substring(7)
      .split('')
      .join('.')
}
複製代碼

能夠看到,ActionTypes.INIT的值就是一個隨機字符串。也就是說,在redux類庫內部(createStore函數內部實現裏面)咱們dispatch了一個type值爲隨機字符串的action。這對於用戶本身編寫的reducer來講,99.99%都不可能找到與之匹配的action.type 。因此,ActionTypes.INIT這個action最終都會進入default條件分支。也就是說,做爲響應,各個reducer最終會返回本身的初始值。而決定state樹某個子樹結構的字面量對象,咱們早就在產生閉包時存在在內存當中了。而後,字面量對象(表明着結構) + 新計算出來的new state(表明着數據) = 子state樹。最後,經過組合層層子state樹,咱們就初始化了一顆完整的state樹了。

回答完第二個問題後,那麼接着回答第三個問題:「state樹是如何更新的?」。其實,state樹跟經過dispatch一個type爲ActionTypes.INIT的action來初始化state樹的原理是同樣的。它們都是經過給全部最基本的reducer傳入一個該reducer負責管理的節點的previous state和當前要廣播的action來產出一個最新的值,也就是說: previousState + action => newState。不一樣於用於state樹初始化的action,後續用於更新state樹的action通常會帶有payload數據,而且會在某個reducer裏面匹配到對應action.type,從而計算出最新的state值。從這裏咱們也能夠看出了,reducer的本質是計算。而計算也正是計算機的本質。

最後,咱們經過對比combineReducers函數的調用代碼結構和其生成的state樹結構來加深對combine機制的印象:

// 寫在一塊的combineReducers函數調用
const finalReducer = combineReducers({
    page1:combineReducers({
        counter:counterReducer,// 初始值爲0
        todo:combineReducers({
            list:listReducer,// 初始值爲[]
            visibility:visibilityReducer// 初始值爲"showAll"
        })
    })
})

// 與之對應的state樹的初始值
{
    page1:{
        counter:0,
        todo:{
            list:[],
            visibility:"showAll"
        }
    }
}
複製代碼

3、中間件運行機制與原理是什麼呢?

在探討redux中間件機制以前,咱們不妨來回答一下中間件是什麼?答曰:「redux的中間件其實就是一個函數。

更確切地講是一個包了三層的函數,形如這樣:

const middleware = function (store) {

            return function (next) {

                return function (action) {
                    // maybe some code here ...
                    next(action)
                    // maybe some code here ...
                }
            }
        }
複製代碼

既然咱們知道了redux的中間件長什麼樣,那麼咱們不由問:「爲何redux的中間件長這樣能有用呢?整個中間件的運行機制又是怎樣的呢?」

咱們不妨回顧一下,咱們平時使用中間件的大體流程:

  1. 寫好一箇中間件;

  2. 註冊中間件,以下:

import Redux from 'redux';

const enhancer = Redux.applyMiddleware(middleware1,middleware2)
複製代碼
  1. 把enhancer傳入redux的createStore方法中,以下:
import Redux from 'redux';

let store = Redux.createStore(counterReducer,enhance);
複製代碼

其實要搞清楚中間件的運行機制,無非就是探索咱們寫的這個包了三層的函數是如何被消費(調用)。咱們寫的中間件首次被傳入了applyMiddleware方法,那咱們來瞧瞧這個方法的真面目吧。源文件applyMiddleware.js的代碼以下:

import compose from './compose'

/**
 * Creates a store enhancer that applies middleware to the dispatch method
 * of the Redux store. This is handy for a variety of tasks, such as expressing
 * asynchronous actions in a concise manner, or logging every action payload.
 *
 * See `redux-thunk` package as an example of the Redux middleware.
 *
 * Because middleware is potentially asynchronous, this should be the first
 * store enhancer in the composition chain.
 *
 * Note that each middleware will be given the `dispatch` and `getState` functions
 * as named arguments.
 *
 * @param {...Function} middlewares The middleware chain to be applied.
 * @returns {Function} A store enhancer applying the middleware.
 */
export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`
      )
    }
    let chain = []

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }

    chain = middlewares.map(middleware => middleware(middlewareAPI)) // 這裏剝洋蔥模型的第一層
    dispatch = compose(...chain)(store.dispatch) //  第二個函數調用剝洋蔥模型的第二層

    return {
      ...store,
      dispatch
    }
  }
  //return enhancer(createStore)(reducer, preloadedState)
}

複製代碼

又是function返回function,redux.js處處可見閉包,可見做者對閉包的應用已經到隨心應手,爐火純青的地步了。就是上面幾個簡短卻不簡單的代碼,實現了redux的中間件機制,可見redux源代碼的凝練程度可見一斑。

applyMiddleware返回的函數將會在傳入createStore以後反客爲主:

export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    return enhancer(createStore)(reducer, preloadedState) // 反客爲主的代碼
  }
  // 剩餘的其餘代碼
  }
複製代碼

咱們手動傳入createStore方法的reducer, preloadedState等參數繞了一圈,最終仍是原樣傳遞到applyMiddleware方法的內部createStore。這就是這行代碼:

const store = createStore(...args)
複製代碼

順便提一下,在applyMiddleware方法裏面的const store = createStore(...args)所建立的store與咱們日常調用redux.createStore()所產生的store是沒什麼區別的。

來到這裏,咱們能夠講applyMiddleware方法裏面的middlewareAPI對象所引用的getState和dispatch都是未經改造的,原生的方法。而咱們寫中間件的過程就是消費這兩個方法(主要是加強dispatch,使用getState從store中獲取新舊state)的過程。

正如上面所說的,咱們的中間件其實就是一個包了三層的函數。借用業界的說法,這是一個洋蔥模型。咱們中間件的核心代碼通常都是寫在了最裏面那一層。那下面咱們來看看咱們傳給redux類庫的中間件這個洋蔥,是如何一層一層地被撥開的呢?而這就是中間件運行機制之所在。

剝洋蔥的核心代碼只有兩行代碼(在applyMiddleware.js):

//  ......

chain = middlewares.map(middleware => middleware(middlewareAPI)) // 這裏剝洋蔥模型的第一層
dispatch = compose(...chain)(store.dispatch) //  第二個函數調用剝洋蔥模型的第二層

// .......
複製代碼

還記得咱們傳給applyMiddleware方法的是一箇中間件數組嗎?經過對遍歷中間件數組,以middlewareAPI入參,咱們剝開了洋蔥模型的第一層。也便是說chain數組中的每個中間件都是這樣的:

function (next) {
    return function (action) {
        // maybe some code here ...
        next(action)
        // maybe some code here ...
    }
}
複製代碼

剝開洋蔥模型的第一層的同時,redux往咱們的中間件注入了一個簡化版的store對象(只有getState方法和dispatch方法),僅此而已。而剝開洋蔥模型的第二層,纔是整個中間件運行機制的靈魂所在。咱們目光往下移,只見「寥寥數語」:

dispatch = compose(...chain)(store.dispatch) //  第二個函數調用剝洋蔥模型的第二層
複製代碼

是的,compose方法纔是核心所在。咱們不防來看看compose方法源碼:

/**
 * Composes single-argument functions from right to left. The rightmost
 * function can take multiple arguments as it provides the signature for
 * the resulting composite function.
 *
 * @param {...Function} funcs The functions to compose.
 * @returns {Function} A function obtained by composing the argument functions
 * from right to left. For example, compose(f, g, h) is identical to doing
 * (...args) => f(g(h(...args))).
 */

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

  if (funcs.length === 1) {
    return funcs[0]
  }
  // 此處的a和b是指中間件的第二層函數
  return funcs.reduce((a, b) => (...args) => a(b(...args)));
  
  // 爲了看清楚嵌套層數,咱們轉成ES5語法來看看
  // return funcs.reduce(function(a,b){
  //   return function(...args){
  //     return a(b(...args));
  //   }
  // })
}

複製代碼

compose方法的實現跟任何一個函數式編程範式裏面的「compose」概念的實現沒有多大的差異(又是函數嵌套 + return,哎,心累)。要想理解這行代碼,我的以爲須要用到幾個概念:

  • 函數能夠被當成值來傳遞
  • 延遲求值
  • 閉包

咱們使用了數組的reduce API把函數看成值來遍歷了一遍,每一個函數都被閉包在最後返回的那個函數的做用域裏面。雖然表面上看到了對每一個函數都使用了函數調用操做符,可是實際上每一個函數都延遲執行了。這段代碼須要在腦海裏面好好品味一下。不過,爲了快速理解後面的代碼,咱們的不防把它對應到這樣的心智模型中去:

compose(f,g,h) === (...args) => f(g(h(...args)))
複製代碼

一旦理解了compose的原理,咱們就會知道咱們中間件的洋蔥模型的第二層是在compose(...chain)(store.dispatch)的最後一個函數調用發生時剝開的。而在剝開的時候,咱們每個中間件的第二層函數都會被注入一個通過後者中間件(按照註冊中間時的順序來算)加強後的dispatch方法。f(g(h(...args)))調用最後返回的是第一個中間件的最裏層的那個函數,以下:

function (action) {
    //  ...
    next(action)
    //  ...
}
複製代碼

也便是說,用戶調用的最終是加強(通過各個中間件魔改)後的dispatch方法。由於這個被中間件加強後的dispatch關聯着一條閉包鏈(這個加強後的dispatch相似於上面一章節所提到的totalReducer,它也是關聯着一個閉包鏈),因此對於一個嚴格使用redux來管理數據流的應用,咱們能夠這麼說:中間件核心代碼(第三層函數)執行的導火索就是緊緊地掌握在用戶的手中。

講到這裏,咱們已經基本上摸清了中間件機制運行的原理了。下面,總結一下:

  1. 調用applyMiddleware方法的時候,redux會把咱們的中間件剝去兩層外衣。剝開第一層的時候,往裏面注入簡化版的store實例;剝開第二層的時候,往裏面注入上一個中間件(按照註冊中間時的順序來算)的最裏層的函數。以此類推,中間件數組最後一箇中間件的第二層被剝開的時候,注入的是原生的dispatch方法。
  2. 用戶調用的dispatch方法是通過全部中間件加強後的dispatch方法,已然不是原生的dispatch方法了。
  3. 用戶調用dispatch方法的時候,程序會進入一個環環相扣的閉包鏈中。也便是說,用戶的每dispatch一次action,咱們註冊的全部的中間件的第三層函數都會被執行一遍。

上面所提原理的心智模型圖大概以下:

假如咱們已經理解了中間件的運行機制與原理,咱們不防經過自問自答的方式來鞏固一下咱們的理解。咱們將會問本身三個問題:

1.不一樣的中間件註冊順序對程序的執行結果有影響嗎?若是有,爲何有這樣的影響?

答曰:有影響的。由於中間件的核心代碼是寫在第三層函數裏面的,因此當咱們在討論中間件的執行順序時,通常是指中間件第三層函數的被執行的順序。正如上面探索中間件的運行機制所指出的,中間件第三層函數的執行順序與中間件的註冊順序是一致的,都是從左到右。因此,不一樣的註冊順序也就意味着不一樣的中間件調用順序。形成這種影響的緣由我能想到的是如下兩種場景:

  1. 後一箇中間件對前一箇中間件產生依賴。舉個例子,假如某個中間件B須要依賴上一個中間件A對action進行添加的某個payload屬性來判斷是否執行某些邏輯,而在實際註冊的時候,你卻把中間件B放在所依賴中間件A的前面,那麼這個中間件B的那段判斷邏輯頗有可能永遠都不會被執行。
  2. 是否有影響有時候取決與該中間件所想要實現的功能。舉個例子,假如某個中間件想要實現的功能是統計dispatch一次action所耗費的時間,它的大概實現是這樣的:
let startTimeStamp = 0
const timeLogger = function (store) {
    return function (next) {
        return function (action) {
            startTimeStamp = Date.now();
            next(action)
            console.log(`整個dispatch耗費的時間是:${Date.now() - startTimeStamp}毫秒`, )
        }
    }
}
複製代碼

基因而爲了實現這種功能的目的,那麼這個timeLogger中間件就必須是放在中間件數組的第一位了。不然的話,統計出來的耗時就不是整個dispatch執行完所耗費的時間了。

2.咱們都知道中間件的核型代碼會放在第三層函數那裏,那若是我在第一層和第二層函數裏面就開始寫點代碼(消費store這個實參)會是怎樣呢?

在上面已經討論過中間件運行機制片斷中,咱們瞭解到,中間件的第一層和第二層函數都是在咱們調用applyMiddleware()時所執行的,也就是說不一樣於中間件的第三層函數,第一層和第二層函數在應用的整個生命週期只會被執行一次。

let dispatch = () => {
  throw new Error(
    `Dispatching while constructing your middleware is not allowed. ` +
      `Other middleware would not be applied to this dispatch.`
  )
}
let chain = []

const middlewareAPI = {
  getState: store.getState,
  dispatch: (...args) => dispatch(...args)
}
複製代碼

看這幾行代碼可知,若是咱們在第一層函數裏面就調用dispatch方法,應該是會報錯的。稍後咱們來驗證一下。 而對於調用getState()而言,由於在chain = middlewares.map(middleware => middleware(middlewareAPI))這行代碼以前,咱們已經建立了store實例,也便是說ActionTypes.INIT已經dispatch了,因此,state樹的初始值已經計算出來的。這個時候,若是咱們調用middlewareAPI的getState方法,獲得的應該也是初始狀態的state樹。咱們不防追想下去,在中間件的第二層函數裏面消費middlewareAPI的這個兩個方法,應該會獲得一樣的結果。由於,dispatch變量仍是指向同一函數引用,而應用中的第二個action也沒有dispatch 出來,因此state樹的值不變,仍是初始值。下面,咱們不防寫個中間件來驗證一下咱們的結論:

const testMiddleware = function (store) {
    
    store.dispatch(); // 通過驗證,會報錯:"Dispatching while constructing your middleware is not allowed. Other middleware would not be applied to this dispatch."
    
    console.log(store.getState()); // 在第一層,拿到的是整個state樹的初始值(已驗證)
    
    return function (next) {
    
        store.dispatch(); // 這裏也不能寫,也會包一樣的錯.
    
        console.log(store.getState()); // 在第二層,拿到的也是整個state樹的初始值(已驗證)

        return function (action) {
            next(action)
        }
    }
}
複製代碼

3.在中間件的第三層函數裏,寫在next(action)以前的代碼與寫在以後的代碼會有什麼不一樣嗎?

正如咱們給出的步驟四的心智模型圖,加強後的dispatch方法的代碼執行流程是夾心餅乾式的。對於每個中間件(第三層函數)而言,寫在next(action)語句先後的語句分別是夾心餅乾的上下層,中心層永遠是通過後一個(這裏按照中間件註冊順序來講)中間件加強後的dispatch方法,也就是此時的next(action)。下面咱們不防用代碼去驗證一下:

const m1 = store=> next=> action=> {
    console.log('m1: before next(action)');
    next(action);
    console.log('m1: after next(action)');
}

const m2 = store=> next=> action=> {
    console.log('m2: before next(action)');
    next(action);
    console.log('m2: after next(action)');
}

// 而後再在原生的dispatch方法裏面打個log
function dispatch(action) {
    console.log('origin dispatch');
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }
    // ........
  }
複製代碼

通過驗證,打印的順序以下:

m1: before next(action)
m2: before next(action)
origin dispatch
m1: after next(action)
m2: after next(action)
複製代碼

是否是挺像夾心餅乾的啊?

回到這個問題的自己,咱們不防也想一想第二章節裏面獲得過的結論:dispatch一次action,其實是一次計算,計算出state樹的最新值。也就是說,只有原生的dispatch方法執行以後,咱們才能拿到最新的state。結合各個中間件與原生的dispatch方法的執行順序之前後,這個問題的答案就呼之欲出了。

那就是:「在調用getState方法去獲取state值的場景下,寫在next(action)以前的代碼與寫在以後的代碼是不一樣的。這個不一樣點在於寫在next(action)以前的getState()拿到的是舊的state,而寫在next(action)以後的getState()拿到的是新的state。」

中間件應用

既然咱們都探索了這麼多,最後,咱們不妨寫幾個簡單的中間件來鞏固一下戰果。

  1. 實現一個用於統計dispatch執行耗時的中間件。
let startTimeStamp = 0
const timeLogger = function (store) {
       return function (next) {
           return async function (action) {
               startTimeStamp = Date.now();
               await next(action)
               console.log(`整個dispatch花費的時間是:${Date.now() - startTimeStamp}毫秒`, )
           }
       }
   }
}
複製代碼

注意,註冊的時候,這個中間要始終放在第一位,箇中理由上文已經解釋過。

  1. 實現一個鑑權的中間件。
function auth(name) {
   return new Promise((reslove,reject)=> {
       setTimeout(() => {
           if(name === 'sam') {
               reslove(true);
           } else {
               reslove(false);
           }
       }, 1000);
   })
}
       
const authMiddleware = function(store){
   return function(next){
       return async function(action) {
           if (action.payload.isNeedToAuth) {
               const isAuthorized = await auth(action.payload.name);

               if(isAuthorized) {
                   next(action);
               } else {
                   alert('您沒有此權限');
               }
           } else {
               next(action);
           }
           
       }
   }
}
複製代碼

對於寫中間件事而言,只要把中間件的運行機制的原理明白,剩下的無非就是如何「消費」store,nextaction等實參的事情。到這裏,中間件的剖析就結束了。但願你們能夠結合實際的業務需求,發揮本身的聰明才智,早日寫出有如《滕王閣序》中所提的「紫電青霜,王將軍之武庫」般的中間件庫。

相關文章
相關標籤/搜索