redux的中間件對於使用過redux的各位都不會感到陌生,通過應用上我們需要的所有要應用在redux流程上的中間件,我們可以加強dispatch的功能。最近抽了點時間把之前整理分析過的中間件有關的東西放在這里分享分享。本文只對中間件涉及到的createStore、applyMiddleware以及典型常用中間的的源碼做解析,讓大家了解redux的內部模塊:createStore.js、applyMiddleware.js,以及redux的中間件之間是怎么串聯在一起並協作工作的。文章內容特別是源碼部分對函數式編程思想有一定要求,比如:柯里化、compose等,源碼中會大量涉及到這些概念,如果讀者對此是不熟悉,可先學習這方面相關資料。
一、thunk作為一個典型redux中間件,它做了什么事?
簡單的thunk使用方式如下:
// action const getUserInfo = (id) => { return function (dispatch, getState, extraArgument){ return reqGet({id: id}) .then(res => res.json().data) .then(info => { dispatch({ type: "GET_USER_INFO", info }) }) .catch(err => console.log('reqGet error: ' + err)); } }; // dispatch action dispatch(getUserInfo(1));
在上述使用實例中,我們應用thunk中間到redux后,可以dispatch一個方法,在方法內部我們想要真正dispatch一個action對象的時候再執行dispatch即可,特別是異步操作時非常方便。當然支持異步操作的redux中間件也並非只有thunik,還有更專業的其他中間件,這非本文內容,這里不再多講。
二、thunk中間件內部是什么樣的?
thunk源碼如下(為了方便閱讀,源碼中的箭頭函數在這里換成了普通函數):
function createThunkMiddleware (extraArgument){ return function ({dispatch, getState}){ return function (next){ return function (action){ if (typeof action === 'function'){ return action(dispatch, getState, extraArgument); } return next(action); }; } } } let thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware; export default thunk;
thunk是一個很常用的redux中間件,應用它之后,我們可以dispatch一個方法,而不僅限於一個純的action對象。它的源碼也很簡單,如上所示,除去語法固定格式也就區區幾行。
下面我們就來看看源碼(為了方便閱讀,源碼中的箭頭函數在這里換成了普通函數),首先是這三層柯里化:
// 外層 function createThunkMiddleware (extraArgument){ // 第一層 return function ({dispatch, getState}){ // 第二層 return function (next){ // 第三層 return function (action){ if (typeof action === 'function'){ return action(dispatch, getState, extraArgument); } return next(action); }; } } }
首先是外層,從thunk最后兩行源碼可知,這一層存在的主要目的是支持在調用applyMiddleware並傳入thunk的時候時候可以不直接傳入thunk本身,而是先調用包裹了thunk的函數(第一層柯里化的父函數)並傳入需要的額外參數,再將該函數調用的后返回的值(也就是真正的thunk)傳給applyMiddleware,從而實現對額外參數傳入的支持,使用方式如下:
const store = createStore(reducer, applyMiddleware(thunk.withExtraArgument({api, whatever})));
如果無需額外參數則用法如下:
const store = createStore(reducer, applyMiddleware(thunk));
接下來來看第一層,這一層是真正applyMiddleware能夠調用的一層,從形參來看,這個函數接收了一個類似於store的對象,因為這個對象被結構以后獲取了它的dispatch和getState這兩個方法,巧的是store也有這兩方法,但這個對象到底是不是store,還是只借用了store的這兩方法合成的一個新對象?這個問題在我們后面分析applyMiddleware源碼時,自會有分曉。
再來看第二層,在第二層這個函數中,我們接收的一個名為next的參數,並在第三層函數內的最后一行代碼中用它去調用了一個action對象,感覺有點 dispatch({type: 'XX_ACTION', data: {}}) 的意思,因為我們可以懷疑它就是一個dispatch方法,或者說是其他中間件處理過的dispatch方法,似乎能通過這行代碼鏈接上所有的中間件,並在所有只能中間件自身邏輯處理完成后,最終調用真實的store.dispath去dispatch一個action對象,再走到下一步,也就是reducer內。
最后我們看看第三層,在這一層函數的內部源碼中首先判斷了action的類型,如果action是一個方法,我們就調用它,並傳入dispatch、getState、extraArgument三個參數,因為在這個方法內部,我們可能需要調用到這些參數,至少dispatch是必須的。這三行源碼才是真正的thunk核心所在,簡直是太簡單了。所有中間件的自身功能邏輯也是在這里實現的。如果action不是一個函數,就走之前解析第二層時提到的步驟。
三層的初步解析就到這里,通過這個分析,其實也沒有得出很重要的結論,對於想要了解applyMiddleware到底干了啥,我們還是很懵逼的。但至少我們可以初步判斷出第一層到第三層均為applyMiddleware對一個redux中間件的基本寫法要求,也就是說無論一個中間件要實現一個怎樣的功能,其固定格式必須是這個,在第三層函數內部才是自己功能邏輯實現的地方。
記住這三層做的事情很重要(雖然憑借着這極少的信息,我們依然很懵逼),但在下一個段落中,我們將再次提到它們,並詳細說明為什么會有這三層柯里化的存在。
三、applyMiddleware內部是怎樣的?createStore又干了什么?
直接上applyMiddleware源碼,為方便閱讀和理解,部分ES6箭頭函數已修改為ES5的普通函數形式,如下:
function applyMiddleware (...middlewares){ return function (createStore){ return function (reducer, preloadedState, enhancer){ const store = createStore(reducer, preloadedState, enhancer); let dispatch = function (){ throw new Error('Dispatching while constructing your middleware is not allowed. Other middleware would not be applied to this dispatch.') }; const middlewareAPI = { getState: store.getState, dispatch: (...args) => dispatch(...args) }; const chain = middlewares.map(middleware => middleware(middlewareAPI)); dispatch = compose(...chain)(store.dispatch); return { ...store, dispatch }; } } }
從其源碼可以看出,applyMiddleware內部一開始也是兩層柯里化,我們從thunk過來本來是為了尋找答案的,這讓我們一過來就又處於懵逼之中,為啥這么多柯里化?哈哈,解鈴還須系鈴人,讓我們先來看看和applyMiddleware最有關系的createStore的主要源碼:
export default function createStore(reducer, preloadedState, enhancer) { if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { enhancer = preloadedState preloadedState = undefined } if (typeof enhancer !== 'undefined') { if (typeof enhancer !== 'function') { throw new Error('Expected the enhancer to be a function.') } return enhancer(createStore)(reducer, preloadedState) } if (typeof reducer !== 'function') { throw new Error('Expected the reducer to be a function.') } var currentReducer = reducer; var currentState = preloadedState; var currentListeners = []; var nextListeners = currentListeners; var isDispatching = false; function ensureCanMutateNextListeners (){ // ... } function dispatch (){ // ... } function subscribe (){ // ... } function getState (){ // ... } function replaceReducer (){ // ... } function observable (){ // ... } dispatch({ type: ActionTypes.INIT }) return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable } }
對於createStore的源碼我們只需要關注和applyMiddleware有關的地方,其他和store有關的不是本文的重點。從其內部前面一部分代碼來看,其實很簡單,就是對調用createStore時傳入的參數進行一個判斷,並對參數做矯正,再決定以哪種方式來執行后續代碼。據此可以得出createStore有多種使用方法,根據第一段參數判斷規則,我們可以得出createStore的兩種使用方式,它們和第一章節中的使用方式相同:
const store = createStore(reducer, {a: 1, b: 2}, applyMiddleware(...));
以及:
const store = createStore(reducer, applyMiddleware(...));
同時根據第一段參數判斷規則,我們還可以肯定的是:applyMiddleware返回的一定是一個函數,在上述章節中我們曾猜想過,經過了各個中間件處理以后,原始的store.dispatch會被改造,但最終還是會返回一個經過改造后的dispatch,這里可以確定至少一半是正確了的。
經過createStore中的第一個參數判斷規則后,對參數進行了校正,得到了新的enhancer得值,如果新的enhancer的值不為undeifined,便將createStore傳入enhancer(即applyMiddleware調用后返回的函數)內,讓enhancer執行創建store的過程。也就時說這里的:
enhancer(createStore)(reducer, preloadedState);
實際上等同於:
applyMiddleware(mdw1, mdw2, mdw3)(createStore)(reducer, preloadedState);
這也解釋了為啥applyMiddleware會有兩層柯里化,同時表明它還有一種很函數式編程的用法,即 :
const store = applyMiddleware(mdw1, mdw2, mdw3)(createStore);
這種方式將創建store的步驟完全放在了applyMiddleware內部,並在其內第二層柯里化的函數內執行創建store的過程即調用createStore,調用后程序將跳轉至createStore走參數判斷流程最后再創建store。
無論哪一種執行createStore的方式,我們都終將得到store,也就是在creaeStore內部最后返回的那個包含dispatch、subscribe、getState等方法的對象。
四、回過頭對applyMiddleware做深入分析
applyMiddleware源碼和中間件thunk的源碼在第三章節和第一章節中有提到,這里就不再貼出來了,回看前面章節中的源碼即可。對於applyMiddleware開頭的兩層柯里化的出現原因以及和createStore有關的方面,在上述章節章節中已有分析。這里主要針對本文的重點,也就是中間件是如何通過applyMiddleware的工作起來並實現挨個串聯的原因做分析。
在第二章節中,我們提到過懷疑在thunk的第一層柯里化中傳入的對象是一個類似於store的對象,通過上個章節中applyMiddleware的確實可以確認了,確實如我們所想一樣。
接下來這幾段代碼是整個applyMiddleware的核心部分,也解釋了在第二章節中,我們對thunk中間件為啥有三層柯里化的疑慮,把這些代碼單獨貼出來,如下:
// ... const chain = middlewares.map(middleware => middleware(middlewareAPI)); dispatch = compose(...chain)(store.dispatch); return { ...store, dispatch }; // ...
首先,applyMiddleware的執行結果最終是返回store的所有方法和一個dispatch方法。這個dispatch方法是怎么來的呢?我們來看頭兩行代碼,這兩行代碼也是所有中間件被串聯起來的核心部分實現,它們也決定了中間件內部為啥會有我們在之前章節中提到的三層柯里化的固定格式,先看第一行代碼:
const chain = middlewares.map(middleware => middleware(middlewareAPI));
遍歷所有的中間件,並調用它們,傳入那個類似於store的對象middlewareAPI,這會導致中間件中第一層柯里化函數被調用,並返回一個接收next(即dispatch)方法作為參數的新函數。為什么會有這一層柯里化呢,主要原因還是考慮到中間件內部會有調用store方法的需求,所以我們需要在此注入相關的方法,其內存函數可以通過閉包的方式來獲取並調用,若有需要的話。
遍歷結束以后,我們拿到了一個包含所有中間件新返回的函數的一個數組,將其賦值給變量chain,譯為函數鏈。
再來看第二句代碼:
dispatch = compose(...chain)(store.dispatch);
我們展開了這個數組,並將其內部的元素(函數)傳給了compose函數,compose函數又返回了我們一個新函數。然后我們再調用這個新函數並傳入了原始的未經任何修改的dispatch方法,
最后返回一個經過了修改的新的dispatch方法。
有幾點疑惑:
1. 什么是compose?在函數式編程中,compose指接收多個函數作為參數,並返回一個新的函數的方式。調用新函數后傳入一個初始的值作為參數,該參數經最后一個函數調用,將結果返回並作為倒數第二個函數的入參,倒數第二個函數調用完后,將其結果返回並作為倒數第三個函數的入參,依次調用,知道最后調用完傳入compose的所有的函數后,返回一個最后的結果。這個結果就是把初始的值經過傳入compose中的個函數改造后的結果,一個簡易的compose實現如下:
function compose (...fncs){ fncs = fncs.reverse(); let result; return function (arg){ result = arg; for (let fnc of fncs){ result = fnc(result); } return result; } }
compose是從右到昨依次調用傳入其內部的函數鏈,還有一種從左到右的方式叫做pipe,即去掉compose源碼中的對函數鏈數組的reverse即可。
從上面對compose的分析中,不難看出,它就實現了對我們中間件的串聯,並對原始的dispatch方法的改造。
在第二章節中,thunk中間件的第二層柯里化函數即在compose內部被調用,並接收了經其右邊那個中間函數改造並返回dispatch方法作為入參,並返回一個新的函數,再在該函數內部添加自己的邏輯,最后調用右邊那個中間函數改造並返回dispatch方法接着執行前一個中間件的邏輯。當然如果只有一個thunk中間件被應用了,或者他出入傳入compose時的最后一個中間件,那么傳入的dispatch方法即為原始的store.dispatch方法。
thunk的第三層柯里化函數,即為被thunk改造后的dispatch方法:
// ... return function (action){ // thunk的內部邏輯 if (typeof action === 'function'){ return action(dispatch, getState, extraArgument); } // 調用經下一個中間件(在compose中為之前的中間件)改造后的dispatch方法(本層洋蔥殼的下一層),並傳入action return next(action); }; // ...
這個改造后的dispatch函數將通過compose傳入thunk左邊的那個中間件作為入參。
經上述分析,我們可以得出一個中間件的串聯和執行時的流程,以下面這段使用applyMiddleware的代碼為例:
export default createStore(reducer, applyMiddleware(middleware1, middleware2, middleware3));
在applyMiddlware內部的compose串聯中間件時,順序是從右至左,就是先調用middleware3、再middleware2、最后middleware1。middleware3最開始接收真正的store.dispatch作為入參,並返回改造的的dispatch函數作為入參傳給middleware2,這個改造后的函數內部包含有對原始store.dispatch的調用。依次內推知道從右到左走完所有的中間件。整個過程就像是給原始的store.dispatch方法套上了一層又一層的殼子,最后得到了一個類似於洋蔥結構的東西,也就是下面源碼中的dispatch,這個經過中間件改造並返回的dispatch方法將替換store被展開后的原始的dispatch方法:
// ... return { ...store, dispatch }; // ...
而原始的store.dispatch就像這洋蔥內部的芯,被覆蓋在了一層又一層的殼的最里面。
而當我們剝殼的時候,剝一層殼,執行一層的邏輯,即走一層中間件的功能,直至調用藏在最里邊的原始的store.dispatch方法去派發action。這樣一來我們就不需要在每次派發action的時候再寫單獨的代碼邏輯的。
總結來說就是:
在中間件串聯的時候,middleware1-3的串聯順序是從右至左的,也就是middleware3被包裹在了最里面,它內部含有對原始的store.dispatch的調用,middleware1被包裹在了最外邊。
當我們在業務代碼中dispatch一個action時,也就是中間件執行的時候,middleware1-3的執行順序是從左至右的,因為最后被包裹的中間件,將被最先執行。
如圖所示:
至此為止,關於applyMiddleware和thunk中間件的分析就完成了,如果問題和不清楚之處煩請指出。