用法
为了对中间件有一个整体的认识,先从用法开始分析。调用中间件的代码如下:
export default function createStore(reducer, preloadedState, enhancer) {
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
}
enhancer
是中间件,且第二个参数为 Function
且没有第三个参数时,可以转移到第二个参数,那么就有两种方式设置中间件:
const store = createStore(reducer, null, applyMiddleware(...))
const store = createStore(reducer, applyMiddleware(...))
再看 源码 中间件的传参:
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
var store = createStore(reducer, preloadedState, enhancer)
...
}
就是为了得到 store
,并通过 createStore
创建,上述两种方法因为在 createStore
函数内部传入了自身函数才得以实现 :
export default function createStore(reducer, preloadedState, enhancer) {
...
if (typeof enhancer !== 'undefined') {
return enhancer(createStore)(reducer, preloadedState)
}
...
}
上述代码可以看出,创建 store 的过程完全交给中间件了,因此开启了中间件第三种使用方式:
const store = applyMiddleware(...)(createStore)
applyMiddleware 源码解析
大家对剖析 applyMiddleware
源码都非常感兴趣,因为它实现精简,但含义甚广,再重温其源码:
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
var store = createStore(reducer, preloadedState, enhancer)
var dispatch = store.dispatch
var chain = []
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
假设大家都已了解 ES6 7 语法,懂得 compose
函数的含义,并且看过一些源码剖析了,我们才能把重点放在核心原理上:为什么中间件函数有三个传参 store => next => action
,第二个参数 next
为什么拥有神奇的作用?
store
代码前几行创建了 store
(如果第三个参数是中间件,就会出现中间件 store 包中间件 store 的情况,但效果是完全 打平 的), middlewareAPI
这个变量,其实就是精简的 store
, 因为它提供了 getState
获取数据,dispatch
派发动作。
下一行,middlewares.map
将这个 store
作为参数执行了一遍中间件,所以中间件第一级参数 store
就是这么来的。
next
下一步我们得到了 chain
, 倒推来看,其中每个中间件只有 next => action
两级参数了。我们假设只有一个中间件 fn
,因此 compose
的效果是:
dispatch = fn(store.dispatch)
那么 next
参数也知道了,就是 store.dispatch
这个原始的 dispatch
.
action
代码的最后,返回了 dispatch
,我们一般会这么用:
store.dispatch(action)
等价于
fn(store.dispatch)(action)
第三个参数也来了,它就是用户自己传的 action
.
单一中间件的场景
我们展开代码来查看一个中间件的运行情况:
fn(middlewareAPI)(store.dispatch)(action)
对应 fn
的代码可能是:
export default store => next => action => {
console.log('beforeState', store.getState())
next(action)
console.log('nextState', store.getState())
}
当我们执行了 next(action)
后,相当于调用了原始 store
dispatch
方法,并将 action
传入其中,可想而知,下一行输出的 state
已经是更新后的了。
但是 next
仅仅是 store.dispatch
, 为什么叫做 next
我们现在还看不出来。
function dispatch(action) {
...
currentState = currentReducer(currentState, action)
...
}
其中还有一段更新监听数组对象,以达到 dispatch
过程不受干扰(快照效果) 作为课后作业大家独立研究:主要思考这段代码的意图:https://github.com/reactjs/redux/blob/master/src/createStore.js#L63
多中间件的场景
我们假设有三个中间件 fn1
fn2
fn3
, 从源码的这两句入手:
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
第一行代码,我们得到了只剩 next => action
参数的 chain
, 暂且叫做:
cfn1
cfn2
cfn3
, 并且有如下对应关系
cfnx = fnx(middlewareAPI)
第二行代码展开后是这样的:
dispatch = cfn1(cfn2(cfn3(store.dispatch)))
可以看到最后传入的中间件 fn3
最先执行。
为了便于后面理解,我先把上面代码的含义写出来:通过传入原始的 store.dispatch
, 希望通过层层中间件的调用,最后产生一个新的 dispatch
. 那么实际上,中间件所组成的 dispatch
, 从函数角度看,就是被执行过一次的 cfn1
cfn2
cfn3
函数。
我们就算不理解新 dispatch
的含义,也可以从代码角度理解:只要执行了新的 dispatch
, 中间件函数 cfnx
系列就要被执行一次,所以 cfnx
的函数本身就是中间件的 dispatch
。
对应 cfn3
的代码可能是:
export default next => action => {
next(action)
}
这就是这个中间件的 dispatch
.
那么执行了 cfn3
后,也就是 dispatch
了之后,其内部可能没有返回值,我们叫做 ncfn3
,大概如下:
export default action => {}
但其函数自身就是返回值 返回给了 cfn2
作为第一个参数,替代了 cnf3
参数 store.dispatch
的位置。
我们再想想,store.dispatch
的返回值是什么?不就是 action => {}
这样的函数吗?这样,一个中间件的 dispatch
传递完成了。我们理解了多中间件 compose
后可以为什么可以组成一个新的 dispatch
了(其实单一中间件也一样,但因为步骤只有一步,让人会想到直接触发 store.dispatch
上,多中间件提炼了这个行为,上升到组合为新的 dispatch
)。
再解释 next 的含义为什么我们在中间件中执行 next(action)
,下一步就能拿到修改过的 store
?
对于 cfn3
来说, next
就是 store.dispatch
。我们先不考虑它为什么是 next
, 但执行它了就会直接执行 store.dispatch
,后面立马拿到修改后的数据不奇怪吧。
对于 cfn2
来说,next
就是 cfn3
执行后的返回值(执行后也还是个函数,内层并没有执行),我们分为两种情况:
-
cfn3
没有执行next(action)
,那cfn1
cfn2
都没法执行store.dispatch
,因为原始的dispatch
没有传递下去,你会发现dispatch
函数被中间件搞失效了(所以中间件还可以捣乱)。为了防止中间件瞎捣乱,在中间件正常的情况请执行next(action)
.
这就是
redux-thunk
的核心思想,如果action
是个function
,就故意执行action
, 而不执行next(action)
, 等于让store.dispatch
失效了!但其目的是明确的,因为会把dispatch
返回给用户,让用户自己调用,正常使用是不会把流程停下来的。
-
cfn3
执行了next(action)
, 那cfn2
什么时候执行next(action)
,cfn3
就什么时候执行next(action) => store.dispatch(action)
, 所以这一步的next
效果与cfn3
相同,继续传递下去也同理。我看了下redux-logger
的文档,果然央求用户把自己放在最后一个,其原因是害怕最右边的中间件『捣乱』,不执行next(action)
, 那logger
再执行next(action)
也无法真正触发dispatch
.
我在考虑这样会不会有很大的局限性,但后来发现,只要中间件常规情况执行了
next(action)
就能保证原始的dispatch
可以被继续分发下去。只要每个中间件都按照这个套路来,next(action)
的效果就与yield
类似。
所以 next
并不是完全意义上的洋葱模型,只能说符合规范(默认都执行了 next(action)
)的中间件才符合洋葱模型。
koa 的洋葱模型可是有技术保证的,
generator
可不会受到代码的影响,而redux
中间件的洋葱模型,会因为某一层不执行next(action)
而中断,而且从右开始直接切断。
为什么在中间件直接 store.dispatch(action)
,传递就会中断?
理解了上面说的话,就很简单了,并不是 store.dispatch(action)
中断了原始 dispatch
的传递,而是你执行完以后不调用 next
函数中断了传递。
总结
还是要画个图总结一下,在不想看文字的时候:
这是 redux 作者 Dan 对 middleware 的描述,middleware 提供了一个分类处理 action 的机会,在 middleware 中你可以检阅每一个流过的 action,挑选出特定类型的 action 进行相应操作,给你一次改变 action 的机会。
为什么 dispatch 需要 middleware
上图表达的是 redux 中一个简单的同步数据流动场景,点击 button 后,在回调中 dispatch 一个 action,reducer 收到 action 后,更新 state 并通知 view 重新渲染。单向数据流,看着没什么问题。但是,如果需要打印每一个 action 信息用来调试,就得去改 dispatch 或者 reducer 代码,使其具有打印日志的功能;又比如点击 button 后,需要先去服务器请求数据,只有等拿到数据后,才能重新渲染 view,此时我们又希望 dispatch 或者 reducer 拥有异步请求的功能;再比如需要异步请求完数据后,打印一条日志,再请求数据,再打印日志,再渲染...
面对多种多样的业务需求,单纯的修改 dispatch 或 reducer 的代码显然不具有普世性,我们需要的是可以组合的,自由插拔的插件机制,这一点 redux 借鉴了 koa 里中间件的思想,koa 是用于构建 web 应用的 NodeJS 框架。另外 reducer 更关心的是数据的转化逻辑,所以 redux 的 middleware 是为了增强 dispatch 而出现的。
上面这张图展示了应用 middleware 后 redux 处理事件的逻辑,每一个 middleware 处理一个相对独立的业务需求,通过串联不同的 middleware,实现变化多样的的功能。那么问题来了:
-
middleware 怎么写?
-
redux 是如何让 middlewares 串联并跑起来的?
四步理解 middleware 机制
redux 提供了 applyMiddleware 这个 api 来加载 middleware,为了方便理解,下图将两者的源码放在一起进行分析。
图左边是 logger,打印 action 的 middleware,图右边则是 applyMiddleware 的源码,applyMiddleware 代码虽然只有二十多行,却非常精炼,接下来我们就分四步来深入解析这张图。
redux 的代码都是用 ES6/7 写的,所以不熟悉诸如
store => next => action =>
或...state
的童鞋,可以先学习下箭头函数,展开运算符。
Step. 1 函数式编程思想设计 middleware
middleware 的设计有点特殊,是一个层层包裹的匿名函数,这其实是函数式编程中的柯里化 curry,一种使用匿名单参数函数来实现多参数函数的方法。applyMiddleware 会对 logger 这个 middleware 进行层层调用,动态地对 store 和 next 参数赋值。
柯里化的 middleware 结构好处在于:
-
易串联,柯里化函数具有延迟执行的特性,通过不断柯里化形成的 middleware 可以累积参数,配合组合( compose,函数式编程的概念,Step. 2 中会介绍)的方式,很容易形成 pipeline 来处理数据流。
-
共享store,在 applyMiddleware 执行过程中,store 还是旧的,但是因为闭包的存在,applyMiddleware 完成后,所有的 middlewares 内部拿到的 store 是最新且相同的。
另外,我们可以发现 applyMiddleware 的结构也是一个多层柯里化的函数,借助 compose , applyMiddleware 可以用来和其他插件一起加强 createStore 函数。
import { createStore, applyMiddleware, compose } from 'redux';
import rootReducer from '../reducers';
import DevTools from '../containers/DevTools';
const finalCreateStore = compose(
// Middleware you want to use in development:
applyMiddleware(d1, d2, d3),
// Required! Enable Redux DevTools with the monitors you chose
DevTools.instrument()
)(createStore);
Step. 2 给 middleware 分发 store
创建一个普通的 store 通过如下方式:
let newStore = applyMiddleware(mid1, mid2, mid3, ...)(createStore)(reducer, null);
上面代码执行完后,applyMiddleware 函数陆续获得了三个参数,第一个是 middlewares 数组,[md1, mid2, mid3, ...],第二个 next 是 Redux 原生的 createStore,最后一个是 reducer。接下来我们从对比图中可以看到,applyMiddleware 利用 createStore 和 reducer 创建了一个 store,然后 store 的 getState
方法和 dispatch
方法又分别被直接和间接地赋值给 middlewareAPI 变量,middlewareAPI 就是对比图中红色箭头所指向的函数的入参 store。
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
};
chain = middlewares.map(middleware => middleware(middlewareAPI));
然后让每个 middleware 带着 middlewareAPI 这个参数分别执行一遍,即执行红色箭头指向的函数。执行完后,获得 chain 数组,[f1, f2, ... , fx, ...,fn],它保存的对象是图中绿色箭头指向的匿名函数,因为闭包,每个匿名函数都可以访问相同的 store,即 middlewareAPI。
备注: middlewareAPI 中的 dispatch 为什么要用匿名函数包裹呢?
我们用 applyMiddleware 是为了改造 dispatch 的,所以 applyMiddleware 执行完后,dispatch 是变化了的,而 middlewareAPI 是 applyMiddleware 执行中分发到各个 middleware,所以必须用匿名函数包裹 dispatch, 这样只要 dispatch 更新了, middlewareAPI 中的 dispatch 应用也会发生变化。
Step. 3 组合串联 middlewares
dispatch = compose(...chain)(store.dispatch);
这一层只有一行代码,但却是 applyMiddleware 精华所在。compose 是函数式编程中的组合,compose 将 chain 中的所有匿名函数,[f1, f2, ... , fx, ..., fn],组装成一个新的函数,即新的 dispatch,当新 dispatch 执行时,[f1, f2, ... , fx, ..., fn],从左到右依次执行( 所以顺序很重要)。Redux 中 compose 的实现是下面这样的,当然实现方式不唯一。
function compose(...funcs) {
return arg => funcs.reduceRight((composed, f) => f(composed), arg);
}
compose(...chain)
返回的是一个匿名函数,函数里的 funcs 就是 chain 数组,当调用 reduceRight 时,依次从 funcs 数组的右端取一个函数 fx 拿来执行,fx 的参数 composed 就是前一次 fx+1 执行的结果,而第一次执行的fn(n代表chain的长度)的参数 arg 就是 store.dispatch。所以当 compose 执行完后,我们得到的 dispatch 是这样的,假设 n = 3。
dispatch = f1(f2(f3(store.dispatch))))
这个时候调用新 dispatch,每个 middleware 的代码不就依次执行了嘛。
Step. 4 在 middleware 中调用 dispatch 会发生什么
经过 compose,所有的 middleware 算是串联起来了,可是还有一个问题,我们有必要挖一挖。在 step 2 时,提到过每个 middleware 都可以访问 store,即 middlewareAPI 这个变量,所以就可以拿到 store 的 dispatch 方法,那么在 middleware 中调用 store.dispatch()
会发生什么,和调用 next()
有区别吗?比如下图:
在 step 2 的时候我们解释过,通过匿名函数的方式,middleware 中 拿到的 dispatch 和最终 compose 结束后的新 dispatch 是保持一致的,所以在middleware 中调用 store.dispatch()
和在其他任何地方调用效果是一样的,而在 middleware 中调用 next()
,效果是进入下一个 middleware。下面这张图说明一切。
正常情况下,如图左,当我们 dispatch 一个 action 时,middleware 通过 next(action)
一层一层处理和传递 action 直到 redux 原生的 dispatch。如果某个 middleware 使用 store.dispatch(action)
来分发 action,就发生了右图的情况,相当于从外层重新来一遍,假如这个 middleware 一直简单粗暴地调用 store.dispatch(action)
,就会形成无限循环了。那么 store.dispatch(action)
的勇武之地在哪里?正确的使用姿势应该是怎么样的?
举个例子,需要发送一个异步请求到服务器获取数据,成功后弹出一个自定义的 Message。这里我门用到了 redux-thunk 这个作者写的 middleware。
const thunk = store => next => action =>
typeof action === 'function' ?
action(store.dispatch, store.getState) :
next(action)
redux-thunk 做的事情就是判断 action 类型是否是函数,若是,则执行 action,若不是,则继续传递 action 到下个 middleware。
针对上面的需求,我们设计了下面的 action:
const getThenShow = (dispatch, getState) => {
const url = 'http://xxx.json';
fetch(url)
.then(response => {
dispatch({
type: 'SHOW_MESSAGE_FOR_ME',
message: response.json(),
});
}, e => {
dispatch({
type: 'FETCH_DATA_FAIL',
message: e,
});
});
};
这个时候只要在业务代码里面调用 store.dispatch(getThenShow)
,redux-thunk 就会拦截并执行 getThenShow 这个 action,getThenShow 会先请求数据,如果成功,dispatch 一个显示 Message 的 action,否则 dispatch 一个请求失败的 action。这里的 dispatch 就是通过 redux-thunk middleware 传递进来的。
在 middleware 中使用 dispatch 的场景一般是:
接受到一个定向 action,这个 action 并不希望到达原生的 dsipatch,存在的目的是为了触发其他新的 action,往往用在异步请求的需求里。
总结
applyMiddleware 机制的核心在于组合 compose,将不同的 middlewares 一层一层包裹到原生的 dispatch 之上,而为了方便进行 compose,需对 middleware 的设计采用柯里化 curry 的方式,达到动态产生 next 方法以及保持 store 的一致性。由于在 middleware 中,可以像在外部一样轻松访问到 store, 因此可以利用当前 store 的 state 来进行条件判断,用 dispatch 方法拦截老的 action 或发送新的 action。