36

详解redux中间件

 3 years ago
source link: https://segmentfault.com/a/1190000023787306
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

关于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函数推出执行栈。

图示:(和真实的执行栈会有差异,这里作为辅助理解)

RNrA3a7.png!mobile

(点击查看大图)

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

关于复合函数就先介绍这些,篇幅有点长,主要是因为它在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方法

看到这里是不是觉得很熟悉了?

(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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK