之前在学习 Koa.js
当中的 中间件 部分时,曾经接触到过中间件这个概念,也了解洋葱模型这个概念,所以在今天我们就来深入的了解一下 Redux
当中与其十分类似的 middleware
什么是 Middleware
所谓中间件,就是处在服务业务与用户应用中间的软件(架构),主要用来将具体业务和底层逻辑解耦的组件,在 Node.js
当中,middleware
是 req
和 res
之间的中间层,可以用来处理很多事情,但是在 Redux
里面,middleware
又是什么呢?在 Redux
的 middleware
的文档里面有这样一句话
It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.
不难理解,在 Redux
里的 middleware
是发送 action
和 action
到达 reducer
之间的第三方扩展,也就是中间层,middleware
提供了一个分类处理 action
的机会,在 middleware
中你可以检阅每一个流过的 action
,挑选出特定类型的 action
进行相应操作,简单的来说就是,middleware
是架在 action
和 store
之间的一座桥梁
为什么要引入 Middleware
我们先来看一个简单的同步数据流的例子,如下图
当我们点击了 button
以后,在回调中 dispatch
一个 action
,然后 reducer
收到 action
后,更新 state
并通知 view
重新渲染,一个很常规的同步数据流动的场景,但是如果我们现在的需求有所变动,比如需要打印每一个 action
信息用来调试,又或者需要在点击了 button
以后进行一些别的操作等等
面对多种多样的业务需求,单纯的修改 dispatch
或 reducer
的代码显然不是很理想的选择,我们需要的是可以组合的,自由插拔的插件机制,所以,在这种情况下,我们就会用上 middleware
,如下图
每一个 middleware
处理一个相对独立的业务需求,通过串联不同的 middleware
,实现变化多样的的功能,但是在本章当中我们不会过多关注它的内部实现原理,这一部分内容我们在之前的 Redux 源码初探 章节当中已经梳理过了,所以在本章当中,我们重点关注以下几个问题
middlewares
是如何工作的?Redux
是如何让middlewares
串联起来的?- 在
middleware
中调用dispatch
会发生什么? - 我们自己的
middlewares
该怎么写?
下面我们就一个一个来看
Middlewares 是如何工作的?
在此之前,我们先简单的介绍一下箭头函数的用法,因为后面我们会经常遇到类似的写法,如下示例
1 | // 一个标准的闭包函数 |
下面我们就正式开始,我们首先先来简单的定义一个日志中间件,如下
1 | // logger |
在 Redux
当中提供了 applyMiddleware
这个 API
用来加载 middleware
,我们将其与我们上面定义的 logger
中间件放到一起来进行对比介绍,如下图所示
函数式编程思想设计 Middleware
middleware
的设计有点特殊,是一个层层包裹的匿名函数,这其实是函数式编程中的 柯里化,applyMiddleware
会对 logger
这个 middleware
进行层层调用,动态地对 store
和 next
参数赋值,这样设计的好处在于
- 易串联,柯里化函数具有延迟执行的特性,通过不断柯里化形成的
middleware
可以累积参数,配合组合compose
的方式,很容易形成pipeline
来处理数据流 - 共享
store
,在applyMiddleware
执行过程中,store
还是旧的,但是因为闭包的存在,在applyMiddleware
完成后,所有的middlewares
内部拿到的store
是最新且相同的
另外,我们可以发现 applyMiddleware
的结构也是一个多层柯里化的函数,借助 compose
,applyMiddleware
可以用来和其他插件一起加强 createStore
函数
1 | import { createStore, applyMiddleware, compose } from 'redux' |
给 Middleware 分发 Store
创建一个普通的 store
通过如下方式
1 | let newStore = applyMiddleware(mid1, mid2, mid3, ...)(createStore)(reducer, null) |
上面代码执行完后,applyMiddleware
函数陆续获得了三个参数,第一个是我们的 middlewares
数组,[mid1, mid2, mid3, ...]
,第二个 next
是 Redux
原生的 createStore
,最后一个是 reducer
我们从对比图中可以看到,applyMiddleware
利用 createStore
和 reducer
创建了一个 store
,然后 store
的 getState
方法和 dispatch
方法又分别被直接和间接地赋值给 middlewareAPI
变量,middlewareAPI
就是对比图中红色箭头所指向的函数的入参 store
1 | var middlewareAPI = { |
map
方法让每个 middleware
带着 middlewareAPI
这个参数分别执行一遍,即执行红色箭头指向的函数(简单来说就是使用 middlewareAPI
作为参数将 middleware
包装一层),在执行完后,获得 chain
数组 [f1, f2, ... , fx, ...,fn]
,它保存的对象是图中绿色箭头指向的匿名函数(中间件序列),因为闭包,所以每个匿名函数都可以访问相同的 store
,即 middlewareAPI
但是这里存在一个问题,
middlewareAPI
中的dispatch
为什么要用匿名函数包裹呢?
我们用 applyMiddleware
是为了改造 dispatch
的,所以 applyMiddleware
执行完后,dispatch
是变化了的,而 middlewareAPI
是 applyMiddleware
执行中分发到各个 middleware
,所以必须用匿名函数包裹 dispatch
(闭包机制),这样只要 dispatch
更新了,middlewareAPI
中的 dispatch
应用也会发生变化
组合串联 Middlewares
1 | dispatch = compose(...chain)(store.dispatch) |
compose
将 chain
中的所有匿名函数 [f1, f2, ... fx, ... fn]
组装成一个新的函数,即新的 dispatch
,当新 dispatch
执行时,[f1, f2, ... fx, ... fn]
从右到左依次执行(注意这里使用的是 reduceRight
,所以顺序是从右到左,与数组的 reduce
区分开来)
Redux
中 compose
的实现是下面这样的
1 | function compose(...funcs) { |
compose(...chain)
返回的是一个匿名函数,函数里的 funcs
就是 chain
数组,当调用 reduceRight
时,依次从数组的右端取一个函数 fx
拿来执行,fx
函数的参数 composed
就是前一次 fx + 1
执行的结果,而第一次执行的 fn
(n
代表 chain
的长度)的参数 arg
就是 store.dispatch
,所以当 compose
执行完后,我们得到的 dispatch
是这样的
1 | dispatch = f1(f2(f3(store.dispatch))) |
这个时候再调用新的 dispatch
,每个 middleware
的代码就会依次执行
在 Middleware 中调用 Dispatch 会发生什么?
经过 compose
之后,所有的 middleware
算是串联起来了,可是还有一个问题,在之前我们提到过每个 middleware
都可以访问 store
,即 middlewareAPI
这个变量,所以就可以拿到 store
的 dispatch
方法,那么在 middleware
中调用 store.dispatch()
会发生什么,和调用 next()
有区别吗?
1 | const logger = store => next => action => { |
在之前我们已经介绍过,通过匿名函数的方式 middleware
中拿到的 dispatch
和最终 compose
结束后的新 dispatch
是保持一致的,所以在 middleware
中调用 store.dispatch()
和在其他任何地方调用效果是一样的,而在 middleware
中调用 next()
的效果则是进入下一个 middleware
,如下图所示
在正常情况下(图左),当我们 dispatch
一个 action
时,middleware
通过 next(action)
一层一层处理和传递 action
直到 Redux
原生的 dispatch
,如果某个 middleware
使用 store.dispatch(action)
来分发 action
,就发生了右图的情况,相当于从外层重新再来一遍
在 Middleware 中调用 Dispatch 的应用场景
我们知道,如果在中间件当中不调用 next
的话,中间件就不会串起来执行的,不过有些特殊的 action
,比如异步请求的 action
,它们的目的地并不是原生的 dispatch
,而是对异步请求的 action
进行拦截,在请求完数据后利用新的 dispatch
发送更新 UI
的 action
,这个 action
就可以把所有的中间价走一遍了
我们可以来模拟一个异步请求到服务器获取数据,成功后弹出一个自定义的 Message
的中间件,我们使用 redux-thunk 这个中间件
1 | const thunk = store => next => action => |
没有看错,整个源码的核心只有这一点点,redux-thunk
做的事情就是判断 action
类型是否是函数,若是则执行 action
,若不是则继续传递 action
到下个 middleware
,我们首先来设计一个请求的 action
1 | const getThenShow = (dispatch, getState) => { |
这个时候只要在业务代码里面调用 store.dispatch(getThenShow)
,redux-thunk
就会拦截并执行 getThenShow
这个 action
,getThenShow
会先请求数据,如果成功则会 dispatch
一个显示 Message
的 action
,否则 dispatch
一个请求失败的 action
,这里的 dispatch
就是通过 redux-thunk
这个 middleware
传递进来的
我们可以简单的总结一下,在
middleware
中使用dispatch
的场景一般是,接受到一个定向action
,这个action
并不希望到达原生的dsipatch
,存在的目的是为了触发其他新的action
,往往用在异步请求的需求当中
自定义的 Middlewares 该怎么写?
实现很简单,我们先来定义一个 callTraceMiddleware
的中间件用来追踪函数的调用过程
1 | // callTraceMiddleware.js |
然后在调用中间件部分添加中间件
1 | const createStoreWithMiddleware = applyMiddleware( |
这样我们运行在浏览器窗口就可以看到打印的函数调用轨迹
总结
applyMiddleware
机制的核心在于组合 compose
,将不同的 middlewares
一层一层包裹到原生的 dispatch
之上,而为了方便进行 compose
,需对 middleware
的设计采用柯里化的方式,达到动态产生 next
方法以及保持 store
的一致性
由于在 middleware
中可以像在外部一样轻松访问到 store
,因此可以利用当前 store
的 state
来进行条件判断,用 dispatch
方法拦截老的 action
或发送新的 action