关于redux中间件是什么以及为什么需要redux中间件的话题,网上有太多的文章已经介绍过了,本文就不再赘述了。如果你有类似的困惑:
- redux中间件究竟是如何作用于dispatch?
- redux的源码和中间件的源码都不复杂,但看起来怎么那么费劲?
- redux中间件的洋葱模型到底是什么?
- ...
那么欢迎往下阅读,希望这篇文章能帮助你多一些对redux中间件的理解。
在深入理解中间件之前,我们先来看一个很关键的概念。
复合函数/函数组合(function composition)
在数学中,复合函数是指逐点地把一个函数作用于另一个函数的结果,所得到的第三个函数。直观地说,复合两个函数是把两个函数链接在一起的过程,内函数的输出就是外函数的输入。
\-- 维基百科
大家看到复合函数应该不陌生,因为上学时的数学课本上都出现过,我们举例回忆下:
f(x) = x^2 + 3x + 1
g(x) = 2x
(f ∘ g)(x) = f(g(x)) = f(2x) = 4x^2 + 6x + 1
其实编程上的复合函数和数学上的概念很相似:
var greet = function(x) { return `Hello, ${ x }` };
var emote = function(x) { return `${x} :)` };
var compose = function(f, g) {
return function(x) {
return f(g(x));
}
}
var happyGreeting = compose(greet, emote);
// happyGreeting(“Mark”) -> Hello, Mark :)
这段代码应该不难理解,接下来我们来看下compose方法的es6写法,效果是等价的:
const compose = (...funcs) => {
return funcs.reduce((f, g) => (x) => f(g(x)));
}
这个写法可能需要你花点时间去理解。如果理解了,那么恭喜你,因为redux的compose写法基本就是这样。但是如果一下子无法理解也没关系,我们只要先记住:
- compose(A, B, C)的返回值是:(arg)=>A(B(C(arg))),
- 内函数的输出就是外函数的输入
我们再举个例子来理解下compose的作用:
// redux compose.js
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)))
}
function console1(nextConsole) {
return (message) => {
console.log('console1开始');
nextConsole(message);
console.log('console1结束');
}
}
function console2(nextConsole) {
return (message) => {
console.log('console2开始');
nextConsole(message);
console.log('console2结束');
}
}
function console3(nextConsole) {
return (message) => {
console.log('console3开始');
nextConsole(message);
console.log('console3结束');
}
}
const log = compose(console1, console2, console3)(console.log);
log('我是Log');
/*
console1开始
console2开始
console3开始
我是Log
console3结束
console2结束
console1结束
*/
看到这样的输出结果是不是有点意外?我们来进一步解析下:
因为:
compose(A, B, C)的返回值是:(arg) => A(B(C(arg)))
所以:
compose(console1, console2, console3)(console.log)的结果是:console1(console2(console3(console.log)))
因为:
内函数的输出就是外函数的输入
所以,根据console1(console2(console3(console.log)))从内到外的执行顺序可得出:
console3的nextConsole参数是console.log
console2的nextConsole参数是console3(console.log)的返回值
console1的nextConsole参数是console2(console3(console.log))的返回值
也就是说在console1(console2(console3(console.log))执行后,由于闭包的形成,所以每个console函数内部的nextConsole保持着对下一个console函数返回值的引用。
所以执行log('我是Log')的运行过程是:
- 执行console1返回的函数,输出“console1开始”,然后执行console1内部的nextConsole(message)时,会将引用的console2返回值推入执行栈开始执行。
- 于是输出“console2开始”,然后执行console2内部的nextConsole(message)时,会将引用的console3返回值推入执行栈开始执行。
- 于是输出“console3开始”,然后执行console3内部的nextConsole(message)时,发现nextConsole就是console.log方法,于是输出“我是log”,接着执行下一句,输出“console3结束”。执行完毕将console3函数推出执行栈。
- 此时执行栈顶部是console2函数,执行完console2的最后一条语句,输出“console2结束”后,将console2函数推出执行栈。
- 同上,此时执行栈顶部是console1函数,执行完console1的最后一条语句,输出“console1结束”后,将console1函数推出执行栈。
图示:(和真实的执行栈会有差异,这里作为辅助理解)
(点击查看大图)
至此,整个运行过程就结束了。其实这就是网上很多文章里提到的洋葱模型,这里我是以执行过程中进栈出栈的方式来讲解,不知道理解起来会不会更方便些~
关于复合函数就先介绍这些,篇幅有点长,主要是因为它在redux中间件里起到了关键的作用。如果一下没理解,可以稍微再花点时间琢磨下,不着急往下读,因为理解了复合函数,基本也就理解了redux中间件的大部分核心内容了。
解析applyMiddleware.js
接下来就是解读源码的时间了~
//redux applyMiddleware.js
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
}
}
}
首先来看下applyMiddleware的框架:applyMiddleware接受一个中间件数组,返回一个参数为createStore的函数,该函数再返回一个参数为reducer、preloadedState、enhancer的函数。
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {...}
}
这里有两个问题?
- 这些参数是从哪儿传来的?
- 为什么要用柯里化的方式去写?
先看第一个问题,是因为实际在configure store时,applyMiddleware是作为redux createStore方法中第三个参数enhancer被调用:
// index.js
const store = createStore(reducer, initialState, applyMiddleware(...middlewares));
// createStore.js
export default function createStore(reducer, preloadedState, enhancer) {
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
...
}
我们可以在createStore的源码中看到,当enhancer是function时,会先传入自身createStore函数,返回的函数再传入初始传给createStore的reducer和preloadedState,所以第一个问题得到了解答。而第二个问题是因为如果要给createStore传多个enhancer的话,需要先用compose组合一下enhancer,而柯里化和compose的配合非常好用,所以这里会采取柯里化的写法。那为什么好用呢?以后会写篇相关的文章来介绍,这里先不多做介绍了~
我们接着分析,那么此时的enhancer是什么?很明显,就是applyMiddleware(...middlewares)的返回值
// applyMiddleware(...middlewares)
(createStore) => (reducer, preloadedState, enhancer) => {...}
那 enhancer(createStore)(reducer, preloadedState) 连续调用的结果是什么?这就来到了applyMiddleware的内部实现,总得来说就是接收外部传入的createStore、reducer、preloadedState参数,用createStore生成一个新的store对象,对新store对象中的dispatch方法用中间件增强,返回该store对象。
// 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 // 返回给全局store的是经过中间件增强的dispatch
}
// }
// }
接着我们分析下内部实现,首先用dispatch变量保存store.dispatch,然后将getState方法和dispatch方法传递给中间件,这里又有两个问题:
- 为什么要将getState和dispatch传给中间件呢?
- 为什么传入的dispatch要用匿名函数包裹下,而不是直接传入store.dispatch?
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 // 返回给全局store的是经过中间件增强的dispatch
}
关于第一个问题,我们先来看两个常见的中间件内部实现(简易版)
// redux-thunk
function createThunkMiddleware ({ dispatch, getState }) {
return (next) =>
(action) => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
// redux-logger
function createLoggerMiddleware({ getState }) {
return (next) =>
(action) => {
const prevState = getState();
next(action);
const nextState = getState();
console.log(`%c prev state`, `color: #9E9E9E`, prevState);
console.log(`%c action`, `color: #03A9F4`, action);
console.log(`%c next state`, `color: #4CAF50`, nextState);
};
}
其实第一个问题的答案也就有了,因为中间件需要接收getState和dispatch在内部使用,logger需要getState方法来获取当前的state并打印,thunk需要接收dispatch方法在内部进行再次派发,
关于第二个问题我们一会再解答 :)
我们继续分析源码,那么此时map后的chain数组也就是每个中间件调用了一次后的结果:
chain = [(next)=>(action)=>{...}, (next)=>(action)=>{...}, (next)=>(action)=>{...}];
// 要注意此时每个中间件的内部实现{...}都闭包引用着传入的getState和dispatch方法
看到这里是不是觉得很熟悉了?
// console1,console2,console3(nextConsole) => (message) => {...}
const log = compose(console1, console2, console3)(console.log);
log('我是Log');
// log执行后输出的洋葱式结果不重复展示了
我们同样可以推导出:
// middleware1, middleware2, middleware3
// (next) => (action) => {...}
// dispatch = compose(...chain)(store.dispatch); 等于下一行
dispatch = compose(middleware1, middleware2, middleware3)(store.dispatch);
如果调用dispatch(action),也会像洋葱模型那样经过每一个中间件,从而实现每个中间件的功能,而该dispatch也正是全局store的dispatch方法,所以我们在项目中使用dispatch时,使用的也都是增强过的dispatch。
至此我们也了解了applyMiddleware是如何将中间件作用于原始dispatch的。
别忘了,我们还漏了一个问题没解答:为什么传入的dispatch要用匿名函数包裹下,而不是直接传入store.dispatch?
我们再来看下内部实现:
let dispatch = store.dispatch // 1 let dispatch = ...
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action) // 2 dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch) // 3 dispatch = ...
首先,代码中三处的dispatch都是同一个引用,那么经由匿名函数包裹的dispatch,通过middlewareAPI传入middleware后,middleware内部的dispatch就可以始终保持着对外部dispatch的引用(因为形成了闭包)。也就是说,当注释3的代码执行后,middleware内部的dispatch也就变成了增强型dispatch。那么这样处理有什么好处呢?我们来看个场景
// redux-thunk
function createThunkMiddleware ({ dispatch, getState }) {
return (next) =>
(action) => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
// 使用到thunk的异步action场景
const setDataAsync = () => {
return (dispatch) => {
setTimeout(() => {
dispatch({ type: 'xxx', payload: 'xxx' });
}, 3000)
}
}
const getData = () => {
return (dispatch) => {
return fetch.get(...).then(() => { dispatch(setDataAsync()); })
}
}
dispatch(getData());
如果是一个异步action嵌套另一个异步action的场景,而此时传入的dispatch如果是原始store.dispatch,dispatch(setDataAsync())的执行就会有问题,因为原始的store.dispatch无法处理传入函数的情况,那么这个场景就需要中间件增强后的dispatch来处理。
所以这也就解释了为什么传入的dispatch要用匿名函数包裹,因为可能在某些中间件内部需要使用到增强后的dispatch,用于处理更多复杂的场景。
好,关于redux中间件的内容就先介绍到这里。非常感谢能看到此处的读者,在现在碎片化阅读盛行的时代,能耐心看完如此篇幅的文章实属不易~
最后,打个小广告,欢迎star一波我司自研的react移动端组件——Zarm
相关介绍文章:
对不起,我们来晚了 —— 基于 React 的组件库 Zarm 2.0 发布
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。