關於redux中間件是什麼以及爲何須要redux中間件的話題,網上有太多的文章已經介紹過了,本文就再也不贅述了。若是你有相似的困惑:react
那麼歡迎往下閱讀,但願這篇文章能幫助你多一些對redux中間件的理解。git
在深刻理解中間件以前,咱們先來看一個很關鍵的概念。es6
在數學中, 複合函數是指 逐點地把一個 函數做用於另外一個函數的結果,所獲得的第三個函數。直觀地說,複合兩個函數是把兩個函數連接在一塊兒的過程,內函數的輸出就是外函數的輸入。github
-- 維基百科編程
你們看到複合函數應該不陌生,由於上學時的數學課本上都出現過,咱們舉例回憶下:redux
f(x) = x^2 + 3x + 1 g(x) = 2x (f ∘ g)(x) = f(g(x)) = f(2x) = 4x^2 + 6x + 1
其實編程上的複合函數和數學上的概念很類似:數組
var greet = function(x) { return `Hello, ${ x }` }; var emote = function(x) { return `${x} :)` }; var compose = function(f, g) { return function(x) { return f(g(x)); } } var happyGreeting = compose(greet, emote); // happyGreeting(「Mark」) -> Hello, Mark :)
這段代碼應該不難理解,接下來咱們來看下compose方法的es6寫法,效果是等價的:閉包
const compose = (...funcs) => { return funcs.reduce((f, g) => (x) => f(g(x))); }
這個寫法可能須要你花點時間去理解。若是理解了,那麼恭喜你,由於redux的compose寫法基本就是這樣。可是若是一會兒沒法理解也不要緊,咱們只要先記住:app
咱們再舉個例子來理解下compose的做用:框架
// redux compose.js function compose (...funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (...args) => a(b(...args))) } function console1(nextConsole) { return (message) => { console.log('console1開始'); nextConsole(message); console.log('console1結束'); } } function console2(nextConsole) { return (message) => { console.log('console2開始'); nextConsole(message); console.log('console2結束'); } } function console3(nextConsole) { return (message) => { console.log('console3開始'); nextConsole(message); console.log('console3結束'); } } const log = compose(console1, console2, console3)(console.log); log('我是Log'); /* console1開始 console2開始 console3開始 我是Log console3結束 console2結束 console1結束 */
看到這樣的輸出結果是否是有點意外?咱們來進一步解析下:
由於:
compose(A, B, C)的返回值是:(arg) => A(B(C(arg)))
因此:
compose(console1, console2, console3)(console.log)的結果是:console1(console2(console3(console.log)))
由於:
內函數的輸出就是外函數的輸入
因此,根據console1(console2(console3(console.log)))從內到外的執行順序可得出:
console3的nextConsole參數是console.log
console2的nextConsole參數是console3(console.log)的返回值
console1的nextConsole參數是console2(console3(console.log))的返回值
也就是說在console1(console2(console3(console.log))執行後,因爲閉包的造成,因此每一個console函數內部的nextConsole保持着對下一個console函數返回值的引用。
因此執行log('我是Log')的運行過程是:
圖示:(和真實的執行棧會有差別,這裏做爲輔助理解)
(點擊查看大圖)
至此,整個運行過程就結束了。其實這就是網上不少文章裏提到的洋蔥模型,這裏我是以執行過程當中進棧出棧的方式來說解,不知道理解起來會不會更方便些~
關於複合函數就先介紹這些,篇幅有點長,主要是由於它在redux中間件裏起到了關鍵的做用。若是一下沒理解,能夠稍微再花點時間琢磨下,不着急往下讀,由於理解了複合函數,基本也就理解了redux中間件的大部分核心內容了。
接下來就是解讀源碼的時間了~
//redux applyMiddleware.js export default function applyMiddleware(...middlewares) { return (createStore) => (reducer, preloadedState, enhancer) => { const store = createStore(reducer, preloadedState, enhancer) let dispatch = store.dispatch let chain = [] const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } } }
首先來看下applyMiddleware的框架:applyMiddleware接受一箇中間件數組,返回一個參數爲createStore的函數,該函數再返回一個參數爲reducer、preloadedState、enhancer的函數。
export default function applyMiddleware(...middlewares) { return (createStore) => (reducer, preloadedState, enhancer) => {...} }
這裏有兩個問題?
先看第一個問題,是由於實際在configure store時,applyMiddleware是做爲redux createStore方法中第三個參數enhancer被調用:
// index.js const store = createStore(reducer, initialState, applyMiddleware(...middlewares)); // createStore.js export default function createStore(reducer, preloadedState, enhancer) { if (typeof enhancer !== 'undefined') { if (typeof enhancer !== 'function') { throw new Error('Expected the enhancer to be a function.') } return enhancer(createStore)(reducer, preloadedState) } ... }
咱們能夠在createStore的源碼中看到,當enhancer是function時,會先傳入自身createStore函數,返回的函數再傳入初始傳給createStore的reducer和preloadedState,因此第一個問題獲得瞭解答。而第二個問題是由於若是要給createStore傳多個enhancer的話,須要先用compose組合一下enhancer,而柯里化和compose的配合很是好用,因此這裏會採起柯里化的寫法。那爲何好用呢?之後會寫篇相關的文章來介紹,這裏先很少作介紹了~
咱們接着分析,那麼此時的enhancer是什麼?很明顯,就是applyMiddleware(...middlewares)的返回值
// applyMiddleware(...middlewares) (createStore) => (reducer, preloadedState, enhancer) => {...}
那 enhancer(createStore)(reducer, preloadedState) 連續調用的結果是什麼?這就來到了applyMiddleware的內部實現,總得來講就是接收外部傳入的createStore、reducer、preloadedState參數,用createStore生成一個新的store對象,對新store對象中的dispatch方法用中間件加強,返回該store對象。
// export default function applyMiddleware(...middlewares) // return (createStore) => (reducer, preloadedState, enhancer) => { const store = createStore(reducer, preloadedState, enhancer) let dispatch = store.dispatch let chain = [] const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch // 返回給全局store的是通過中間件加強的dispatch } // } // }
接着咱們分析下內部實現,首先用dispatch變量保存store.dispatch,而後將getState方法和dispatch方法傳遞給中間件,這裏又有兩個問題:
let dispatch = store.dispatch; let chain = []; const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch // 返回給全局store的是通過中間件加強的dispatch }
關於第一個問題,咱們先來看兩個常見的中間件內部實現(簡易版)
// redux-thunk function createThunkMiddleware ({ dispatch, getState }) { return (next) => (action) => { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; } // redux-logger function createLoggerMiddleware({ getState }) { return (next) => (action) => { const prevState = getState(); next(action); const nextState = getState(); console.log(`%c prev state`, `color: #9E9E9E`, prevState); console.log(`%c action`, `color: #03A9F4`, action); console.log(`%c next state`, `color: #4CAF50`, nextState); }; }
其實第一個問題的答案也就有了,由於中間件須要接收getState和dispatch在內部使用,logger須要getState方法來獲取當前的state並打印,thunk須要接收dispatch方法在內部進行再次派發,
關於第二個問題咱們一會再解答 :)
咱們繼續分析源碼,那麼此時map後的chain數組也就是每一箇中間件調用了一次後的結果:
chain = [(next)=>(action)=>{...}, (next)=>(action)=>{...}, (next)=>(action)=>{...}]; // 要注意此時每一箇中間件的內部實現{...}都閉包引用着傳入的getState和dispatch方法
看到這裏是否是以爲很熟悉了?
// console1,console2,console3(nextConsole) => (message) => {...}
const log = compose(console1, console2, console3)(console.log);
log('我是Log');
// log執行後輸出的洋蔥式結果不重複展現了
咱們一樣能夠推導出:
// middleware1, middleware2, middleware3 // (next) => (action) => {...} // dispatch = compose(...chain)(store.dispatch); 等於下一行 dispatch = compose(middleware1, middleware2, middleware3)(store.dispatch);
若是調用dispatch(action),也會像洋蔥模型那樣通過每個中間件,從而實現每一箇中間件的功能,而該dispatch也正是全局store的dispatch方法,因此咱們在項目中使用dispatch時,使用的也都是加強過的dispatch。
至此咱們也瞭解了applyMiddleware是如何將中間件做用於原始dispatch的。
別忘了,咱們還漏了一個問題沒解答:爲何傳入的dispatch要用匿名函數包裹下,而不是直接傳入store.dispatch?
咱們再來看下內部實現:
let dispatch = store.dispatch // 1 const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) // 2 } chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) // 3
首先,代碼中三處的dispatch都是同一個,那麼經由匿名函數包裹的dispatch,經過middlewareAPI傳入middleware後,middleware內部的dispatch就能夠始終保持着對外部dispatch的引用(由於造成了閉包)。也就是說,當註釋3的代碼執行後,middleware內部的dispatch也就變成了加強型dispatch。那麼這樣處理有什麼好處呢?咱們來看個場景
// redux-thunk function createThunkMiddleware ({ dispatch, getState }) { return (next) => (action) => { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; } // 使用到thunk的異步action場景 const setDataAsync = () => { return (dispatch) => { setTimeout(() => { dispatch({ type: 'xxx', payload: 'xxx' }); }, 3000) } } const getData = () => { return (dispatch) => { return fetch.get(...).then(() => { dispatch(setDataAsync()); }) } } dispatch(getData());
若是是一個異步action嵌套另外一個異步action的場景,而此時傳入的dispatch若是是原始store.dispatch,dispatch(setDataAsync())的執行就會有問題,由於原始的store.dispatch沒法處理傳入函數的狀況,那麼這個場景就須要中間件加強後的dispatch來處理。
因此這也就解釋了爲何傳入的dispatch要用匿名函數包裹,由於可能在某些中間件內部須要使用到加強後的dispatch,用於處理更多複雜的場景。
好,關於redux中間件的內容就先介紹到這裏。很是感謝能看到此處的讀者,在如今碎片化閱讀盛行的時代,能耐心看完如此篇幅的文章實屬不易~
最後,打個小廣告,歡迎star一波我司自研的react移動端組件——Zarm
相關介紹文章:
對不起,咱們來晚了 —— 基於 React 的組件庫 Zarm 2.0 發佈