对于 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
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action) // 2
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch) // 3
首先,代码中三处的 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 公布
参考:
图解 Redux 中 middleware 的洋葱模型
Understanding Redux Middleware