redux 源碼研究:中間件
設計模式與 redux 中間件
中間件是代理/裝飾模式的一種的實踐方式,通過改造 store.dispatch 方法,可以攔截 action(代理)或添加額外功能(裝飾)。
突然發現 Javascript 里的代理/裝飾模式的寫法蠻通用的....
對於創建的 store 對象,如果希望代理/裝飾 dispatch 函數,基本的格式如下:
- 新建一個變數指向 store.dispatch。
- 新建同名函數 dispach,接收參數為 action。
- 編寫自己的額外邏輯。
- 在 dispach 內部執行 oldDispatch,並返回( store.dispatch 是有返回值的)。
- 令 store.dispatch 指向新的 dispatch ,返回新的 store。
簡單的說:函數地址交換,在新函數中執行老函數。
const applyMyMiddlware = (store) => {n // 1. 新建一個變數指向 store.dispatchn const oldDispatch = store.dispatch;nn // 2. 新建 dispach,接收參數為 actionn const dispatch = (action) => {nn // 3. 編寫額外邏輯n /* n ........n */nn // 3.1 所謂的代理就是攔截參數 action,根據 action 來進行自己的操作n // 3.2 所謂的裝飾就是不攔截 action,但是在這之前進行自己的邏輯處理n // 3.3 注意對象中 this(如果有) 的指向問題nn // 4. 在 dispach 內部執行 oldDispatch,並返回。n return oldDispatch(action);n // 4.1 store.dispatch 是有返回值的,返回值類型是 actionn };n //5 令 store.dispatch 指向新的 dispatch ,返回新的 storen store.dispatch = dispatch;nn return storen // 或者也可以這樣寫n // returen {n // ...store,n // dispatchn // } n}n
執行 store = applyMyMiddlware(store)
後, 調用 store.dispatch(action) 的結果便為代理/裝飾後的結果。
applyMiddleware 源碼研究
redux 提供了官方載入中間件的函數 applyMiddleware,同時規定了中間件的寫法必須是:
({dispatch, getState}) => next => action => {n // .... 中間件自己的邏輯nn return next(action);n}n
直到看源碼之前,我只是單純的記住了這麼一長串的多重調用,並不理解為什麼。
而這種多重返回的原因,就在 applyMiddleware 的源碼里
import compose from ./compose export default function applyMiddleware(...middlewares) {n return (createStore) => (...args) => {n const store = createStore(...args)n let dispatch = () => {n throw new Error(n `Dispatching while constructing your middleware is not allowed. ` +n `Other middleware would not be applied to this dispatch.`n )n }n let chain = []nn const middlewareAPI = {n getState: store.getState,n dispatch: (...args) => dispatch(...args)n }n chain = middlewares.map(middleware => middleware(middlewareAPI))n dispatch = compose(...chain)(store.dispatch)nn return {n ...store,n dispatchn }n }n}n
1. ({dispatch, getState})
分析一下 applyMiddleware 源碼很容易找到 {dispatch, getState} 的來源:
- 首先用傳入的 createStore 方法創建了 store 對象。此時 store 中有 store.dispatch 以及 store.getState 方法(subscribe 暫時不考慮)。
- 初始化了一個 dispatch,但是中間塞了一個斷言。如果直接調用,就會報錯。
- 定義了一個 chain 數組和
const middlewareAPI = {n getState: store.getState,n dispatch: (...args) => dispatch(...args)n }n
getState 可以獲得當前 state 狀態,dispatch 則是經過處理添加了新功能
- 通過 map 函數,把每個中間件執行了一遍,傳入的參數就是 middlewareAPI。
chain = middlewares.map(middleware => middleware(middlewareAPI))n
因此,對於中間件:
const middleware = ({dispatch, getState}) => next => action => {n // .... 中間件自己的邏輯nn return next(action);n}n
第一個參數 {dispatch, getState} 顯然是 (middlewareAPI),返回值為
next => action => {n // .... 中間件自己的邏輯nn return next(action);n}n
這麼調用的好處是,在返回值(也是個函數)內部依然可以調用到 store.getState 方法閉包。
2. next 是什麼(上):喪心病狂的 compose
經過 map 遍歷,chain 數組此刻的值為:
[n next => action => {n // .... 中間件自己的邏輯nn return next(action);n },n next => action => {n // .... 中間件自己的邏輯nn return next(action);n } n // ...其他中間件n]n
這麼一種形式。
dispatch = compose(...chain)(store.dispatch)
,是整段代碼中最不(喪)好(心)理(病)解(狂)的部分。
貼一下 compose 函數源碼:
export default function compose(...funcs) {n if (funcs.length === 0) {n return arg => argn }nn if (funcs.length === 1) {n return funcs[0]n }nn return funcs.reduce((a, b) => (...args) => a(b(...args)))n}n
根據其源碼,compose 應對了 3 種數組的情況。
對於 dispatch = compose(...chain)(store.dispatch)
:
- 如果 chian 的長度是 0(也就是未傳入中間件),等價於 dispatch = (args=> args)(store.dispatch) ,即 dispatch = store.dispatch。
如果 chian 的長度為 1,也就是說為 n[n next => action => {n console.log(0號中間件)n return next(action)n }n];n
如果用一個變數 temp0 指向上述函數的地址,compose(...chain)(store.dispatch)
便等同於 temp0(next = store.disptach)
。此時 next 為 store.dispatch,next(action) 便等同於 store.dispatch(action)。
因此 temp0(next = store.disptach)
的返回值應為:
dispatch = temp0(next = store.disptach) = action => {n console.log(0號中間件);n return store.dispatch(action);n }n
結論:
在無中間件的情況下, dispatch 為 store.dispatch;在只有一個中間件的情況下,next 的值是 store.dispatch
。把 console.log(0號中間件);
換成其他的邏輯,中間件就可以在保證原本 store.dispatch
功能的情況下,實現自己的額外功能。
3. next 是什麼(下):庶民推理
接下來的推理相當繞,我也是捋了半天才捋明白。
懶人請直接看結論。
結論1:當多於 2 個的元素的時候,傳入的 action 按照初始化時中間件數組的順序依次經過每個中間件,最後依靠執行 store 原本的 dispatch (當然前提是此 action 沒有被中途攔截)離開,完成整個流程。
註:單向數據流
結論2:對於每一個中間件
({dispatch, getState}) => next => action => {..... return next(action)}
,如果不做攔截 action 最早傳入的 action;如果進行攔截,後面的 action 為攔截後生成的 action。最後一位中間件的 next 為 store.dispatch,其餘中間件的 next 都是下一位中間件action => {..... return next(action)}
的部分。
以下是無聊的推理過程:
- 假設 chain 的長度 為 2 。
即:
// 先假設只有 2 個元素n chain = [n next => action => {n console.log(0 號中間件);n return next(action);n }, n next => action => {n console.log(1 號中間件)n return next(action);n }]n
腦補 compose 的執行過程。
第 1 步. return funcs.reduce((a, b) => (...args) => a(b(...args)))
因為沒有初始值,所以 a b 為最開始的兩個元素。即
return (...args) => a(b(...args)));
即 compose(...chian) = (...args) => a(b(...args)));
。
數組的 reduce 方法很有意思,接收一個回調函數(和一個初始值)做參數。該回調函數的第一個參數便是該回調函數上一次執行的結果。如果有初始值用初始值,如果沒有初始值則直接從第二個元素開始循環,初始值為第一個元素。常用於解決遞歸的邏輯,和 map 相比最大的好處是不用引入外界變數。
第 2 步. 根據 JavaScript 的語法,先執行
b(...args)
b 為
next=> action => {n console.log(1 號中間件)n return next(action);n }n
所以 b(...args) 的執行結果為
action => {n console.log(1 號中間件)n return (...args)(action);n }n
第 3 步. 執行 a(b(...args))
a 為:
next => action => {n console.log(0 號中間件);n return next(action);n }n
a(b(...args)) 就等同於
action => {n console.log(0 號中間件);n return (n // 用 b(...args) 的返回值代替 nextn action => {n console.log(1 號中間件)n return (...args)(action);n }n )(action)n}n
即 compose(...chian)
為
(...args) => action => {n console.log(0 號中間件);n return (n // 用 b(...args) 的值代替 nextn action => {n console.log(1 號中間件)n return (...args)(action);n }n )(action)n
第 4 步:dispatch
dispatch 等價於 compose(...chian)(store.dispatch) 等價於 n // 因為 compose(...chian)(store.dispatch) 的參數 ...args 等於 store.dispatchn // 去掉 (...args)=> 並應用 store.dispatch 替換 (...args)(action) 為 (store.dispatch)(action)n dispatch = action => {n console.log(0 號中間件);n return (action => {n console.log(1 號中間件)n // 使用 store.dispatch 代替 ...argsn return (store.dispatch)(action);n })(action);n換個寫法:ndispatch = action => {n console.log(0 號中間件);nn const next = action => {n console.log(1 號中間件);n return store.dispatch(action);n }nn reutrn next(action);n}n
3.1. 遞歸與中間件調用
現在考慮 chain 的數組多於 2 個元素的情況,例如 chain = [a, b, c]
。
由 3 得知 , b c 的執行結果是
dispatchBC = action => {n console.log(b);nn const next = action => {n console.log(c);n return store.dispatch(action);n }nn reutrn next(action);n}n
因此 compose(...[a, b, c])
的執行結果等同與
dispatch = action => {n console.log(a);nn const next = dispatchBC;nn return next(action);nn}n
讚美遞歸!
後記:為什麼要採用這種複雜的調用?
沒看懂 applyMiddleware 源碼之前總是覺得作者故意找茬,為什麼不能用訂閱/發布的模式去寫?
比如像這個樣子:
const applyObserver = (...middlewares) {n // .... 創建 store 的邏輯n const oldDispatch = store.dispatch;n const dispatch = (action) => {n for(middleware in middlewares) {n middleware.call(null, {dispatch: store.dispatch, getState: store.getState})n }n return oldDispatch(action)n }n return {n ...store,n dispatchn }n}n
看懂了以後發自內心的讚歎:卧槽太牛逼了。
使用 applyObserver 只能實現裝飾模式,無法實現對 action 的攔截與轉換。如果每一個中間件都能消費或者產生新的 action,那麼一個 action 傳入後會產生多個 action,而這與 redux 單項數據流 的理念相悖。applyMiddleware 的寫法最大程度的保證了 action 的流向,每一步的數據變化都是可以追蹤的。
這邊是 redux 中間件使用多重返回函數的真正原因。
這也是 compose 為什麼這麼牛逼的原因。
後記2:函數與 JavaScript
我剛開始用 js 的時候有位高人對我說:JavaScript 其實並不是正統的 OOP 函數。
確實,直到 ES6 裡面才有了 extends 關鍵字進行繼承,ES6 之前只有 proTotype。而所謂的 class 也不過是轉成函數,進行調用。雖然 JavaScript 經過 es6 的革新和 es7 的強化後寫法不再那麼反人類,但是離純 OOP 的語言比如 Java 還有不小的差距。
研究過 dva 和 redux 的部分源碼之後,我發現 JavaScript 框架的作者在解決通用性問題的方式,都是通過提供了組合的函數而不是一個組合過的類( dva 處理非同步調用的時候是返回了一個 takeEvery 的函數)。
沒有什麼不是一個函數可以解決的問題,如果有就再來一個。
這個就和目前的 OOP 思想差別相當大了,瞄準的是功能而不是對象。
對於 Java,雖然可以使用反射實現動態調用,但是類必須真實存在的;
對於 JavaScript,有沒有類無所謂,沒有就自己造一個。只要產生的對象能嘎嘎叫並像鴨子一樣走路,那就是鴨子(著名的鴨式辨型)。現在前端推廣 stateless 組件和高階組件,寫來寫去也是函數。推薦閱讀:
※請教 redux 與 eventEmitter?
※redux 中的 state 樹太大會不會有性能問題?
※React+AntD後台管理系統解決方案(補)
※構建離線優先的 React 應用
TAG:Redux |