做者从2016年开始接触 React+Redux,经过阅读Redux源码,了解了其实现原理。git
Redux代码量很少,结构也很清晰,函数式编程思想贯穿着整个Redux源码,如纯函数,高阶函数,Curry,Compose。github
本文首先会介绍函数式编程的思想,再逐步介绍Redux中间件的实现。编程
看完本文,但愿能够帮助你了解中间件实现的原理。redux
Redux是可预测的状态管理框架。它很好的解决多交互,多数据源的诉求。闭包
Redux设计理念有三个原则: 1. 单一数据源 2. State只读 3. 使用纯函数变动state值。app
基本概念 | 原则 | 解释 |
---|---|---|
Store | (1) 单一数据源 (2) State只读 | Store能够看作是数据存储的一个容器。在这个容器里面,只会维护惟一的一个State Tree。 Store会给定4种基础操做方法:dispatch(action), getState(), replaceReducer(nextReducer), subscribe(listener)框架 根据单一数据源原则,全部数据会经过store.getState()方法调用获取。dom 根据State只读原则,数据变动会经过store,dispatch(action)方法。koa |
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.
接受函数做为参数传入,并能返回封装后函数。
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 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中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 方法有三个主要的步骤,以下:
看到这里,你可能有这么几个疑问?
为了解决这4个疑问,下面将针对相应问题,逐步解析。
疑问:
解决思路:
const middleware1 = action => action;
const middleware2 = action => action;
const final = action => store.dispatch(action);
/*
1. compose(...)将全部中间件串联
2. 定义final做为最后执行dispatch的函数
*/
compose(final, middleware2, middleware1)(action)
复制代码
疑问:
能够参考咱们对Koa2中间件的定义 const koaMiddleware = async (ctx, next) => { };
解决思路:
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的做用,延迟计算和参数复用。
疑问:
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方法,会发生死循环在调用链中。
根据上述文字的描述,右图是死循环的说明。
解决思路:
// 这就是为何在给全部middleware,共享Store的时候,会从新定义一遍getState和dispatch方法。
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
复制代码
疑问:
上述例子有提到每次都会返回action给下一个中间件,例如 const middleware1 = store => action => action;
如何保证中间件不会由于没有传递action而断裂?
这里必须说明的是:Koa中间件能够经过调用await next()方法,继续执行下一个中间件,也能够中断当前执行,好比 ctx.response.body = ‘xxxx’ (直接中断下面中间件的执行)。
通常状况下,Redux不容许调用链中断,由于咱们最终须要改变state内容。(* 好比redux-thunk使用有意截断的除外)。
解决思路:
// 柯里化处理参数
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, 若是有帮助到你,点个赞 咩~
简历请投递至邮箱linjiayu@meituan.com