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中间件的分析就完成了,若是问题和不清楚之处烦请指出。

相关文章
相关标签/搜索