Description
applyMiddleware
之前解释过 applyMiddleware 是中间件模块,什么是中间件呢?在 Express 框架中,
middleware 是指在框架接受请求到产生响应过程中的代码,可以完成记录日志,内容压缩,添加 CORS headers 等工作,就这类框架而言,其实这一词汇的含义等价于“插件”(plugin)——用于扩展功能的可拆装模块。middleware 最优秀的特性就是可以被链式组合
Redux 的中间件是如何和 Redux 主流程结合的
Redux 中间件将会在 action 被分发之后、到达 reducer 之前执行,并且支持链式调用,按引用顺序依次执行。如下图所示:
在上一篇 createStore 源码分析中,不难看出一个规律,Redux 源码中只有同步操作,那如果想在 Redux 中引入异步数据流,这时候就要使用 Redux 中间件来增强 createStore了。最经典的异步中间件莫过于 redux-thunk 了 ,以其为例我们可以更好的了解中间件工作原理。
先回顾一下 redux-thunk 的基本使用,如下:
import axios from 'axios'
// 引入 createStore 和 applyMiddleware
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers';
// 创建一个有 thunk 中间件加持的 store 对象
const store = createStore(
reducer,
applyMiddleware(thunk)
);
// 用于发起付款请求,并处理请求结果。由于涉及资金,我们希望感知请求的发送和响应的返回
// 入参是付款相关的信息(包括用户账密、金额等)
// 注意 payMoney 的返回值仍然是一个函数
const payMoney = (payInfo) => (dispatch) => {
fetch().then(res => { dispatch()})
return axios.post('/api/payMoney', {
payInfo
})
.then(function (response) {
console.log(response);
// 付款成功信号
dispatch({ type: 'paySuccess' })
})
.catch(function (error) {
console.log(error);
// 付款失败信号
dispatch({ type: 'payError' })
});
}
// 支付信息,入参
const payInfo = {
userName: xxx,
password: xxx,
count: xxx,
......
}
// dispatch 一个 action,注意这个 action 是一个函数
store.dispatch(payMoney(payInfo));
再看看 redux-thunk 的源码,非常简洁:
// createThunkMiddleware 用于创建 thunk 中间件
function createThunkMiddleware(extraArgument) {
// 返回值是一个 thunk,它是一个函数
return ({ dispatch, getState }) => (next) => (action) => {
// thunk 若感知到 action 是一个函数,就会执行 action
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
// 若 action 不是一个函数,则不处理,直接放过
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
redux-thunk 做的主要操作就是拦截 action,若 action 是一个函数就会被执行并且返回执行结果,若 action 不是函数就会跳过不做处理。
redux-thunk 带来的改变非常好理解,它允许我们以函数的形式派发一个 action
但是上一篇中我们提过,dispatch 的入参有严格的参数限制,只能是一个对象。为什么启用中间件模块之后会允许以函数的形式派发 action,这就是 applyMiddleware 所做的工作,改写了 dispatch 方法。
Redux 中间件的实现原理
说了这么多,终于到 applyMiddleware 中间件模块了,先来看看源码吧
// applyMiddlerware 会使用“...”运算符将入参收敛为一个数组
export default function applyMiddleware(...middlewares) {
// 它返回的是一个接收 createStore 为入参的函数
return createStore => (...args) => {
// 首先调用 createStore,创建一个 store
const store = createStore(...args)
// 一个临时的dispatch,作用是在dispatch改造完成前调用dispatch只会打印错误信息
let dispatch = () => {
throw new Error(
`Dispatching while constructing your middleware is not allowed. ` +
`Other middleware would not be applied to this dispatch.`
)
}
// middlewareAPI 是中间件的入参
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// 遍历中间件数组,调用每个中间件,并且传入 middlewareAPI 作为入参,得到目标函数数组 chain
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 改写原有的 dispatch:将 chain 中的函数按照顺序“组合”起来,调用最终组合出来的函数,传入 dispatch 作为入参
dispatch = compose(...chain)(store.dispatch)
// 返回一个新的 store 对象,这个 store 对象的 dispatch 已经被改写过了
return {
...store,
dispatch
}
}
}
我们一步一步来解析源码
1. 首先,applyMiddleware 返回的是一个接收 createStore 为入参的函数,这个函数作为 enhancer 传入 createStore 方法。
回顾一下上一篇的源码
// 当 enhancer 不为空时,便会将原来的 createStore 作为参数传入到 enhancer 中
if (typeof enhancer !== 'undefined') {
return enhancer(createStore)(reducer, preloadedState);
}
从上面代码可以看出,若 enhancer 存在,那么 createStore 内部就会直接 return 一个针对 enhancer 的调用。在这个调用中,第一层入参是 createStore,对应 createStore 函数本身。第二层入参是 reducer 和 preloadedState,对应 applyMiddleware 中的 args 入参。
enhancer 的意思就是增强器,增强的正是 createStore 的能力,所以传入 createStore 及其入参是很有必要的。
2. 改写 dispatch 函数
// middlewareAPI 是中间件的入参
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// 遍历中间件数组,调用每个中间件,并且传入 middlewareAPI 作为入参,得到目标函数数组 chain
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 改写原有的 dispatch:将 chain 中的函数按照顺序“组合”起来,调用最终组合出来的函数,传入 dispatch 作为入参
dispatch = compose(...chain)(store.dispatch)
上面的代码片段做了两件事:
- 以 middlewareAPI 作为入参,逐个调用传入的中间件,获取一个由“内层函数”组成的数组 chain
- 调用 compose 函数,将 chain 中的“内层函数”逐个组合起来,并调用最终组合出来的函数
按照约定,所有的 Redux 中间件都必须是高阶函数。在高阶函数中,一般习惯把原函数称为外层函数,将 return 出来的函数称为内层函数,以 thunk 源码为例
({ dispatch, getState }) => (next) => (action) => {
// thunk 若感知到 action 是一个函数,就会执行 action
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
// 若 action 不是一个函数,则不处理,直接放过
return next(action);
};
外层函数的作用是获取 dispatch 和 getState 两个 API,真正的中间件逻辑是在内层函数中实现的,等到 middlewares.map(middleware => middleware(middlewareAPI)) 执行完毕,所有的内层函数被提取到 chain 数组。
// 此时chain等于:
chain = [
next => action => {}, //包含getState、dispatch函数的闭包,假定此函数为f1
next => action => {}, //包含getState、dispatch函数的闭包,假定此函数为f2
...
next => action => {}, //包含getState、dispatch函数的闭包,假定此函数为fn
]
提取出所有中间件逻辑之后,调用 compose 函数组合所有中间件。然后传入原生 dispatch 执行该函数,返回值就是被改写的 dispatch。这个 compose 函数之前提到过,是一个工具函数用来进行函数合成,并不是 Redux 的专利,我们来看下源码:
// compose 会首先利用“...”运算符将入参收敛为数组格式
export default function compose(...funcs) {
// 处理数组为空的边界情况
if (funcs.length === 0) {
return arg => arg
}
// 若只有一个函数,也就谈不上组合,直接返回
if (funcs.length === 1) {
return funcs[0]
}
// 若有多个函数,那么调用 reduce 方法来实现函数的组合
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
举个例子,如下 compose 调用
compose([f1, f2, f3, f4])
会把函数组合成
(...args) => f1(f2(f3(f4(...args))))
如此一来多个中间件的逻辑就会被聚合到一个函数上去,如果被调用时中间件会按顺序依次被执行。
dispatch = compose(...chain)(store.dispatch)
//等价于:
dispatch = f1(f2(...(fn(store.dispatch))))
在applyMiddleware方法中,我们传入的「参数」是原始的dispatch方法,dispatch 函数被一层层的改造,返回的「结果」是改造后的dispatch方法。
我们再结合 redux-thunk 的源码去理解,并且小小的优化源码结构,使其看上去更直观易懂,这样dispatch 的改造过程就一目了然了。
function createThunkMiddleware(extraArgument) {
return function({ dispatch, getState }) { // 这是「中间件函数」
//参数是store中的dispatch和getState方法
return function(next) { // 这是中间件函数创建的「改造函数」
//参数next是被当前中间件改造前的dispatch
//因为在被当前中间件改造之前,可能已经被其他中间件改造过了,所以不妨叫next
return function(action) { // 这是改造函数「改造后的dispatch方法」
if (typeof action === 'function') {
//如果action是一个函数,就调用这个函数,并传入参数给函数使用
return action(dispatch, getState, extraArgument);
}
//否则调用用改造前的dispatch方法
return next(action);
}
}
}
}
这差不多就是 Redux 的核心源码了,可以看出格外简洁,但其中的设计思想值得我们深入学习与理解。