redux(三)- 中间件
本文探讨下基于洋葱模型的中间件机制,及
redux applyMiddleware的实现原理。
引言#
当我们需要在某个函数执行前后搞些事情时,通常是将其再次封装成新的函数,如:
这种方式的优点显而易见,即代码结构清晰,易于维护及扩展。但其缺点也较明显,代码可复用性较低。譬如当需要在另一个函数执行前后同样记录“开始”、“结束”时,就需要再封装出一个相同的函数,如:
还有一种场景,若需要在函数 fooWrapper 执行前/后再执行其他操作,则需要修改 fooWrapper 或 再次嵌套 fooWrapper。
那么,有没有一种方式可以将其抽象并可以根据需要指定相应的wrapper呢?答案就是 compose,一个基于洋葱模型的中间件实现方式。
洋葱模型#

redux 中有个compose 函数,如下所示:
该函数的主要作用就是遍历所有的中间件函数 funcs后,将其聚合并返回一个聚合函数。
tip
注意:执行 compose 后最终返回的是一个函数,只有运行其返回的函数才能让传入 compose 的函数依次执行,即 compose(fn1, fn2, fn3)()。其中,传入的中间件函数的执行顺序是从右到左依次执行的,前一个函数的执行结果作为后一个函数的参数,因此,中间件函数的入参需与返回值的类型保持一致。
接下来,举个🌰详细说明基于洋葱模型的中间件机制。代码如下:
步骤1:结合上文中
compose实现方式,分析其执行过程(可参考文档 MDN | Array.prototype.reduce 了解 reduce 函数),因为传入fn1、fn2和fn3三个参数,因此reduce函数会经历两轮循环,如下所示:循环 a值 b值 返回值 第一轮循环 fn1 fn2 (...args) => fn1(fn2(...args)) 第二轮循环 (...args) => fn1(fn2(...args)) fn3 (...args) => fn1(fn2(fn3(...args))) 故经
compose处理之后,最终返回的值是(...args) => fn1(fn2(fn3(...args))),即:步骤2:给组合函数
fn传入需要被增强的函数dispatch,依次执行函数fn3、fn2和fn1,此时打印出:执行完毕,返回增强版
_dispatch为:其中,next 为:
其中,next 为:
其中,next 为:
步骤3:给增强后的
_dispatch函数传入参数并执行,其结果如下:
redux middleware#
redux 是个 javascript 状态管理工具,其提供了函数 applyMiddleware,以利用中间件机制实现定制化的增强 dispatch 函数。
middleware 功能#
redux middleware 可以在 Action 被指派到 Reducer 前进行额外的处理。

middleware 格式#
首先,middleware 是在 action 被 dispatch 前/后对其进行处理,因此,middleware 是一个入参为 action 的函数:
其次,为了能够串联多个 middleware ,middleware 应该接收一个 dispatch 并返回一个执行 dispatch 的函数:
最后,为了取得当前的 state 或者重新 dispatch 一个 action,middleware 需要传入参数 store,因此需将其再包裹一层:
用 ES6 箭头函数改写,并将函数及参数重命名后,得:
举个例子,当我们需要实现一个用于记录 action 及其产生的新的 state 的中间件,则可以写为:
redux applyMiddleware#
applyMiddleware 是一个 store enhancer,用来包装 redux store,让 redux store 拥有 middleware 的功能。其源码如下:
从源码中可以看出,applyMiddleware 的主要流程为3步:
- 通过 createStore 创建一个新的 store;
- 将所有的
middleware函数串联在一起,并在最后串联上store.dispatch; - 将
redux store的dispatch函数改成middleware chain。
第一步#
回顾 createStore 函数,其接收参数 enhancer 函数作为增强器,即:
增强器的作用是用于改造 Store 的相关 API,因此需要传入 createStore 以初始化 Store。所以, applyMiddleware 的第一步即为接收 createStore 函数,并初始化 Store。
第二步#
为了避免 middleware 在建立过程中调用 store.dispatch 导致串联中间件出错,applyMiddleware 会通过 map 将 getState 和改造后 dispatch 传递给每一个中间件。
此时,chain 是一组形如 next => action => {...} 的函数数组,即中间件数组:

使用 compose 将每个 middleware 中的 next 都指向下一个 middleware。

最后,将 store.dispatch 传入经 compose 处理后的函数,即可完成整个 middleware chain 的建立。

note
问:middlewareAPI 中定义的 dispatch 为什么要抛出异常?
答:applyMiddleware 的目的是将所有的中间件组装成一个超级 dispatch 并让其替换原始 dispatch,因此,需要避免在组装过程中使用 dispatch。需要注意的是,此处是通过 let 进行定义的 dispatch,故可在组装完成后修改其引用地址,保证需要使用 dispatch 时使用的是组装后的超级 dispatch。
note
问:middlewareAPI 中的 dispatch 为什么要用匿名函数包裹呢?
答:我们用 applyMiddleware 是为了改造 dispatch 的,所以 applyMiddleware 执行完后,dispatch 是变化了的,而 middlewareAPI 是 applyMiddleware 执行中分发到各个 middleware,所以必须用匿名函数包裹 dispatch, 这样只要 dispatch 更新了, middlewareAPI 中的 dispatch 应用也会发生变化。
第三步#
applyMiddleware 的最后一步就是将修改(一系列 middleware 包装)后的 dispatch 传到 redux 中,替换原有的 dispatch。
redux-thunk#
核心思想#
Redux-thunk 代码量极少,只有短短十几行,如下:
其核心代码就两行,即判断每个经过它的action:如果是function类型,就调用这个function(并传入 dispatch 和 getState 及 extraArgument 为参数),而不是任由让它到达 reducer,因为 reducer 是个纯函数,Redux 规定到达 reducer 的 action 必须是一个 plain object 类型。
note
当 action 是个函数时,则会执行这个函数并返回,此处并没有通过 next 将中间件执行至下一个,而是在函数 action 内调用 dispatch。那么,在 middleware 中调用 dispatch 会发生什么?
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),则会形成无限循环。
使用方法#
举个例子,需要发送一个异步请求到服务器获取数据,成功后弹出一个自定义的 Message。
- 定义全局的 store,并使用
redux-thunk等中间件。
- 异步 action
此时,只要在业务代码里面调用 store.dispatch(getThenShow),redux-thunk 就会拦截并执行 getThenShow 这个 action,getThenShow 会先请求数据,如果成功,dispatch 一个显示 Message 的 action,否则 dispatch 一个请求失败的 action。这里的 dispatch 就是通过 redux-thunk middleware 传递进来的。