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 middlewareredux
是个 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 applyMiddlewareapplyMiddleware
是一个 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
传递进来的。