Open libin1991 opened 6 years ago
上一篇文章介绍了Redux的数据中心,并分别讲解了数据中心为开发者提供的各种接口,了解到要触发状态的更新就需要调用dispatch方法来分发action。然而store提供的dispatch方法只能够用来分发特定格式的action。
dispatch
action
store
如果我们想要更强大的功能怎么办?如果我们想要打印状态变化前后的日志?或者说想自定义某类方法来作为dispatch的参数?我们当然可以重写原来的dispatch方法,然而这并不优雅,维护成本相对较高。这篇文章想详细讲解一下,在函数式编程的背景下如何以中间件的方式优雅地扩展我们的dispatch方法。
中间件这个概念存在于许多流行的Web框架中,可以把它想象成是请求/响应分发的中间层,用于对请求/响应做进一步的处理,而无需改变原有的代码逻辑。在node.js社区的KOA轻量级框架很出色地体现了这一点(当然它肯定不是第一个这样干的人)。koa本身只提供了最基础的请求/响应功能,如果想要更强大的功能(比如说日志,时间记录等功能)则需要自己添加相应的中间件。
node.js
Redux继承了这一理念,它把中间件应用到了dispatch方法的扩展中,让我们可以优雅地扩展dispatch方法,而不需要重写原有的dispatch方法,接下来我们好好体会一下它的精妙之处。
在分析源码之前先来看看在Redux里面如何使用中间件,最关键的是applyMiddleware这个方法
applyMiddleware
import { createStore, applyMiddleware } from 'redux' // Add thunk import thunk from 'redux-thunk' import logger from 'redux-logger' const reducer = (state) => state let newCreateStore = applyMiddleware( logger, thunk )(createStore) // 创建store,数据中心 let store = newCreateStore(reducer)
其中thunk跟logger就是我们提到的中间件,依次把它们传入applyMiddleware函数中,就会返回一个新的函数,然后再用这个函数处理原始的createStore方法就会返回一个增强过的createStore方法。
thunk
logger
createStore
另外,还记得createStore函数可以接收enhancer这个参数不?其实applyMiddleware这个方法经过调用后所得到的就是一个增强器。为此我们还可以这样调用createStore,并生成store。
enhancer
.... let enhancer = applyMiddleware( logger, thunk ) let store = createStore(reducer, enhancer)
这种做法跟前面的扩展效果是一样的。
在源码分析之前,先举个例子来看看一个简单的中间件内部应该是什么样子的,我分别定义middleware1,middleware2两个中间件(他们本质是高阶函数),并用来扩展originDispatch函数
中间件
middleware1
middleware2
originDispatch
let originDispatch = (...args) => { console.log(...args) } const middleware1 = (dispatch) => { return (...args) => { console.log('middleware1 before dispatch') dispatch(...args) console.log('middleware1 after dispatch') } } const middleware2 = (dispatch) => { return (...args) => { console.log('middleware2 before dispatch') dispatch(...args) console.log('middleware2 before dispatch') } } originDispatch = middleware2(middleware1(originDispatch)) originDispatch('ruby', 'cool', 'language')
结果如下
middleware2 before dispatch middleware1 before dispatch ruby cool language middleware1 after dispatch middleware2 before dispatch
是不是运行过程是不是有点像洋葱?我们可以使用中间件来对原有的方法进行增强,并返回一个增强了的方法,然后再用另一个中间件来对这个已经增强过的方法再进一步增强,模型示意图如下
从上面的洋葱模型可以看出我们如果要增强一个方法,它的步骤如下
newFunc = f1(f2(func))
可以简单地把f1,f2理解成我们需要各自定义的中间件函数,然而如果我们每次都要手动调用这些方法的话似乎并不太优雅,这个时候可以使用compose函数来完成这种事情。
f1
f2
compose在中文里面是组合的意思,Redux所定义的compose函数可以把函数的参数列表构造成依次调用的形式,并返回一个新的函数。它的源码如下
compose
export default function compose(...funcs) { ... // 以上都是判断 return funcs.reduce((a, b) => (...args) => a(b(...args))) }
文字解释可能还不如流程图来得直观,下面简单地分析一下compose(f1, f2, f3, f4)的调用过程
compose(f1, f2, f3, f4)
a: f1, b: f2, return: (...args) => f1(f2(...args)) a: (...args) => f1(f2(...args)), b: f3, return: (...args) => f1(f2(f3(...args))) a: (...args) => f1(f2(f3(...args))), b: f4, return: (...args) => f1(f2(f3(f4(...args))))
把这个方法应用在最初的例子中
> newfunc = compose(middleware2, middleware1)(originDispatch) [Function] > newfunc('node', 'good', 'languate') middleware2 before dispatch middleware1 before dispatch node good languate middleware1 after dispatch middleware2 before dispatch
结果是一样的。而且从这个例子还可以看出在compose函数的参数列表中越靠后的函数,在构造完成之后,距离原始函数就越近。
applyMiddleware.js这个文件里面就包含着它的源码
export default function applyMiddleware(...middlewares) { return createStore => (...args) => { const store = createStore(...args) let dispatch = () => { 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) } // #1 中间件应该接收store const chain = middlewares.map(middleware => middleware(middlewareAPI)) // #2 返回的函数用于处理dispatch函数 dispatch = compose(...chain)(store.dispatch) // #3 替换dispatch return { ...store, dispatch } } }
代码片段#2中我们传入compose函数里的所有函数都是用于扩展dispatch的,这些函数会被定义为这种形式
#2
(dispatch) => { return function(...args) { // do something before dispatch(...args) // do something after } }
这些函数会接收一个dispatch方法为参数,并返回一个增强的dispatch方法。然而我们需要编写的中间件却不仅如此,接下来再看看代码片段#1,以及相关的上下文逻辑
#1
export default function applyMiddleware(...middlewares) { .... const middlewareAPI = { getState: store.getState, dispatch: (...args) => dispatch(...args) } // #1 中间件应该接收store const chain = middlewares.map(middleware => middleware(middlewareAPI)) // #2 返回的函数用于处理dispatch函数 dispatch = compose(...chain)(store.dispatch) ... }
我们通过map方法来处理applyMiddleware所接收的所有中间件,让他们分别以middlewareAPI这个对象作为参数调用过后会返回一个新的函数列表,而这个函数列表才是真正用来增强dispatch的。
map
middlewareAPI
middlewareAPI是仅仅包含了getState与dispatch这两个字段的对象,可以把它看成是一个精简版的store。因此我们需要编写的中间件应该是以store作为参数,并且返回一个用于增强dispatch方法的函数,而这个store我们只能够使用getState,dispatch这两个接口。听起来有点拗口,下面我们自行编写一个用于打印状态日志的中间件。
getState
const Logger = (store) => (dispatch) => { return function(...args) { const wrappedDispatch = store.dispatch const getState = store.getState console.log('before dispatch', getState()) dispatch(...args) console.log('after dispatch', getState()) console.info(dispatch) console.info(wrappedDispatch) } }
其中dispatch与wrappedDispatch所指代的分发方法是不一样的。
wrappedDispatch
dispatch是从参数中传入,如果当前中间件是第一个对dispatch方法进行增强的中间件,则当前的dispatch所指向的就是Redux原生定义的dispatch方法。如果当前中间件前面已经有若干中间件的调用,则当前dispatch所指代的是经过前面中间件加强过的新的dispatch方法。我们可以来验证一下
let enhancer = applyMiddleware( Logger, // 我们自己编写的Logger thunk )
dispatch的打印结果如下
ƒ (action) { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }
可见,这是一个经过thunk中间件处理后返回的方法。
wrappedDispatch因为匿名函数(...args) => dispatch(...args)的关系,在applyMiddleware函数运行完成并返回之后,匿名函数内部的dispatch会始终指向经过我们增强的dispatch方法。也就是说在中间件里面执行store.dispatch就会始终运行最外层的被增强过的dispatch方法。模型如下
(...args) => dispatch(...args)
store.dispatch
wrappedDispatch打印结果虽然看不出什么,但我也顺手贴一下吧
ƒ dispatch() { return _dispatch.apply(undefined, arguments); }
接下来,我们看applyMiddleware的返回值,它会返回一个新的函数,该函数会以createStore作为参数,处理过后返回一个新的createStore方法,它的模式大概是这样子
(createStore) => (...args) => { // createStore方法用来创建store return { ... getState: ... dispatch: ... } }
而在applyMiddleware中实际上我们只需要增强dispatch方法,为此我们只需要用新的dispatch方法来替换原来的便可。代码片段#3就是用新的dispatch方法取代原来store中的dispatch方法。
#3
.... return { ...store, dispatch } ....
本章着重介绍了Redux中的中间件的原理,我们可以通过洋葱模型来增强dispatch函数。还可以通过compose方法构造调用链,使得我们的调用逻辑更加优雅。分析的过程中我还顺手编写了一个Logger中间件,用于打印action分发前后的状态,我们也可以根据自己的需求来编写属于自己的中间件。
Logger
上一篇文章介绍了Redux的数据中心,并分别讲解了数据中心为开发者提供的各种接口,了解到要触发状态的更新就需要调用
dispatch
方法来分发action
。然而store
提供的dispatch
方法只能够用来分发特定格式的action
。如果我们想要更强大的功能怎么办?如果我们想要打印状态变化前后的日志?或者说想自定义某类方法来作为
dispatch
的参数?我们当然可以重写原来的dispatch
方法,然而这并不优雅,维护成本相对较高。这篇文章想详细讲解一下,在函数式编程的背景下如何以中间件的方式优雅地扩展我们的dispatch
方法。1. 中间件
中间件这个概念存在于许多流行的Web框架中,可以把它想象成是请求/响应分发的中间层,用于对请求/响应做进一步的处理,而无需改变原有的代码逻辑。在
node.js
社区的KOA轻量级框架很出色地体现了这一点(当然它肯定不是第一个这样干的人)。koa本身只提供了最基础的请求/响应功能,如果想要更强大的功能(比如说日志,时间记录等功能)则需要自己添加相应的中间件。Redux继承了这一理念,它把中间件应用到了
dispatch
方法的扩展中,让我们可以优雅地扩展dispatch
方法,而不需要重写原有的dispatch
方法,接下来我们好好体会一下它的精妙之处。2. 中间件在Redux中的应用
在分析源码之前先来看看在Redux里面如何使用中间件,最关键的是
applyMiddleware
这个方法import { createStore, applyMiddleware } from 'redux' // Add thunk import thunk from 'redux-thunk' import logger from 'redux-logger' const reducer = (state) => state let newCreateStore = applyMiddleware( logger, thunk )(createStore) // 创建store,数据中心 let store = newCreateStore(reducer)
其中
thunk
跟logger
就是我们提到的中间件,依次把它们传入applyMiddleware
函数中,就会返回一个新的函数,然后再用这个函数处理原始的createStore
方法就会返回一个增强过的createStore
方法。另外,还记得
createStore
函数可以接收enhancer
这个参数不?其实applyMiddleware
这个方法经过调用后所得到的就是一个增强器。为此我们还可以这样调用createStore
,并生成store
。.... let enhancer = applyMiddleware( logger, thunk ) let store = createStore(reducer, enhancer)
这种做法跟前面的扩展效果是一样的。
3. 源码分析
1) 中间件原理
在源码分析之前,先举个例子来看看一个简单的
中间件
内部应该是什么样子的,我分别定义middleware1
,middleware2
两个中间件(他们本质是高阶函数),并用来扩展originDispatch
函数let originDispatch = (...args) => { console.log(...args) } const middleware1 = (dispatch) => { return (...args) => { console.log('middleware1 before dispatch') dispatch(...args) console.log('middleware1 after dispatch') } } const middleware2 = (dispatch) => { return (...args) => { console.log('middleware2 before dispatch') dispatch(...args) console.log('middleware2 before dispatch') } } originDispatch = middleware2(middleware1(originDispatch)) originDispatch('ruby', 'cool', 'language')
结果如下
是不是运行过程是不是有点像洋葱?我们可以使用中间件来对原有的方法进行增强,并返回一个增强了的方法,然后再用另一个中间件来对这个已经增强过的方法再进一步增强,模型示意图如下
2) compose--方法封链辅助函数
从上面的洋葱模型可以看出我们如果要增强一个方法,它的步骤如下
可以简单地把
f1
,f2
理解成我们需要各自定义的中间件函数,然而如果我们每次都要手动调用这些方法的话似乎并不太优雅,这个时候可以使用compose函数来完成这种事情。compose
在中文里面是组合的意思,Redux所定义的compose
函数可以把函数的参数列表构造成依次调用的形式,并返回一个新的函数。它的源码如下export default function compose(...funcs) { ... // 以上都是判断 return funcs.reduce((a, b) => (...args) => a(b(...args))) }
文字解释可能还不如流程图来得直观,下面简单地分析一下
compose(f1, f2, f3, f4)
的调用过程a: f1, b: f2, return: (...args) => f1(f2(...args)) a: (...args) => f1(f2(...args)), b: f3, return: (...args) => f1(f2(f3(...args))) a: (...args) => f1(f2(f3(...args))), b: f4, return: (...args) => f1(f2(f3(f4(...args))))
把这个方法应用在最初的例子中
> newfunc = compose(middleware2, middleware1)(originDispatch) [Function] > newfunc('node', 'good', 'languate') middleware2 before dispatch middleware1 before dispatch node good languate middleware1 after dispatch middleware2 before dispatch
结果是一样的。而且从这个例子还可以看出在
compose
函数的参数列表中越靠后的函数,在构造完成之后,距离原始函数就越近。3) applyMiddleware--收集中间件,扩展createStore
applyMiddleware.js这个文件里面就包含着它的源码
export default function applyMiddleware(...middlewares) { return createStore => (...args) => { const store = createStore(...args) let dispatch = () => { 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) } // #1 中间件应该接收store const chain = middlewares.map(middleware => middleware(middlewareAPI)) // #2 返回的函数用于处理dispatch函数 dispatch = compose(...chain)(store.dispatch) // #3 替换dispatch return { ...store, dispatch } } }
代码片段
#2
中我们传入compose
函数里的所有函数都是用于扩展dispatch
的,这些函数会被定义为这种形式(dispatch) => { return function(...args) { // do something before dispatch(...args) // do something after } }
这些函数会接收一个
dispatch
方法为参数,并返回一个增强的dispatch
方法。然而我们需要编写的中间件却不仅如此,接下来再看看代码片段#1
,以及相关的上下文逻辑export default function applyMiddleware(...middlewares) { .... const middlewareAPI = { getState: store.getState, dispatch: (...args) => dispatch(...args) } // #1 中间件应该接收store const chain = middlewares.map(middleware => middleware(middlewareAPI)) // #2 返回的函数用于处理dispatch函数 dispatch = compose(...chain)(store.dispatch) ... }
我们通过
map
方法来处理applyMiddleware
所接收的所有中间件,让他们分别以middlewareAPI
这个对象作为参数调用过后会返回一个新的函数列表,而这个函数列表才是真正用来增强dispatch
的。middlewareAPI
是仅仅包含了getState
与dispatch
这两个字段的对象,可以把它看成是一个精简版的store
。因此我们需要编写的中间件应该是以store
作为参数,并且返回一个用于增强dispatch
方法的函数,而这个store
我们只能够使用getState
,dispatch
这两个接口。听起来有点拗口,下面我们自行编写一个用于打印状态日志的中间件。const Logger = (store) => (dispatch) => { return function(...args) { const wrappedDispatch = store.dispatch const getState = store.getState console.log('before dispatch', getState()) dispatch(...args) console.log('after dispatch', getState()) console.info(dispatch) console.info(wrappedDispatch) } }
其中
dispatch
与wrappedDispatch
所指代的分发方法是不一样的。dispatch
是从参数中传入,如果当前中间件是第一个对dispatch
方法进行增强的中间件,则当前的dispatch
所指向的就是Redux原生定义的dispatch
方法。如果当前中间件前面已经有若干中间件的调用,则当前dispatch
所指代的是经过前面中间件加强过的新的dispatch
方法。我们可以来验证一下let enhancer = applyMiddleware( Logger, // 我们自己编写的Logger thunk )
dispatch
的打印结果如下ƒ (action) { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }
可见,这是一个经过
thunk
中间件处理后返回的方法。wrappedDispatch
因为匿名函数(...args) => dispatch(...args)
的关系,在applyMiddleware
函数运行完成并返回之后,匿名函数内部的dispatch
会始终指向经过我们增强的dispatch
方法。也就是说在中间件里面执行store.dispatch
就会始终运行最外层的被增强过的dispatch
方法。模型如下wrappedDispatch
打印结果虽然看不出什么,但我也顺手贴一下吧ƒ dispatch() { return _dispatch.apply(undefined, arguments); }
接下来,我们看
applyMiddleware
的返回值,它会返回一个新的函数,该函数会以createStore
作为参数,处理过后返回一个新的createStore
方法,它的模式大概是这样子(createStore) => (...args) => { // createStore方法用来创建store return { ... getState: ... dispatch: ... } }
而在
applyMiddleware
中实际上我们只需要增强dispatch
方法,为此我们只需要用新的dispatch
方法来替换原来的便可。代码片段#3
就是用新的dispatch
方法取代原来store
中的dispatch
方法。4. 尾声
本章着重介绍了Redux中的中间件的原理,我们可以通过洋葱模型来增强
dispatch
函数。还可以通过compose
方法构造调用链,使得我们的调用逻辑更加优雅。分析的过程中我还顺手编写了一个Logger
中间件,用于打印action
分发前后的状态,我们也可以根据自己的需求来编写属于自己的中间件。Happy Coding and Writing!!!