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中间件一样,在控制台输出状态的变化!