Redux的中間件原理分析

redux的中間件對於使用過redux的各位都不會感到陌生,經過應用上咱們須要的全部要應用在redux流程上的中間件,咱們能夠增強dispatch的功能。最近抽了點時間把以前整理分析過的中間件有關的東西放在這裏分享分享。本文只對中間件涉及到的createStore、applyMiddleware以及典型經常使用中間的的源碼作解析,讓你們瞭解redux的內部模塊:createStore.js、applyMiddleware.js,以及redux的中間件之間是怎麼串聯在一塊兒並協做工做的。文章內容特別是源碼部分對函數式編程思想有必定要求,好比:柯里化、compose等,源碼中會大量涉及到這些概念,若是讀者對此是不熟悉,可先學習這方面相關資料。

1、thunk做爲一個典型redux中間件,它作了什麼事?編程

簡單的thunk使用方式以下:json

// 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,還有更專業的其餘中間件,這非本文內容,這裏再也不多講。redux

 

2、thunk中間件內部是什麼樣的?api

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對象。它的源碼也很簡單,如上所示,除去語法固定格式也就區區幾行。閉包

下面咱們就來看看源碼(爲了方便閱讀,源碼中的箭頭函數在這裏換成了普通函數),首先是這三層柯里化:app

// 外層
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中間件的基本寫法要求,也就是說不管一箇中間件要實現一個怎樣的功能,其固定格式必須是這個,在第三層函數內部纔是本身功能邏輯實現的地方。

記住這三層作的事情很重要(雖然憑藉着這極少的信息,咱們依然很懵逼),但在下一個段落中,咱們將再次提到它們,並詳細說明爲何會有這三層柯里化的存在。

 

3、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等方法的對象。

 

4、回過頭對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串聯中間件時,順序是從右至左,就是先調用middleware三、再middleware二、最後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中間件的分析就完成了,若是問題和不清楚之處煩請指出。

相關文章
相關標籤/搜索