关于redux中间件是什么以及为何须要redux中间件的话题,网上有太多的文章已经介绍过了,本文就再也不赘述了。若是你有相似的困惑:react
那么欢迎往下阅读,但愿这篇文章能帮助你多一些对redux中间件的理解。git
在深刻理解中间件以前,咱们先来看一个很关键的概念。es6
在数学中, 复合函数是指 逐点地把一个 函数做用于另外一个函数的结果,所获得的第三个函数。直观地说,复合两个函数是把两个函数连接在一块儿的过程,内函数的输出就是外函数的输入。github
-- 维基百科编程
你们看到复合函数应该不陌生,由于上学时的数学课本上都出现过,咱们举例回忆下:redux
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写法基本就是这样。可是若是一会儿没法理解也不要紧,咱们只要先记住:app
咱们再举个例子来理解下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')的运行过程是:
图示:(和真实的执行栈会有差别,这里做为辅助理解)
(点击查看大图)
至此,整个运行过程就结束了。其实这就是网上不少文章里提到的洋葱模型,这里我是以执行过程当中进栈出栈的方式来说解,不知道理解起来会不会更方便些~
关于复合函数就先介绍这些,篇幅有点长,主要是由于它在redux中间件里起到了关键的做用。若是一下没理解,能够稍微再花点时间琢磨下,不着急往下读,由于理解了复合函数,基本也就理解了redux中间件的大部分核心内容了。
接下来就是解读源码的时间了~
//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方法传递给中间件,这里又有两个问题:
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 发布