原文發佈於個人 GitHub 博客,歡迎 star 😳react
最近翻出了以前分析的 applyMiddleware 發現本身又看不懂了😳,從新看了一遍源代碼,梳理了洋蔥模型的實現方法,在這裏分享一下。git
applyMiddleware 函數最短可是最 Redux 最精髓的地方,成功的讓 Redux 有了極大的可拓展空間,在 action 傳遞的過程當中帶來無數的「反作用」,雖然這每每也是麻煩所在。 這個 middleware 的洋蔥模型思想是從 koa 的中間件拿過來的,用圖來表示最直觀。github
上圖以前先上一段用來示例的代碼(via 中間件的洋蔥模型),咱們會圍繞這段代碼理解 applyMiddleware 的洋蔥模型機制:redux
function M1(store) { return function(next) { return function(action) { console.log('A middleware1 開始'); next(action) console.log('B middleware1 結束'); }; }; } function M2(store) { return function(next) { return function(action) { console.log('C middleware2 開始'); next(action) console.log('D middleware2 結束'); }; }; } function M3(store) { return function(next) { return function(action) { console.log('E middleware3 開始'); next(action) console.log('F middleware3 結束'); }; }; } function reducer(state, action) { if (action.type === 'MIDDLEWARE_TEST') { console.log('======= G ======='); } return {}; } var store = Redux.createStore( reducer, Redux.applyMiddleware( M1, M2, M3 ) ); store.dispatch({ type: 'MIDDLEWARE_TEST' }); 複製代碼
再放上 Redux 的洋蔥模型的示意圖(via 中間件的洋蔥模型),以上代碼中間件的洋蔥模型以下圖:數組
--------------------------------------
| middleware1 |
| ---------------------------- |
| | middleware2 | |
| | ------------------- | |
| | | middleware3 | | |
| | | | | |
next next next ——————————— | | |
dispatch —————————————> | reducer | — 收尾工做->|
nextState <————————————— | G | | | |
| A | C | E ——————————— F | D | B |
| | | | | |
| | ------------------- | |
| ---------------------------- |
--------------------------------------
順序 A -> C -> E -> G -> F -> D -> B
\---------------/ \----------/
↓ ↓
更新 state 完畢 收尾工做
複製代碼
咱們將每一個 middleware 真正帶來反作用的部分(在這裏反作用是好的,咱們須要的就是中間件的反作用),稱爲M?反作用,它的函數簽名是 (action) => {}
(記住這個名字)。promise
對這個示例代碼來講,Redux 中間件的洋蔥模型運行過程就是:bash
用戶派發 action → action 傳入 M1 反作用 → 打印 A → 執行 M1 的 next(這個 next 指向 M2 反作用)→ 打印 C → 執行 M2 的 next(這個 next 指向 M3 反作用)→ 打印 E → 執行 M3 的 next(這個 next 指向store.dispatch
)→ 執行完畢返回到 M3 反作用打印 F → 返回到 M2 打印 E → 返回到 M1 反作用打印 B -> dispatch 執行完畢。markdown
那麼問題來了,M1 M2 M3的 next 是如何綁定的呢?閉包
答:柯里化綁定,一箇中間件完整的函數簽名是 store => next => action {}
,可是最後執行的洋蔥模型只剩下了 action,外層的 store 和 next 通過了柯里化綁定了對應的函數,接下來看一下 next 是如何綁定的。app
const store = createStore(...args) let chain = [] const middlewareAPI = { getState: store.getState, dispatch: (...args) => dispatch(...args) } chain = middlewares.map(middleware => middleware(middlewareAPI)) // 綁定 {dispatch和getState} dispatch = compose(...chain)(store.dispatch) // 綁定 next 複製代碼
關鍵點就是兩句綁定,先來看第一句
chain = middlewares.map(middleware => middleware(middlewareAPI)) // 綁定 {dispatch和getState}
爲何要綁定 getState
?由於中間件須要隨時拿到當前的 state,爲何要拿到 dispatch
?由於中間件中可能會存在派發 action 的行爲(好比 redux-thunk),因此用這個 map 函數柯里化綁定了 getState
和 dispatch
。
此時 chain = [(next)=>(action)=>{…}, (next)=>(action)=>{…}, (next)=>(action)=>{…}]
,…
裏閉包引用着 dispatch
和 getState
。
接下來 dispatch = compose(...chain)(store.dispatch)
,先了解一下 compose
函數
compose(A, B, C)(arg) === A(B(C(arg)))
複製代碼
這就是 compose 的做用,從右至左依次將右邊的返回值做爲左邊的參數傳入,層層包裹起來,在 React 中嵌套 Decorator 就是這麼寫,好比:
compose(D1, D2, D3)(Button)
// 層層包裹後的組件就是
<D1>
<D2>
<D3>
<Button />
</D3>
</D2>
</D1>
複製代碼
再說回 Redux
dispatch = compose(...chain)(store.dispatch)
複製代碼
在實例代碼中至關於
dispatch = MC1(MC2(MC3(store.dispatch)))
複製代碼
MC就是 chain 中的元素,沒錯,這又是一次柯里化。
至此,真相大白,dispatch 作了一點微小的貢獻,一共幹了兩件事:1. 綁定了各個中間件的 next。2. 暴露出一個接口用來接收 action。其實說了這麼多,middleware 就是在自定義一個dispatch,這個 dispatch 會按照洋蔥模型來進行 pipe。
OK,到如今咱們已經拿到了想要的 dispatch,返回就能夠收工了,來看最終執行的靈魂一圖流:
然而可達鴨眉頭一皺,發現事情還沒這麼簡單,有幾個問題要想一下
const middlewareAPI = { getState: store.getState, dispatch: (...args) => dispatch(...args) } 複製代碼
在這裏 dispatch 使用匿名函數是爲了能在 middleware 中調用 compose 的最新的 dispatch(閉包),必須是匿名函數而不是直接寫成 store.dispatch。
若是直接寫成 store.dispatch
,那麼在某個 middleware(除最後一個,最後一個middleware拿到的是原始的 store.dispatch
)dispatch 一個 action,好比 redux-thunk
function createThunkMiddleware(extraArgument) { return ({ dispatch, getState }) => next => action => { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; } 複製代碼
就是攔截函數類型的 action,再可以對函數形式的 action(實際上是個 actionCreator)暴露 API 再執行一次,若是這個 actionCreator 是多層函數的嵌套,則必須每次執行 actionCreator 後的 actionCreator 均可以引用最新的 dispatch 才行。若是不寫成匿名函數,那這個 actionCreator 又走了沒有通過任何中間件修飾的 store.dispatch
,這顯然是不行的。因此要寫成匿名函數的閉包引用。
還有,這裏使用了 ...args
而不是 action
,是由於有個 PR,這個 PR 的做者認爲在 dispatch 時須要提供多個參數,像這樣 dispatch(action, option)
,這種狀況確實存在,可是隻有當這個需提供多參數的中間件是第一個被調用的中間件時(即在 middlewares 數組中排最後)才確定有效 ,由於沒法保證上一個調用這個多參數中間件的中間件是使用的 next(action) 或是 next(...args) 來調用,因此被改爲了 next(…args) ,在這個 PR 的討論中能夠看到 Dan 對這個改動持保留意見(但他仍是改了),這個改動其實真的挺蛋疼的,我做爲一個純良的第三方中間件,怎麼能知道你上箇中間件傳了什麼亂七八糟的屬性呢,再說傳了我也不知道是什麼意思啊大哥。感受這就是爲了某些 middleware 可以配合使用,不想往 action 里加東西,就加在參數中了,究竟是什麼參數只有這些有約定好參數的 middleware 才能知道了。
Note: logger must be the last middleware in chain, otherwise it will log thunk and promise, not actual actions (#20).
要求必須把本身放在 middleware 的最後一個,理由是
Otherwise it'll log thunks and promises but not actual actions.
試想,logger 想 log 什麼?就是 store.dispatch
時的信息,因此 logger 確定要在 store.dispatch
的先後 console,還記不記得上面哪一個中間件拿到了 store.dispatch,就是最後一個,若是把 logger
放在第一個的話你就能打出全部的 action
了,好比 redux-thunk
的 actionCreator,打印的數量確定比放在最後一個多,由於並非全部的 action 都能走到最後,也有新的 action 在 middleware 在中間被派發。