redux applyMiddleware 原理剖析

用法

爲了對中間件有一個總體的認識,先從用法開始分析。調用中間件的代碼以下:前端

源碼 createStore.js#39react

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

enhancer 是中間件,且第二個參數爲 Function 且沒有第三個參數時,能夠轉移到第二個參數,那麼就有兩種方式設置中間件:git

const store = createStore(reducer, null, applyMiddleware(...))複製代碼
const store = createStore(reducer, applyMiddleware(...))複製代碼

再看 源碼 中間件的傳參:github

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => { 
    var store = createStore(reducer, preloadedState, enhancer)
    ... 
}複製代碼

就是爲了獲得 store,並經過 createStore 建立,上述兩種方法由於在 createStore 函數內部傳入了自身函數才得以實現 :redux

export default function createStore(reducer, preloadedState, enhancer) {
    ...
    if (typeof enhancer !== 'undefined') {
      return enhancer(createStore)(reducer, preloadedState)
    }
    ...
}複製代碼

上述代碼能夠看出,建立 store 的過程徹底交給中間件了,所以開啓了中間件第三種使用方式:數組

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

applyMiddleware 源碼解析

你們對剖析 applyMiddleware 源碼都很是感興趣,由於它實現精簡,但含義甚廣,再重溫其源碼微信

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    var store = createStore(reducer, preloadedState, enhancer)
    var dispatch = store.dispatch
    var chain = []

    var middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}複製代碼

假設你們都已瞭解 ES6 7 語法,懂得 compose 函數的含義,而且看過一些源碼剖析了,咱們才能把重點放在覈心原理上:爲何中間件函數有三個傳參 store => next => action ,第二個參數 next 爲何擁有神奇的做用?app

store

代碼前幾行建立了 store (若是第三個參數是中間件,就會出現中間件 store 包中間件 store 的狀況,但效果是徹底 打平 的), middlewareAPI 這個變量,其實就是精簡的 store, 由於它提供了 getState 獲取數據,dispatch 派發動做。frontend

下一行,middlewares.map 將這個 store 做爲參數執行了一遍中間件,因此中間件第一級參數 store 就是這麼來的。koa

next

下一步咱們獲得了 chain, 倒推來看,其中每一箇中間件只有 next => action 兩級參數了。咱們假設只有一箇中間件 fn ,所以 compose 的效果是:

dispatch = fn(store.dispatch)複製代碼

那麼 next 參數也知道了,就是 store.dispatch 這個原始的 dispatch.

action

代碼的最後,返回了 dispatch ,咱們通常會這麼用:

store.dispatch(action)複製代碼

等價於

fn(store.dispatch)(action)複製代碼

第三個參數也來了,它就是用戶本身傳的 action.

單一中間件的場景

咱們展開代碼來查看一箇中間件的運行狀況:

fn(middlewareAPI)(store.dispatch)(action)複製代碼

對應 fn 的代碼多是:

export default store => next => action => {
    console.log('beforeState', store.getState())
    next(action)
    console.log('nextState', store.getState())
}複製代碼

當咱們執行了 next(action) 後,至關於調用了原始 store dispatch 方法,並將 action 傳入其中,可想而知,下一行輸出的 state 已是更新後的了。

可是 next 僅僅是 store.dispatch, 爲何叫作 next 咱們如今還看不出來。

詳見 dispatch 後馬上修改 state:

function dispatch(action) {
    ...
    currentState = currentReducer(currentState, action)
    ...
}複製代碼

其中還有一段更新監聽數組對象,以達到 dispatch 過程不受干擾(快照效果) 做爲課後做業你們獨立研究:主要思考這段代碼的意圖:github.com/reactjs/red…

多中間件的場景

咱們假設有三個中間件 fn1 fn2 fn3, 從源碼的這兩句入手:

chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)複製代碼

第一行代碼,咱們獲得了只剩 next => action 參數的 chain, 暫且叫作:

cfn1 cfn2 cfn3, 而且有以下對應關係

cfnx = fnx(middlewareAPI)複製代碼

第二行代碼展開後是這樣的:

dispatch = cfn1(cfn2(cfn3(store.dispatch)))複製代碼

能夠看到最後傳入的中間件 fn3 最早執行。

爲了便於後面理解,我先把上面代碼的含義寫出來:經過傳入原始的 store.dispatch, 但願經過層層中間件的調用,最後產生一個新的 dispatch. 那麼實際上中間件所組成的 dispatch, 從函數角度看,就是被執行過一次的 cfn1 cfn2 cfn3 函數

咱們就算不理解新 dispatch 的含義,也能夠從代碼角度理解:只要執行了新的 dispatch , 中間件函數 cfnx 系列就要被執行一次,因此 cfnx 的函數自己就是中間件的 dispatch

對應 cfn3 的代碼多是:

export default next => action => {
    next(action)
}複製代碼

這就是這個中間件的 dispatch.

那麼執行了 cfn3 後,也就是 dispatch 了以後,其內部可能沒有返回值,咱們叫作 ncfn3,大概以下:

export default action => {}複製代碼

其函數自身就是返回值 返回給了 cfn2 做爲第一個參數,替代了 cnf3 參數 store.dispatch 的位置。

咱們再想一想,store.dispatch 的返回值是什麼?不就是 action => {} 這樣的函數嗎?這樣,一箇中間件的 dispatch 傳遞完成了。咱們理解了多中間件 compose 後能夠爲何能夠組成一個新的 dispatch 了(其實單一中間件也同樣,但由於步驟只有一步,讓人會想到直接觸發 store.dispatch 上,多中間件提煉了這個行爲,上升到組合爲新的 dispatch)。

再解釋 next 的含義

爲何咱們在中間件中執行 next(action) ,下一步就能拿到修改過的 store ?

對於 cfn3 來講, next 就是 store.dispatch 。咱們先不考慮它爲何是 next , 但執行它了就會直接執行 store.dispatch ,後面立馬拿到修改後的數據不奇怪吧。

對於 cfn2 來講,next 就是 cfn3 執行後的返回值(執行後也仍是個函數,內層並無執行),咱們分爲兩種狀況:

  1. cfn3 沒有執行 next(action),那 cfn1 cfn2 都無法執行 store.dispatch,由於原始的 dispatch 沒有傳遞下去,你會發現 dispatch 函數被中間件搞失效了(因此中間件還能夠搗亂)。爲了防止中間件瞎搗亂,在中間件正常的狀況請執行 next(action).

這就是 redux-thunk 的核心思想,若是 action 是個 function ,就故意執行 action , 而不執行 next(action) , 等於讓 store.dispatch 失效了!但其目的是明確的,由於會把 dispatch 返回給用戶,讓用戶本身調用,正常使用是不會把流程停下來的。

  1. cfn3 執行了 next(action), 那 cfn2 何時執行 next(action)cfn3 就何時執行 next(action) => store.dispatch(action) , 因此這一步的 next 效果與 cfn3 相同,繼續傳遞下去也同理。我看了下 redux-logger 的文檔,果真央求用戶把本身放在最後一個,其緣由是懼怕最右邊的中間件『搗亂』,不執行 next(action) , 那 logger 再執行 next(action) 也沒法真正觸發 dispatch .

我在考慮這樣會不會有很大的侷限性,但後來發現,只要中間件常規狀況執行了 next(action) 就能保證原始的 dispatch 能夠被繼續分發下去。只要每一箇中間件都按照這個套路來, next(action) 的效果就與 yield 相似。

因此 next 並非徹底意義上的洋蔥模型,只能說符合規範(默認都執行了 next(action))的中間件才符合洋蔥模型。

koa 的洋蔥模型但是有技術保證的,generator 可不會受到代碼的影響,而 redux 中間件的洋蔥模型,會由於某一層不執行 next(action) 而中斷,並且從右開始直接切斷。

爲何在中間件直接 store.dispatch(action) ,傳遞就會中斷?

理解了上面說的話,就很簡單了,並非 store.dispatch(action) 中斷了原始 dispatch 的傳遞,而是你執行完之後不調用 next 函數中斷了傳遞。

總結

仍是要畫個圖總結一下,在不想看文字的時候:

0cbb4b06-c769-11e6-97ad-7998ac6bab19.png


本文對你有幫助?歡迎掃碼加入前端學習小組微信羣:

相關文章
相關標籤/搜索