Redux Middleware 源码解析
作者从 2016 年开始接触 React+Redux,通过阅读 Redux 源码,了解了其实现原理。Redux 代码量不多,结构也很清晰,函数式编程思想贯穿着整个 Redux 源码,如纯函数,高阶函数,Curry,Compose。
本文首先会介绍函数式编程的思想,再逐步介绍 Redux 中间件的实现。看完本文,希望可以帮助你了解中间件实现的原理。
基本概念
Redux 是可预测的状态管理框架。它很好的解决多交互,多数据源的诉求。
Redux 设计理念有三个原则: 1. 单一数据源 2. State 只读 3. 使用纯函数变更 state 值。
基本概念原则解释 Store (1) 单一数据源 (2) State 只读 Store 可以看做是数据存储的一个容器。在这个容器里面,只会维护唯一的一个 State Tree。
Store 会给定4种基础操作方法:dispatch(action)、getState()、replaceReducer(nextReducer)、subscribe(listener)
根据单一数据源原则,所有数据会通过 store.getState() 方法调用获取。
根据 State 只读原则,数据变更会通过 store,dispatch(action) 方法。
Action(3) 使用纯函数变更 state 值 Action 可以理解为变更数据的信息载体。type 是变更数据的唯一标志,payload 是用来携带需要变更的数据。
格式为:const action = { type: 'xxx', payload: 'yyy' };
Reducer(3) 使用纯函数变更 state 值 Reducer 是个纯函数。负责根据获取 action.type 的内容,计算 state 数值。
reducer: prevState => action => newState。
正常的一个同步数据流为:view 层触发 actionCreator,actionCreator 通过 store.dispatch(action) 方法,变更 reducer。
但是面对多种多样的业务场景,同步数据流方式显然无法满足。对于改变 reducer 的异步数据操作,就需要用到中间件的概念。
函数式编程
函数式编程贯穿着 Redux 的核心。这里会简单介绍几个基本概念。如果你已经了解了函数式编程的核心技术,例如 高阶函数,compose, currying,递归,可以直接绕过这里。
我简单理解的函数式编程思想是: 通过函数的拆解,抽象,组合的方式去编程。复杂问题可以拆解成小粒度函数,最终利用组合函数的调用达成目的。
高阶函数
Higher order functions can take functions as parameters and return functions as return values.
接受函数作为参数传入,并能返回封装后函数。
Compose
Composes functions from right to left.
组合函数,将函数串联起来执行。就像 domino 一样,推倒第一个函数,其他函数也跟着执行。
首先我们看一个简单的例子。
// 实现公式: f(x) = (x + 100) * 2 - 100
const add = a => a + 100;
const multiple = m => m * 2;
const subtract = s => s - 100;
// 深度嵌套函数模式 deeply nested function,将所有函数串联执行起来。
subtract(multiple(add(200)));
上述例子执行结果为:500
compose 其实是通过 reduce() 方法,实现将所有函数的串联。不直接使用深度嵌套函数模式,增强了代码可读性。不要把它想的很难。
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(subtract, multiple, add)(200);
Currying
Currying is the technique of translating the evaluation of a function that takes multiple arguments into evaluating a sequence of functions, each with a single argument
翻译过来是:把接受多个参数 的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
直接撸代码解释
// 实现公式: f(x, y, z) = (x + 100) * y - z;
const fn = (x, y, z) => (x + 100) * y - z;
fn(200, 2, 100);
// Curring实现 使用一层层包裹的单参匿名函数,来实现多参数函数的方法
const fn = x => y => z => (x + 100) * y - z;
fn(200)(2)(100);
Currying 只允许接受单参数。
Redux applyMiddleware.js
Redux 中 reducer 更关注的是数据逻辑转化,所以 Redux 中间件是为了增强 dispatch 方法出现的。如我们上面图,所描述的流程。中间件调用链,会在 dispatch(action) 方法之前调用。
所以 Redux 中间件实现核心目标是:改造 dispatch 方法。
redux 对中间件的实现,代码是很精简。整体都不超过20行。
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
const store = createStore(reducer, preloadedState, enhancer)
let dispatch = store.dispatch
let chain = []
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
接下来,一步步的解析 Redux 在中间件实现的过程。
applyMiddleware.js 方法有三个主要的步骤,如下:
- 将所有的中间件组合在一起, 并保证最后一个执行的是 dispatch(action) 方法。
- 像 Koa 所有中间件对 ctx 的调用一样。保证所有的中间件都能访问到 Store。
- 最后将含有中间件调用链的新 dispatch 方法,合并到 Store 中。
- redux 对中间件的定义格式为:mid1 = store => next => action => { next(action) };
看到这里,你可能有这么几个疑问?
- 如何将所有的 middleware 串联执行在一起?并可以保证最后一个执行的是 dispatch 方法?
- 如何让所有的中间件都可以访问到 Store?
- 因为新形成的 dispatch 方法,为含有中间件调用链的方法结合。中间件如果调用 dispatch,岂不是会死循环在调用链中?
- 为什么将中间件格式定义为 mid1 = store => next => action => { next(action) } ?
为了解决这4个疑问,下面将针对相应问题,逐步解析。
中间件串联
疑问:如何将所有的 middleware 串联执行在一起?并可以保证最后一个执行的是 dispatch(action) 方法?
解决思路:
- 深度嵌套函数 / compose 组合函数方法,将所有的中间件串联起来。
- 封装最后一个函数作为 dispatch(action) 方法。
const middleware1 = action => action;
const middleware2 = action => action;
const final = action => store.dispatch(action);
/*
compose(...)将所有中间件串联
定义final作为最后执行dispatch的函数
*/
compose(final, middleware2, middleware1)(action)
中间件可访问 Store
疑问:如何让所有的中间件都可以访问到 Store?
可以参考我们对 Koa2 中间件的定义
const koaMiddleware = async(ctx, next) => {};
解决思路:
给每一个 middleware 传递 Store,保证每一个中间件访问到的都是一致的。
const middleware1 = (store, action) => action;
const middleware2 = (store, action) => action;
const final = (store, action) => store.dispatch(action);
如果我们想使用 compose 方法,将所有中间件串联起来,那就必须传递单一参数。根据上面函数式编程讲到的 currying 方法,对每个中间件柯里化处理。
// 柯里化处理参数
const middleware1 = store => action => action;
const middleware2 = store => action => action;
const final = store => action => store.dispatch(action);
// 将store保存在各个函数中 -> 循环执行处理。
const chain = [final, middleware2, middleware1].map(midItem => midItem(store));
compose(...chain)(action);
通过循环处理,将 store 内容,传递给所有中间件。这里就体现了 currying 的作用,延迟计算和参数复用。
中间件调用新 dispatch 方法死循环
疑问:因为新形成的 dispatch 方法,为含有中间件调用链的方法结合。中间件如果调用 dispatch,岂不是会死循环在调用链中?
new_dispatch = compose(...chain)(store.dispatch);
new_store = { ...store, dispatch: new_dispatch };
根据源码的解析,新和成 new_dispatch 是带有中间件调用链的新函数,并不是原来使用的 store.dispatch 方法。
如果根据上面的例子使用的方式传入 store:
const chain = [final, middleware2, middleware1].map(midItem => midItem(store));
此时保存在各个中间件中的 store.dispatch 为已组合中间件 dispatch 方法,中间件如果调用 dispatch 方法,会发生死循环在调用链中。
解决思路:给定所有中间件的 dispatch 方法为原生 store.dispatch 方法,不是新和成的 dispatch 方法。
// 这就是为什么在给所有 middleware,共享 Store 的时候,会重新定义一遍 getState 和 dispatch 方法。
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
保证中间件不断裂
疑问:为什么将中间件格式定义为 mid1 = store => next => action => { next(action) } ?
上述例子有提到每次都会返回 action 给下一个中间件,例如 const middleware1 = store => action => action,如何保证中间件不会因为没有传递 action 而断裂?
这里必须说明的是:Koa 中间件可以通过调用 await next() 方法,继续执行下一个中间件,也可以中断当前执行,比如 ctx.response.body = ‘xxxx’(直接中断下面中间件的执行)。一般情况下,Redux 不允许调用链中断,因为我们最终需要改变 state 内容。(比如 redux-thunk 使用有意截断的除外)。
解决思路:如果可以保证,上一个中间件都有下一个中间件的注册,类似Koa对下一个中间件调用方式next(),不就可以保证了中间件不会断裂。
// 柯里化处理参数
const middleware1 = store => next => action => { log(1); next(action)};
const middleware2 = store => next => action => { log(2); next(action)};
// 中间件串联
const chain = [middleware1, middleware2 ].map(midItem => midItem({
dispatch: (action) => store.dispatch(action)}));
// compose(...chain) 会形成一个调用链,next 指代下一个函数的注册,如果执行到了最后 next 就是原生的 store.dispatch 方法
dispatch = compose(...chain)(store.dispatch);
总结
Redux applyMiddleware.js 机制的核心在于,函数式编程的 compose 组合函数,需将所有的中间件串联起来。为了配合 compose 对单参函数的使用,对每个中间件采用 currying 的设计。同时利用闭包原理做到每个中间件共享 Store。
另外,Redux / React 应用函数式编程思想设计,其实是通过组合和抽象来减低软件管理复杂度。
简单写了个学习例子,参考:https://github.com/Linjiayu6/learn-redux-code
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

上一篇: 前端面试题汇总
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论