Redux中间件原理

613 阅读5分钟
Redux是优秀出色的JavaScript状态管理,深受前端开发者喜爱,很普遍的都是在react中使用!Redux的源码很少、很清晰,里面用到了不常用的魔法。最近再看Redux时候,中间件的实现原理引起了我的注意,所以通过此篇文章来记录一下理解的过程!

中间件的本质

其实Redux实现的中间件是使用了中间件模式,通过状态改变之前 -> 状态改变 -> 状态改变之后来获取状态的变化。比如在Redux中众所周知的中间件logger,通过使用logger中间件可以在浏览器控制台看到状态的反应变化,这样就可以实时的看到状态是怎样变化的了!那么,Redux中间件是怎样实现的呢?

中间件实现原理

像Express或koa等等的服务端框架对大家来说都不陌生,其中Express的中间件调用是根据回调参数来进行调用的,下面是一段来自Express官方文档的中间件代码示例:

var app = express()

app.use(function (req, res, next) {
  console.log('Time:', Date.now())
  next()
})

可以看到,想要执行中间件的时候必须要调用next函数( 中间件函数的回调 ),从而把控制权交给下一个中间件!那么,Redux的中间件是怎么实现的呢?下面来一段关键部分的源码:

let dispatch: Dispatch = () => {
    throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
         'Other middleware would not be applied to this dispatch.'
    )
}
const middlewareAPI: MiddlewareAPI = {
    getState: store.getState,
    dispatch: (action, ...args) => dispatch(action, ...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose<typeof dispatch>(...chain)(store.dispatch)
return {
    ...
}

先来看一下中间件的写法:

const middleware = store => next => action => {}
  • middlewareAPI: 是一个对象,包含了store中的getState方法和一个dispatch函数,这个对象是中间件的参数,也就是对应了store参数。
  • chain: 是一个函数数组
  • dispatch: 是一个返回compose函数之后的函数,也就是第一次执行把store.dispatch(原始的dispatch)向compose进行传参后,返回新的dispatch函数,记为dispatch2。下一次再执行的时候通过把dispatch2传入,然后再返回一个新的dispatch,记为dispatch3。以此类推!

这样的话,通过管道的方式(上一个的结果是下一个函数的输入参数)最终返回了一个全新的dispatch,包含了所有中间件的dispatch,这样在调用store.dispatch的话会调用所有中间返回的最后一个函数,即:(action) => {}。

理解中间件

为了理解Redux的applyMiddleware,我特意的写了一个小demo,以验证Redux的这种中间件实现方式的本质,下面来看一下:

function logger(store) {
    return function logger1(next) {        
        return function logger2(action) {            
            console.log('logger之前')
            let result = next(action)
            console.log('logger之后')
            return result        
        }
    }
}
function increment(store) {
    return function increment1(next) {
        return function increment2(action) {
            console.log('increment之前')
            let result = next(action)
            console.log('increment之后')
            return result
        }
    }
}
function applyMiddleWare(...middleWares) {
    let dispatch: Function = () => console.log('原始的dispatch')
     const chain = middleWares.map(middleware => middleware({}))
     dispatch = flowRight(...chain)(dispatch)
     return {
        dispatch
    }
}
const apply = applyMiddleWare(logger, increment)
apply.dispatch()

函数名字都是随意取的,为了看到更多的细节,我在返回函数中也为函数命名了名字,这样在debugger的时候可以看到更多的细节,这也是不用ES6箭头写法的原因。

这里一共有两个中间件,logger和increment,flowRight函数是模仿lodash.flowRight()方法,返回了一个函数,也就是compose!applyMiddleWare函数和Redux的applyMiddleware函数写的基本一致,但这里为了模拟,所以store用了一个空对象。

当调用apply.dispatch()函数的时候会输出中间件以及原始的dispatch结果,其中的细节来通过debugger来看一下。

debugger
const apply = applyMiddleWare(logger, increment)


当走到chain这里会执行一次中间件,可以看到是走到了logger函数内,下一次执行increment函数,最终的chain的结果是[next => action => { ... }, next => action => { ... }]。

然后执行flowRight,由于flowRight是返回函数的函数,那么执行这一段之后的最终结果(dispatch)是上述Redux源码介绍的那样,是符合Redux实现的applyMiddleware函数的。

接下来调用apply.dispatch(),这次debugger这一段代码:

debugger
apply.dispatch()


运行logger2函数,执行console.log('logger之前'),继续往下执行,运行next(action)


可以发现运行next(action)之后,logger函数的控制权交给了increment2函数!

接下来重复上述步骤,当increment2函数执行的时候,发现没有中间件了,所以next这时指向原始的dispatch:


这时,在所有中间件函数之前的语句和原始的dispatch函数都已执行完毕。接下来运行next后面的语句:



发现原始的dispatch函数执行完之后退出栈,将控制权交还给上一个dispatch,即:logger2!

当logger2也执行完毕之后,控制权交还给increment2!最终都执行完之后,退栈,销毁。

来看一下最终的结果:


输出结果:dispatch之前 -> dispatch -> dispatch之后

通过调试,可以清晰的看到是怎样执行的。

即使没用过Redux,也应该用过axios,跟axios拦截器原理差不多,也就是请求之前 -> 请求 -> 响应之前 -> 响应结果!

可能有些人会有疑问,为什么applyMiddleware函数返回全新的dispatch,调用之后会执行中间件里的函数呢?这都要靠函数的特性:闭包!通过闭包,记住了每次compose的dispatch,从而会依次执行中间件并把每一次的dispatch传进去。

来看一下Redux的compose实现:


官方用到了reduce方法,可以返回累积的结果!不同的是,这里是返回了一个函数,这个返回的函数包含了累积的函数。

以上述的demo,运行过程是:

  • 第一次compose,a -> logger1,b -> increment1,参数是原始的dispatch,执行b(dispatch)返回了包装后的dispatch,即:
  • let dispatch2 = b(dispatch)
    dispatch = a(dispatch2)
  • 第二次compose,发现没有可执行的中间件了,那么就会退出,此时:
  • /** Compose Before: (...args) => () => { ... } 
     * 执行compose(...chain)(dispatch)
     * Compose After: () => { ... } 
     */
    Result: dispatch = () => { /** 这里是compose之后的结果 */ }

此时,在使用Redux的store.dispatch()就会运行结果了,就像官方的logger中间件一样,在控制台输出状态的变化!