对于redux中间件是什么以及为什么须要redux中间件的话题,网上有太多的文章曾经介绍过了,本文就不再赘述了。如果你有相似的困惑:

  • redux中间件到底是如何作用于dispatch?
  • redux的源码和中间件的源码都不简单,但看起来怎么那么吃力?
  • redux中间件的洋葱模型到底是什么?
  • ...

那么欢送往下浏览,心愿这篇文章能帮忙你多一些对redux中间件的了解。

在深刻了解中间件之前,咱们先来看一个很要害的概念。

复合函数/函数组合(function composition)

在数学中,复合函数是指逐点地把一个函数作用于另一个函数的后果,所失去的第三个函数。

直观地说,复合两个函数是把两个函数链接在一起的过程,内函数的输入就是外函数的输出。

-- 维基百科

大家看到复合函数应该不生疏,因为上学时的数学课本上都呈现过,咱们举例回顾下:

f(x) = x^2 + 3x + 1g(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写法根本就是这样。然而如果一下子无奈了解也没关系,咱们只有先记住:

  1. compose(A, B, C)的返回值是:(arg)=>A(B(C(arg))),
  2. 内函数的输入就是外函数的输出

咱们再举个例子来了解下compose的作用:

// redux compose.jsfunction 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开始我是Logconsole3完结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')的运行过程是:

  1. 执行console1返回的函数,输入“console1开始”,而后执行console1外部的nextConsole(message)时,会将援用的console2返回值推入执行栈开始执行。
  2. 于是输入“console2开始”,而后执行console2外部的nextConsole(message)时,会将援用的console3返回值推入执行栈开始执行。
  3. 于是输入“console3开始”,而后执行console3外部的nextConsole(message)时,发现nextConsole就是console.log办法,于是输入“我是log”,接着执行下一句,输入“console3完结”。执行结束将console3函数推出执行栈。
  4. 此时执行栈顶部是console2函数,执行完console2的最初一条语句,输入“console2完结”后,将console2函数推出执行栈。
  5. 同上,此时执行栈顶部是console1函数,执行完console1的最初一条语句,输入“console1完结”后,将console1函数推出执行栈。

图示:(和实在的执行栈会有差别,这里作为辅助了解)

(点击查看大图)

至此,整个运行过程就完结了。其实这就是网上很多文章里提到的洋葱模型,这里我是以执行过程中进栈出栈的形式来解说,不晓得了解起来会不会更不便些~

对于复合函数就先介绍这些,篇幅有点长,次要是因为它在redux中间件里起到了要害的作用。如果一下没了解,能够略微再花点工夫推敲下,不焦急往下读,因为了解了复合函数,根本也就了解了redux中间件的大部分核心内容了。

解析applyMiddleware.js

接下来就是解读源码的工夫了~

//redux applyMiddleware.jsexport 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) => {...}}

这里有两个问题?

  1. 这些参数是从哪儿传来的?
  2. 为什么要用柯里化的形式去写?

先看第一个问题,是因为理论在configure store时,applyMiddleware是作为redux createStore办法中第三个参数enhancer被调用:

// index.jsconst store = createStore(reducer, initialState, applyMiddleware(...middlewares));// createStore.jsexport 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办法传递给中间件,这里又有两个问题:

  1. 为什么要将getState和dispatch传给中间件呢?
  2. 为什么传入的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-thunkfunction createThunkMiddleware ({ dispatch, getState }) {  return (next) =>       (action) => {      if (typeof action === 'function') {        return action(dispatch, getState, extraArgument);      }      return next(action);    };}// redux-loggerfunction 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 // 1const 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-thunkfunction 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