前面我们通过五个专栏介绍了 Redux 的基本用法:用户通过调用 dispatch 发射一个 action 到 reducer,然后 reducer 接受 action 和当前的 state 计算出新的 state,由于 View 视图层都会订阅 state 数据状态的变化,所以当 reducer 产生了新的 state 时,就会引发 View 视图层重新渲染。
不知道大家有没有发现,这一整套流程下来,都是同步的操作,也就是说 action 发出后,reducer 会根据 action 立刻算出新的 state,没有任何异步操作。那么关于异步操作,Redux 又是怎么进行处理的呢?
Redux 实际上并没有提供处理异步的具体方法,而是向外暴露了 middleware 的集成方式,将具体的异步处理丢给社区去实现。
middleware概念
在说 middleware 之前,你可以根据我们之前说到的知识点想一想,如果让你设计异步处理,你会放在哪一步进行处理:
1、View:视图层肯定不是明智之举,视图层 View 是 State 数据的具体展现形式,是一一对象的,不可能承担数据异步处理的能力,不然 Redux 的使用就没有必要了,排除!
2、Action 或者 ActionCreator:我们之前一直强调 Action 只是一个普通的 JavaScript 对象,用来描述一个动作,一个 reducer 修改 State 的动作,而为了代码复用,我们会抽取出 ActionCreator 方法,所以也不应该承担额外的职责,排除!
3、Reducer:额!在 Redux 中,Reducer 被定义为纯函数,只承担计算 State 的功能,不合适承担其他功能,也承担不了,因为理论上,纯函数不能进行读写操作!
经过一系列的分析,视图层 View、Action 或者 ActionCreator 和 Reducer 都不适合处理异步操作,那就只能在发送 Action 的时候进行处理了。我们通过调用 dispatch 发送一个 action,发送 action 后,Reducer 就会根据 action 计算出 State。我们可以通过改造 dispatch,添加一些逻辑处理。比如,我们可以在 dispatch 中添加输出日志:
const next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action)
next(action)
console.log('next state', store.getState())
}
上面的示例代码将原本的 store.dispatch 缓存在 next 变量中,将 store.dispatch 指向重新定义的 dispatchAndLog 函数,函数会在真正调用原本的 store.dispatch 逻辑的前后输出相关的日志。这就是 middleware 的雏形了。middleware 就是一个函数,对 store.dispatch 方法进行了改造,在发出 Action 和执行 Reducer 这两步之间添加了一些其他功能。
middleware 使用
关于常用功能的 middleware,社区中有太多的实现了,如果我们在上面实现的日志 middleware 就有现成的 redux-logger 模块。基本使用如下:
import { applyMiddleware, createStore } from 'redux'
import createLogger from 'redux-logger'
const logger = createLogger()
const store = createStore(
reducer,
applyMiddleware(logger)
)
在上面的示例代码中,redux-logger 会提供一个生成器 createLogger,用来生成日志中间件 logger。然后,将它传入 applyMiddleware 方法之中,最后将 reducer 和 applyMiddleware(logger) 传入 createStore 方法,就完成了 store.dispatch() 的功能增强。
这里有几点需要特别注意:
- 1、createStore 可以接受整个应用的初始化数据状态作为参数,这时 createStore 就会接受三个参数,applyMiddleware 就会作为第三个参数进行传入了;
const store = createStore(
reducer,
initial_state,
applyMiddleware(logger)
)
- 2、当向 applyMiddleware 方法传入多个 middleware 时,需要注意传入的 middleware 的顺序,比如说这里的 logger 中间件需要放在所有 middleware 的最后,否则输出的结果就不正确。
const store = createStore(
reducer,
applyMiddleware(thunk, promise, logger)
)
middleware 机制
为了弄清楚 Redux middleware 的机制,我们先来看一下 applyMiddleware 方法做了什么吧:
import compose from './compose'
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)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
applyMiddleware 方法会接受多个 middleware 作为参数,返回两个匿名函数嵌套执行的结果。在最里层的函数中,构造了 store.getState 和 dispatch 组成的对象 middlewareAPI,然后遍历传入的所有 middleware,依次向每个 middleware 方法传入构造好的 middlewareAPI 执行 middleware,获取所有 middleware 执行后得到的结果数组,接着通过 compose 方法组合所有 middleware 执行后得到的结果数组,最后返回最新的 store 和 dispatch 合并后的对象。
说了 applyMiddleware 方法的逻辑,其中涉及到 compose 多个 middleware 执行结果的组合方法,再看看它的源代码又有什么逻辑:
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
compose 方法的代码就是 Redux middleWare 核心之所在了,它会对接受参数,也就是 middleWare 根据 middlewareAPI 执行后的结果个数进行判断,如果参数长度为 0,就说明没有有效的 middleWare,直接返回一个返回接受参数的函数,即 dispatch => dispatch;如果参数长度为 1,就说明只有一个有效的 middleWare,直接返回这个有效的 middleWare 执行结果就好了;如果参数长度大于 1,就通过 reduce 进行合并,比如 compose(f, g, h) 就会转化成 (…args) => f(g(h(…args)))。
说了这么多关于源码的东西,大家可能跟我一样,是一脸懵逼的:说了这么多源码,那到底怎么用呢?
还记得我们在上一专栏完成的 Redux-Case 吗?为了弄明白 middleware 的机制,我们就在 Case 上面实现一个简单的日志中间件:在 src 目录下新建 middleware 文件夹,然后在该文件夹下新建 log.js 文件,并写入如下代码:
export default store => next => action => {
console.log(`dispatch: ${action.type}`)
next(action)
console.log(`finish: ${action.type}`)
}
接下来修改 src/store/index.js 文件如下:
import { createStore, applyMiddleware } from 'redux'
import reducers from '../reducers'
+ import log from '../middleware/log'
- export default createStore(reducers)
+ export default createStore(reducers, applyMiddleware(log))
最后在项目的根目录下运行 npn start,等服务跑起来后就可以测试页面了,打开浏览器控制台。每当执行一个修改 State 数据状态的操作,你都会发现浏览器控制台会打印出类似下面的日志:
dispatch: ADD_TODO
finish: ADD_TODO
dispatch: REMOVE_TODO
finish: REMOVE_TODO
接下来,我们将分如下几个步骤详细的分析一下 middleware 的运行原理。
1、函数式编程设计
在上面实现的日志中间件源码中,有一个很显著的特点:就是 middleware 的设计使用了三层匿名函数嵌套,这就是函数式编程中的 currying(柯里化)。currying 崇尚使用单参数函数实现多参数函数的功能。在对日志中间件定义完成后,我们会将 middleware 传入 applyMiddleware,applyMiddleware 会对 middleware 进行层层调用,并动态的为参数 store 和 next 参数赋值。
利用 currying 的结构设计 middleware 会有如下几点好处:
易串联:currying 函数具有延迟执行的特性,再加上 currying 充分利用了闭包的特性可以实现参数的累积。通过 currying 形式设计的 middleware 配合上组合(compose)很容易形成 pipeline 的数据流处理方式。
共享数据:在 applyMiddleware 执行的过程中,store 还是旧的,但是因为 currying 具有函数闭包的特性,在 applyMiddleware 函数执行完成后,所有的 middleware 内部都可以拿到最新并且相同的 store 了。
通过源码,我们不难发现 applyMiddleware 函数也是通过 currying 的形式进行构造的,同样借助 compose,applyMiddleware 函数也可以结合其他插件来增强 createStore 函数的功能,使用场景就是加入 Redux DevTool:
import { createStore, applyMiddleware, compose } from 'redux'
const finalCreateStore = compose(
applyMiddleware(m1, m2, m3),
// 集成 Redux DevTool
DevTools.instrument()
)(createStore)
2、分发 store 到中间件
我们构建好 middleware 后,会将 middleware 依次传入 applyMiddleware 函数。applyMiddleware 函数逻辑会通过 currying 的形式,根据传入的参数计算出新的 store,形如:
const newStore = applyMiddleware(...middlewares)(createStore)(reducer, preloadedState)
这一点,只要你认真阅读过源代码就能够很轻易的看出来。applyMiddleware 函数会依次接受三组参数的调用:第一组参数是多个 middleware 组成的数组 [mid1, mid2, mid3, …];第二组参数是 Redux 原生的 createStore 函数,因为最后无论怎样都会调用原生的 createStore 方法创建 store;第三组参数是调用 Redux 原生 createStore 方法所传递的参数,这些参数可以包括 reducer、preloadedState、enhancer。这样一来 applyMiddleware 函数的经过三次调用后,最终会通过调用 createStore 方法创建一个 store。而创建的 store 中包含的 getStore 和 dispatch 方法又分别被直接和间接地赋值给 middlewareAPI 对象的属性了:
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
紧接着,就会让每个 middleware 带着 middlewareAPI 这个参数分别执行一遍。等全部执行完后,得到的 chain 数组 [f1, f2, …., fn] 就是每个 middleware 第二个箭头函数返回的匿名函数了,因为函数闭包的特性,每个匿名函数都可以访问到相同的 store,即 middlewareAPI。
3、compose
在前面的内容中,我们已经提到 compose 方法会在两个地方被用到:一是在 applyMiddleware 方法中被调用,用来合并所有 middlewares 接受 middlewareAPI 传参调用后组成的匿名函数数组,并生成新的 dispatch:
dispatch = compose(...chain)(store.dispatch)
另外一个是作为方法暴露给用户,通过 applyMiddleware 函数结合其他插件来增强 createStore 函数的功能,如上面第 1 点的讲解。
而在 compose 函数内部实现如下:
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
通过源码实现可以看出,compose 函数始终都会返回一个匿名函数,不管 funcs 长度是多少,在 funcs 数组长度大于 1 时,会使用 reduce 函数一次从左端取一个函数 fx 拿出来执行,fx 函数执行的参数就是 fx+1 函数执行的结果,而最后一次执行 fn(n 代表 chain 的长度)函数时的参数 args 就是 store.dispatch 或者更多符合调用 createStore 函数时传递的参数。所以最终 compose 执行完后,得到的 dispatch 是这样的函数,假设 n = 3:
dispatch = f1(f2(f3()))
这就是 middleware 的实际作用,增强或者改写了 dispatch,然后当新的 dispatch 被调用时,其中嵌套包含的每个 middleware 就会依次执行了。
4、在中间件中的 dispatch
经过 compose 函数组合后,所有的 middleware 都被串联起来了。可是还有一个问题,在分发 store 时,我们有提到过每个 middleware 都可以访问到 store,即 middlewareAPI 这个变量,而 store 变量里面是包含有 dispatch 属性的。那么,在 middleware 中调用 store.dispatch() 会发生什么呢?或者和调用 next() 有什么区别?下面我们就来说明这两者的不同:
export default store => next => action => {
console.log(`dispatch: ${action.type}`)
next(action)
console.log(`finish: ${action.type}`)
}
export default store => next => action => {
console.log(`dispatch: ${action.type}`)
store.dispatch(action)
console.log(`finish: ${action.type}`)
}
每个 middleware 中 store 的 dispatch 通过匿名函数的方式和最终 compose 结束后产生的新的 dispatch 保持一致,这一点我们在之前的 compose 也解释过,所以,在 middleware 中调用 store.dispatch() 和在其他任何地方调用效果一样。而在 middleware 中调用 next() 时,会使执行进程进入到下一个 middleware。
如上图所示,左边这张图展示的是,当在 middleware 中通过调用 next 方法分发 action 时,执行方式会向内层的 middleware 依次处理并传递 action,直至 Redux 原生的 dispatch。但是如果某个 middleware 通过 store.dispatch 方法来分发 action,就会出现右侧图片所示的情况,这时 middleware 的调用过程会重新来一遍。假如这个 middleware 一直简单粗暴地调用 store.dispatch 方法来分发 action,就会导致无限循环。这样一来 store.dispatch 方法应该用在什么场景呢?
假如我们要发送一个异步请求到服务端请求数据,成功后需要弹出一个自定义 message,这里我们会用到 Redux Thunk:
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_SUCCESS_MESSAGE',
message: response.json()
})
}).catch(() => {
dispatch({
type: 'SHOW_FAIL_MESSAGE',
message: 'error'
})
})
}
这个时候只要在应用中调用 store.dispatch(getThenShow),Redux Thunk 就会执行 getThenShow 方法。getThenShow 方法逻辑会先请求数据,如果成功就会分发显示成功 message 的 action;否则,就会分发一个显示失败 message 的 action。而这里的 dispatch 就是通过 Redux Thunk middleware 传递进来的。
依照上面的分析,在 middleware 中使用 dispatch 的场景一般是接受一个定向的 action,而这个 action 并不希望到达原生的分发的 action,往往用在异步请求的需求中。
总结
middleware 是 Redux 另一个核心,它非常巧妙的利用函数 currying 的形式将接受多个参数的函数拆分成接受一个参数的多个函数,并很好的运用的函数闭包的特性,使数据状态做到了共享。通过非常简短的代码实现了极其强大的 middleware 体系。本专栏只是讲到了 middleware 的由来,基本使用以及运行机制、处理方式,但是它到底是怎么处理异步数据流,我们将会在下一个专栏进行讲解!
问答
1、middlewareAPI 中的 dispatch 为什么要用匿名函数进行包裹?
我们调用 applyMiddleware 方法的目的是改造 dispatch,所以在 applyMiddleware 方法执行完后,dispatch 是变化的,而 middlewareAPI 是 applyMiddleware 执行过程中分发到各个 middleware 的,所以必须使用匿名函数进行包裹,因为只有这样 disptach 更新了,middlewareAPI 中的 disptach 也就会相应的更新(它们是同一个变量)。
2、为什么在 middleware 中执行 store.dispatch 会导致 middleware 的调用过程会重新来一遍?
还记得我们之前分析的 middleware 里面的 store.dispatch 是指的什么吗?就是 middlewareAPI 里面的 dispatch 函数,这个 dispatch 和最终通过 compose 结束后新创建的 disptach 保持一致,而这个新的 dispatch 就是通过 compose 后所有 middleware 的组合函数,所以执行这个 disptach 会导致所有 middleware 组合函数重头开始执行。
applyMiddleware(fm, gm, hm) ==>
compose(f, g, h) ==>
dispatch = (...args) => f(g(h(...args)))