Redux 是 React 生态中最出名的状态管理库,它的中间件(middleware)机制让 redux 更加灵活,衍生出诸如 redux-thunk , redux-promise, redux-saga 等 middleware。
Redux 的中间件机制的基本逻辑如下:
- 构建 redux store 时,调用 applyMiddleware 函数将 middlewares 插入整个 action 流程中
- 每个 middleware 都会参与到 action 的 dispatch 整个流程
- 每个 middleware 都需要自行判读要不要处理当前的 action ,例如 thunk 只处理 函数类型的 action, redux-promise 只处理 promise 类型的 action
- 如果某个 middleware 不处理当前action,就转手交给下个 middleware
- 最后一个 middleware 如果不处理当前action,就转手交给 redux 内置的 store.dispatch 方法,进行 redux 标准的 action 处理
这个机制不难理解,但是不少开发者在阅读相关源码时遇到不小困难。
本文将一一拆解 middleware 相关源码,尽力让读者承受最少的心智负担读懂它。
applyMiddleware.ts
https:///reduxjs/redux/blob/176e66adc9a90df2690620075e82dca21cc1cd25/src/applyMiddleware.ts#L60
这是 redux middleware 的主体的全部代码,除去 TypeScript 相关的类型声明,其实也就寥寥几行
export default function applyMiddleware( ...middlewares: Middleware[] ): StoreEnhancer<any> { return (createStore: StoreEnhancerStoreCreator) => <S, A extends AnyAction>( reducer: Reducer<S, A>, preloadedState?: PreloadedState<S> ) => { const store = createStore(reducer, preloadedState) let dispatch: Dispatch = () => { throw new Error( 'Dispatching while constructing your middleware is not allowed. ' + 'Other middleware would not be applied to this dispatch.' ) } const middlewareAPI: MiddlewareAPI = { getState: store.getState, dispatch: (action, ...args) => dispatch(action, ...args) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose<typeof dispatch>(...chain)(store.dispatch) return { ...store, dispatch } } }
为了方便还没学过 TypeScript 的读者,同时尽量让核心代码更显眼,我把上面的代码转换成 JavaScript 形式
function applyMiddleware(...middlewares) { return (createStore) => (reducer, preloadedState) => { const store = createStore(reducer, preloadedState) 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: (action, ...args) => dispatch(action, ...args) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } } }
正式开始解析代码
首先这明显是个高阶函数,因为它的返回值还是个函数。
在函数主体中看到了一个临时变量 dispatch 被赋值了2次 ,十分有意思。
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: (action, ...args) => dispatch(action, ...args) // 使用 } const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) // 第二次赋值
从第二到四行 Error 构造器中消息的内容,可以看出这段代码希望在 middleware 链被构建的时候,dispatch 不应该被调用,否则抛错。
但奇怪的是,这个临时变量 dispatch 紧接着在 middlewareAPI 的定义中就被使用了 —— 为什么在 redux 应用正常使用时不会抛错呢?
原因就在于而第二次对临时变量 dispatch的赋值 !
让我们写一点模拟代码来尝试理解这边的奥秘
let dispatch = () => { console.log( 'Dispatching while constructing your middleware is not allowed. ' + 'Other middleware would not be applied to this dispatch.' ) }; const obj = { name: 'dispatch', dispatch: dispatch } obj.dispatch(); dispatch = () => { console.log("dispatch function is ready"); } obj.dispatch();
运行上述代码后,发现打印出
Dispatching while constructing your middleware is not allowed. Other middleware would not be applied to this dispatch. Dispatching while constructing your middleware is not allowed. Other middleware would not be applied to this dispatch.
两行都是错误信息!!!翻车!我大意了,没有闪!
为什么跟预期的不一样?
const obj = { name: 'dispatch', dispatch: dispatch } // 由于dispatch在前面已经定义好了,obj的定义相当于 const obj = { name: 'dispatch', dispatch: () => { console.log( 'Dispatching while constructing your middleware is not allowed. ' + 'Other middleware would not be applied to this dispatch.' ) } }
仔细一看,我们模拟的 obj 跟 上面 变量 middlewareAPI 的区别在于 变量 middlewareAPI的 dispatch 属性是个箭头函数。于是我们修改 obj 的模拟让它更接近 变量 middlewareAPI
let dispatch = () => { console.log( 'Dispatching while constructing your middleware is not allowed. ' + 'Other middleware would not be applied to this dispatch.' ) }; const obj = { name: 'dispatch', dispatch: (...arg) => dispatch(...arg) } obj.dispatch(); dispatch = () => { console.log("dispatch function is ready"); } obj.dispatch();
再次运行后,打印出2行不同的消息
Dispatching while constructing your middleware is not allowed. Other middleware would not be applied to this dispatch. dispatch function is ready
解读:由于 obj (以及 变量 middlewareAPI) 的 dispatch 属性是个函数,所以在运行时才会去查找并调用 变量 dispatch ,因此第二次赋值生效了。简言之,这里就是个闭包场景。
compose.ts
继续解读源码
在第二次对变量dispatch赋值时,使用了一个 compose 函数,一起看看它的定义
https:///reduxjs/redux/blob/master/src/compose.ts
(同理,我把源码转化成了JavaScript)
function compose(...funcs) { if (funcs.length === 0) { // infer the argument type so it is usable in inference down the line return arg => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (...args) => a(b(...args))) }
最难读懂的明显是最后这行。(首先吐槽的是参数 a,b 命名太草率了吧)
复习一下 Array.prototype.reduce() 函数的定义
reduce() 方法对数组中的每个元素执行一个由参数提供的reducer函数(升序执行),将其结果汇总为单个返回值
arr.reduce(callback( accumulator, currentValue[, index[, array]] ) {
// return result from executing something for accumulator or currentValue
}[, initialValue]);
简单测试代码
const arr = [1, 2, 3, 4, 5]; const sum = arr.reduce(((accumulator, currentValue) => { console.log(accumulator); return accumulator + currentValue; // 返回值将作为下次遍历的 accumulator })); console.log(sum);
运行上面代码,打印出
accumulator 1 ; currentValue 2 accumulator 3 ; currentValue 3 accumulator 6 ; currentValue 4 accumulator 10 ; currentValue 5 sum 15
reduce 其实有第二个可选参数(initialValue)作accumulator的初始值,如果没有该参数,数组的第一个元素就会作为accumulator的初始值, 而第一次遍历中的 currentValue 实际上是数组的第二个元素 —— 这就是上面代码输出中 第一行是 accumulator 1 ; currentValue 2 的原因。
(compose.ts 中 reduce方法也没有设置第二个参数,因此这种用法是本文的重点关注)
一个小点:reduce方法的返回值其实就是最后一个 accumulator
简单的复习结束。
此外,箭头函数的主体如果不带大括号,主体表达式的返回值默认就是箭头函数的返回值 —— 这简化了代码,但是对不熟悉箭头函数的读者而言可读性更差了。
因此,让我们尝试”重构“ compose.ts 代码,让它看起来更友善可读一些
function compose(...funcs) { // ... return funcs.reduce((accumulator, currentFunc) => { return (...args) => { return accumulator(currentFunc(...args)) } }) }
这边嵌套了两三层函数,看起来仍然有点头疼,让我们尝试阐述这几行代码:
- 遍历 compose 参数传入的函数数组
- reduce 的参数是个回调函数,在这个回调函数中即将返回一个新的函数 作为下一次遍历的 accumulator
- 这个新函数中先调用当前遍历的函数 currentFunc (并原封不动传入参数),然后将返回值作为参数调用当前遍历的 accumulator
似乎还是难以看清楚具体的思路。
让我们尝试模拟调用 compose 函数
const newFunc = compose(func1, func2, func3); // 调用compose newFunc(someArg); // 调用这个新函数
分析代码过程:
- 在 reduce 的第一次遍历中,accumulator 其实是 func1 , currentFunc 其实是 func2
- accumulator(currentFunc(...args)) 即先调用了 func2 得到了返回值,然后才传入 func1 作为参数并执行 —— 即最终实际的执行顺序里 func2 是先于 func1
- 同理推断出最终的执行顺序是颠倒的 即 func3, func2, func1
- newFunc 其实是 reduce 的最后一个 accumulator, 即是一个 (...args) => { // ... } 匿名箭头函数
- 调用 newFunc 时传入的参数 someArg 对应的是reduce里最后一次遍历的 (...args) => { return accumulator(currentFunc(...args)) } 的 args, 它马上会被最后一个 currentFunc 使用,在本例中是 func3
到此整个思路逐渐清晰了:
newFunc(someArg) 的实际执行情况是
- 执行 func3(someArg) 得到 val3
- 执行 func2(val3) 得到 val2
- 执行 func1(val2) 得到 val1
写一点真正的测试用例验证我们的解读
// 参数和返回值都是拥有 a,b 两个属性的对象 const func1 = ({a, b}) => { return { a: a - 1, b: a * b, } }; const func2 = ({a, b}) => { return { a: a * 3, b: a + b, } }; const newFunc = compose(func1, func2); const result = newFunc({a: 1, b: 2}); console.log(JSON.stringify(result, null, 4));
根据我们的理解以上代码应该先执行 func2({a: 1, b: 2}) 得到 {a: 3, b: 3},然后执行 func1( {a: 3, b: 3}) 得到 {a: 2, b: 9} ,读者可以运行上述代码会发现结果跟预期一致。
注意:这个例子中的 func1 和 func2 都是简单函数(非高阶),直接计算结果并返回。
compose.ts 的解读到此告一段落
返回 applyMiddleware.ts 解读剩余代码
const middlewareAPI = { getState: store.getState, dispatch: (action, ...args) => dispatch(action, ...args) } const chain = middlewares.map(middleware => middleware(middlewareAPI))
这边的代码需要配合一个真实的 redux middleware 一起解读, 那就让我们看看最负盛名的redux middleware吧 —— redux-thunk https:///reduxjs/redux-thunk/blob/master/src/index.js
function createThunkMiddleware(extraArgument) { return ({ dispatch, getState }) => (next) => (action) => { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; } const thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware; export default thunk;
相信很多读者看到三个连续的 => 会头大,让我们善意”重构“一下 —— 一个redux-thunk middleware 如下(这也是标准的redux middleware 规范)
const thunk = ({dispatch, getState}) => { // 符合规范 return (next) => { // 中间过渡 return (action) => { // 中间件核心逻辑 if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; }; };
为了方便阅读,这边对三层函数做简单定义和命名区分:
- thunk (或标准的redux middleware)等以 ({dispatch, getState}) 为参数的箭头函数,在本文命名为 conventional middleware function —— 这种格式为了符合 redux middleware 的规范
- 中间那层以 (next) 为参数的箭头函数,在本文命名为 intermediate middleware function ——这种格式作为中间过渡,方便把前后的middleware串联起来
- 最里面的以 (action)为参数的箭头函数,在本文命名为 core middleware function —— 这是包含中间件核心逻辑的部分
因此,applyMiddleware.ts 的上述代码无非是把 conventional middleware function 数组转变成 intermediate middleware function 数组(即 给 dispatch, getState 赋上值)
const middlewareAPI = { getState: store.getState, dispatch: (action, ...args) => dispatch(action, ...args) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) // chain 的元素都是下面这种形式的函数 —— intermediate middleware function (next) => { // 中间过渡 return (action) => { // 中间件核心逻辑 if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; };
理解 thunk 的核心逻辑
(action) => { // 中间件核心逻辑 if (typeof action === 'function') { // 判断 action 是否是个函数 return action(dispatch, getState, extraArgument); } return next(action); };
解读:
- 判断 action 是否是个函数类型
- 如果action是函数,那么调用这个函数
- 如果action不是函数,那么不做任何事情,调用next
next 是什么?
回答这个问题前,把前面的代码集成在一起
const middlewareAPI = { getState: store.getState, dispatch: (action, ...args) => dispatch(action, ...args) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch)
- 变量 chain 是 intermediate middleware function 数组
- compose 将 intermediate middleware function 数组使用 reduce 处理,返回最后一个 accumulator
- 给这个 accumulator 传入 store.dispatch 作为参数
模拟举例:
// 假设现有三个 intermediate middleware function // 里面的 core middleware function 都不做任何action操作 // 仅仅是输出log,方便我们理解整个流程 const intermediateFunc1 = (next) => { console.log('intermediateFunc1 run'); const coreFun1 = (action) => { console.log("coreFun1 run"); next(action); } return coreFun1; } const intermediateFunc2 = (next) => { console.log('intermediateFunc2 run'); const coreFunc2 = (action) => { console.log("coreFunc2 run"); next(action); } return coreFunc2; } const intermediateFunc3 = (next) => { console.log('intermediateFunc3 run'); const coreFunc3 = (action) => { console.log("coreFunc3 run"); next(action); } return coreFunc3; } // 模拟 store.dispatch const dispatch = (action) => { console.log('run dispatch'); } // 调用 compose console.log('start compose'); const composedFunc = compose(intermediateFunc1, intermediateFunc2, intermediateFunc3); // 传入 dispatch 得到 realDispatch console.log('pass dispatch'); const realDispatch = composedFunc(dispatch); // 调用 realDispatch 模拟 dispatch action console.log('call realDispatch'); realDispatch({name: 'testing action'});
以上的模拟代码输出了以下结果
start compose pass dispatch intermediateFunc3 run intermediateFunc2 run intermediateFunc1 run call realDispatch coreFun1 run coreFunc2 run coreFunc3 run run dispatch
发现几个重要的点:
- compose 过程中,所有 intermediate middleware function 都没有执行
- 调用 composedFunc 传入 dispatch 时,intermediate middleware function 才执行了 —— 且如预期地以倒序执行(3,2,1)
- 调用 realDispatch 时,所有 core middleware function 又以正序执行 (1,2,3)最后执行 dispatch (模拟 store.dispatch的函数)
解读:
- compose 过程只是将所有 intermediate middleware function 拼接在一起,但并未执行。且 compose 最后返回的仍然是一个函数 composedFunc —— 事实上, composedFunc 类似于下面这样一个函数
const composedFunc = (...args3) => { return ((...args2) => { return intermediateFunc1(intermediateFunc2(args2)) })(intermediateFunc3(args3)) } // 由于在 redux middleware 的场景下, ...args 参数其实就是 next, 等同于 const composedFunc = (next3) => { return ((next2) => { return intermediateFunc1(intermediateFunc2(next2)) })(intermediateFunc3(next3)) }
2. 调用 composedFunc 时,才真正如多米诺骨牌那样一个个运行了 intermediate middleware function
const realDispatch = composedFunc(dispatch);
其中 next3 就是 dispatch, 而 next2 是 intermediateFunc3(next3) 的返回值, 依次类推,得到以下表达式
intermediateFunc1(intermediateFunc2(intermediateFunc3(dispatch)))
接着尝试代入 intermediate middleware function 和 core middleware function 的实际代码(其实就是 next 参数的代入),展开式子就能得到 realDispatch 实际上如下
const realDispatch = (action) => { console.log("coreFun1 run"); ((action) => { console.log("coreFunc2 run"); ((action) => { console.log("coreFunc3 run"); dispatch(action); })(action); })(action); }
另外,读者可以尝试debug打断点,也会发现下图
因此,最后执行时 core middleware function 是以正序执行的。
Redux middleware 的相关源码到此就解读完毕了。
尽管我这两天写文章时尽力追求讲解得更简单易懂,但某种程度上感觉仍然很复杂。在分解intermediate middleware function 和 core middleware function 时,甚至唤起了高中和大学时分解数学式子的回忆。
总之,希望本文能帮助到你。欢迎点赞、喜欢、收藏!
p.s. 本文讲解的部分函数其实涉及了函数式编程的内容,如柯里化。感兴趣的小伙伴可以自行进一步搜索学习。
相关链接:
https:///reduxjs/redux-thunk/blob/master/src/index.js