理解 Redux 原始碼(四)- applyMiddleware
在聊 applyMiddleware 前想先稍微提一下這個方法的脈絡,我當初直接看這個方法時整個是一頭霧水,在好好爬了一遍官方文件後才覺得有比較理解,而我認為先了解 enhancer
以及 middleware
會比較好瞭解這整個方法,但如果已經很熟悉的話可以直接跳到第三部分看原始碼。
先看一下在實際使用時的寫法
import { createStore, applyMiddleware } from 'redux' |
看得出 applyMiddleware
帶入 middleware 並呼叫後會返回一個 enhancer 被帶入 createStore,被以 enhancer(createStore)(reducer, preloadedState)
的形式呼叫,那 enhancer 又是什麼?
一、enhancer
在官網上對於 enhancer 的型別描述是這樣的。
type StoreEnhancer = (next: StoreCreator) => StoreCreator |
enhancer 是一個高階函數,接受了一個 createStore
方法做為參數並返回與 createStore
同樣型別的方法,目的是將原本 createStore 內的方法增加或者修改一些功能,看一下官網的兩個 enhancer 的例子
const sayHiOnDispatch = (createStore) => { |
引入時會搭配 compose 將多個 enhancer 結合為一個。
const composedEnhancer = compose(sayHiOnDispatch, includeMeaningOfLife) |
sayHiOnDispatch
改寫了原本的 dispatch 方法,在呼叫 dispatch 時會 log 出hi
,includeMeaningOfLife
則是改寫原本的 getState 方法,使回傳結果多一個 meaningOfLife 的屬性。
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' }) |
因此看得出來 Redux 給了一個相當彈性的入口點,透過 enhancer 在原本的 feature 上另外增添其他的功能。
接著再來看看 middleware。
二、middleware
如果有寫過 server 端邏輯的話對於這個名詞應該不陌生,在真正處理請求之前會先通過一個或多個 middleware 對請求做一些邏輯處理,而 Redux 的 middleware
也是類似的概念,只不過這裡的請求變成了 dispatch
方法。
先看看 Redux 的定義。
這裡的 compose
就是指 functional programming 中常用到的 compose 技巧,至於如何實作待會會提到,可以先理解為,middleware
接受 dispatch 方法做為參數並返回一個新的 dispatch 方法,通常被用來處理非同步作業,或是實作 logger。
而 middleware 有一定的格式,如果想自己寫 custom middleware 的時候必須照著官方格式寫
// Middleware written as ES5 functions |
看起來有點嚇人,但是如果拆成兩個部分就比較好理解了。
exampleMiddleware
exampleMiddleware
在 applyMiddleware
呼叫時就會被呼叫並傳入 storeAPI(也就是 dispatch, getState 方法),目的是為了讓 custom middleware 內部可以取用到這兩個方法。
wrapDispatch
這個方法才是真正 middleware 的核心,方法呼叫後會返回一個handleAction
方法,而仔細看會發現handleAction
和 dispatch 方法一樣都接受一個 action 做為參數,原來其實handleAction
就是包裝後的 dispatch 方法!wrapDispatch
接受的參數 next
是下一個 middleware 的 handleAction
方法,而如果已經沒有下一個 middleware 時,next
就是 Redux 提供的 dispatch
方法。
為了方便理解可以先想像 applyMiddleware
只帶入一個 middleware 的場景,這時理所當然 next
就是原始的 dispatch 方法,而 wrapDispatch
也是在applyMiddleware
呼叫時就會被呼叫,這時就像 Redux 的定義說的,接受原本的 dispatch 方法做為參數,並且返回一個新的 dispatch 方法 handleAction
。
雖然這裡盡可能講得不那麼抽象一點,但可能還是有點難理解,接著就看看原始碼裡面 middleware 是如何被處理的吧!
三、applyMiddleware
有了基本對 enhancer 及 middleware 的了解後就可以來看看 applyMiddleware
import compose from './compose' |
可以看出 applyMiddleware
返回的方法是個非常典型的 enhancer 格式,如果先直接看最後返回的部分可以發現如同稍早說的,這個 enhancer 相當聚焦在改寫 dispatch 方法,接著來看看 enhancer 做了什麼。
首先呼叫了 createStore 方法取得 store 物件以方便最後返回時 spread 展開取得其他方法,接著定義 middlewareAPI 讓 middleware 使用,並先用 let 定義一個可修改的 dispatch 方法,如果 dispatch 在建構時就被呼叫會馬上噴錯,拿剛才的 middleware 格式舉例示意如下
function exampleMiddleware(storeAPI) { |
接著將所有傳入的 middleware 呼叫一次後放進 chain
陣列,拿剛才的例子來說此時陣列的所有 item 是每個 middleware 的 wrapDispatch
方法,然後將 chain
展開給 compose 當參數呼叫,而這裡我想先稍微說一下 compose 的作用。
compose 在 functional programming 中是一個常見的高階函式,作用是接收多個函式作為參數並且組合成一個新的函式,只要一呼叫新的函式便會由右而左呼叫所有被傳入的函示,並將返回的結果傳入下一個函式,例如。
const f = (x) => x + 5 |
此時呼叫 composedFunc
會將參數帶入並由右而左呼叫 h, g, f
composedFunc(4) // 等於 f(g(h(4))) -> 返回 11 |
大概理解以後來看一下 compose 的原始碼。
function compose(...funcs) { |
有剛才的例子之後應該會比較好理解,這邊邏輯確實是有點繞,如果不夠清楚可以多看幾次,了解以後就可以回歸到 applyMiddleware
程式碼上
dispatch = compose(...chain)(store.dispatch) |
覺得亂的話先拆兩段看,然後寫白話一點
const composedWrapDispatchFunc = compose(wrapDispatch1, wrapDispatch2, wrapDispatch3...) |
這樣就將所有 middleware 的 wrapDispatch
方法串在一起,並且產生一個新的 dispatch 方法了!
然後恭喜有看到這邊的人,applyMiddleware
原始碼到這邊就結束了!
如果還是覺得比較抽象的話,可以試著將最常用的 middleware 也就是 redux-thunk 的程式碼帶入看看,這邊我節錄一點 redux-thunk 程式碼並改成範例的格式。
function middleware({ dispatch, getState }) { |
套用了 redux-thunk 之後,action creator 通常會被寫成這樣的格式
function fetchPosts() { |
在每次執行 dispatch 時會先判斷 action 型別是 function 時則直接呼叫它,而不是被傳入 dispatch 當作參數,這就也就是在此時呼叫了 innerFunction
執行非同步作業,當裡面的 dispatch 被呼叫時開始下一次的 middleware 循環。
結語
這篇介紹了 Redux 是如何透過撰寫固定格式的 middleware、enhancer 來擴充 store 內部方法,也提到 applyMiddleware 如何透過 compose 將每個 middleware 串接在一起。
老實說當初看到 applyMiddleware
程式碼還高興了一下想說很短應該一下就看完了,結果由於 middleware
、enhancer
觀念不熟悉再回官網爬一遍之後才理解(慚愧),整個看下來反而比 combineReducers
花更久時間理解XD,於是寫這篇文章的方式就有先稍微提到一下這兩個觀念,希望能幫助到剛好有看到這篇文章的人。
至此 Redux 中比較常用的方法就介紹完了,其實再探究下去的話還有很多可以聊的東西,像是 react-redux 如何配合 redux 去讓 react 狀態更新後重新渲染,以及為什麼 Redux 一直強調 reducer 必須是 pure function , redux-devtools-extension 是如何藉這點達成 time-traveling 等等,再追下去真的追不完啊~這些主題有機會研究一下再來和大家分享。
另外因為小弟不常寫這樣的文章,如果有什麼不清楚或有誤之處,希望大家也不客氣鞭下去,感激不盡!
參考文獻