redux applyMiddleware 原理剖析

黃子毅發表於2019-03-04

用法

為了對中介軟體有一個整體的認識,先從用法開始分析。呼叫中介軟體的程式碼如下:

原始碼 createStore.js#39

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

enhancer 是中介軟體,且第二個引數為 Function 且沒有第三個引數時,可以轉移到第二個引數,那麼就有兩種方式設定中介軟體:

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

再看 原始碼 中介軟體的傳參:

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

就是為了得到 store,並通過 createStore 建立,上述兩種方法因為在 createStore 函式內部傳入了自身函式才得以實現 :

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 為什麼擁有神奇的作用?

store

程式碼前幾行建立了 store (如果第三個引數是中介軟體,就會出現中介軟體 store 包中介軟體 store 的情況,但效果是完全 打平 的), middlewareAPI 這個變數,其實就是精簡的 store, 因為它提供了 getState 獲取資料,dispatch 派發動作。

下一行,middlewares.map 將這個 store 作為引數執行了一遍中介軟體,所以中介軟體第一級引數 store 就是這麼來的。

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 函式中斷了傳遞。

總結

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

redux applyMiddleware 原理剖析
0cbb4b06-c769-11e6-97ad-7998ac6bab19.png


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

redux applyMiddleware 原理剖析

相關文章