乐趣区

关于前端:详解redux中间件

对于 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 写法根本就是这样。然而如果一下子无奈了解也没关系,咱们只有先记住:

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

咱们再举个例子来了解下 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’)的运行过程是:

  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.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) => {...}
}

这里有两个问题?

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

先看第一个问题,是因为理论在 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 办法传递给中间件,这里又有两个问题:

  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-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

退出移动版