作者从2016年开始接触 React+Redux,通过阅读Redux源码,了解了其实现原理。
Redux代码量不多,结构也很清晰,函数式编程思想贯穿着整个Redux源码,如纯函数,高阶函数,Curry,Compose。
本文首先会介绍函数式编程的思想,再逐步介绍Redux中间件的实现。
看完本文,希望可以帮助你了解中间件实现的原理。
1) 基本概念
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的异步数据操作,就需要用到中间件的概念。如图所示。
2) 函数式编程
函数式编程贯穿着Redux的核心。这里会简单介绍几个基本概念。如果你已经了解了函数式编程的核心技术,例如 高阶函数,compose, currying,递归,可以直接绕过这里。
我简单理解的函数式编程思想是: 通过函数的拆解,抽象,组合的方式去编程。复杂问题可以拆解成小粒度函数,最终利用组合函数的调用达成目的。
2.1) 高阶函数
Higher order functions can take functions as parameters and return functions as return values.
接受函数作为参数传入,并能返回封装后函数。
2.2) 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);
2.3) 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只允许接受单参数。
3) 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个疑问,下面将针对相应问题,逐步解析。
3.1) 中间件串联
疑问:
- 如何将所有的middleware串联执行在一起?并可以保证最后一个执行的是dispatch(action)方法?
解决思路:
- 深度嵌套函数 / compose组合函数方法,将所有的中间件串联起来。
- 封装最后一个函数作为dispatch(action)方法。
const middleware1 = action => action;
const middleware2 = action => action;
const final = action => store.dispatch(action);
/*
1. compose(...)将所有中间件串联
2. 定义final作为最后执行dispatch的函数
*/
compose(final, middleware2, middleware1)(action)
3.2) 中间件可访问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的作用,延迟计算和参数复用。
3.3) 中间件调用新dispatch方法死循环
疑问:
- 因为新形成的dispatch方法,为含有中间件调用链的方法结合。中间件如果调用dispatch,岂不是会死循环在调用链中?
new_dispatch = compose(...chain)(store.dispatch);
new_store = { ...store, dispatch: new_dispatch };
根据源码的解析,新和成new_dispatch是带有中间件调用链的新函数,并不是原来使用的store.dispatch方法。
如果根据3.2) 例子使用的方式传入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)
3.4) 保证中间件不断裂
疑问:
- 为什么将中间件格式定义为 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);
4) 总结
Redux applyMiddleware.js机制的核心在于,函数式编程的compose组合函数,需将所有的中间件串联起来。
为了配合compose对单参函数的使用,对每个中间件采用currying的设计。同时,利用闭包原理做到每个中间件共享Store。
另外,Redux / React应用函数式编程思想设计,其实是通过组合和抽象来减低软件管理复杂度。
简单写了个学习例子 参考 https://github.com/Linjiayu6/learn-redux-code, 如果有帮助到你,点个赞 咩~
简历请投递至邮箱linjiayu@meituan.com