图解Redux中middleware的洋葱模型

5,734 阅读6分钟

原文发布于我的 GitHub 博客,欢迎 star 😳

前言

最近翻出了之前分析的 applyMiddleware 发现自己又看不懂了😳,重新看了一遍源代码,梳理了洋葱模型的实现方法,在这里分享一下。

applyMiddleware源码解析

applyMiddleware 函数最短但是最 Redux 最精髓的地方,成功的让 Redux 有了极大的可拓展空间,在 action 传递的过程中带来无数的“副作用”,虽然这往往也是麻烦所在。 这个 middleware 的洋葱模型思想是从 koa 的中间件拿过来的,用图来表示最直观。

上图之前先上一段用来示例的代码(via 中间件的洋葱模型),我们会围绕这段代码理解 applyMiddleware 的洋葱模型机制:

function M1(store) {
  return function(next) {
    return function(action) {
      console.log('A middleware1 开始');
      next(action)
      console.log('B middleware1 结束');
    };
  };
}

function M2(store) {
  return function(next) {
    return function(action) {
      console.log('C middleware2 开始');
      next(action)
      console.log('D middleware2 结束');
    };
  };
}

function M3(store) {
  return function(next) {
    return function(action) {
      console.log('E middleware3 开始');
      next(action)
      console.log('F middleware3 结束');
    };
  };
}
  
function reducer(state, action) {
  if (action.type === 'MIDDLEWARE_TEST') {
    console.log('======= G =======');  
  }
  return {};
}
  
var store = Redux.createStore(
  reducer,
  Redux.applyMiddleware(
    M1,
    M2,
    M3
  )
);

store.dispatch({ type: 'MIDDLEWARE_TEST' });

再放上 Redux 的洋葱模型的示意图(via 中间件的洋葱模型),以上代码中间件的洋葱模型如下图:

            --------------------------------------
            |            middleware1              |
            |    ----------------------------     |
            |    |       middleware2         |    |
            |    |    -------------------    |    |
            |    |    |  middleware3    |    |    |
            |    |    |                 |    |    |
          next next next  ———————————   |    |    |
dispatch  —————————————> |  reducer  | — 收尾工作->|
nextState <————————————— |     G     |  |    |    |
            | A  | C  | E ——————————— F |  D |  B |
            |    |    |                 |    |    |
            |    |    -------------------    |    |
            |    ----------------------------     |
            --------------------------------------


顺序 A -> C -> E -> G -> F -> D -> B
    \---------------/   \----------/
            ↓                ↓
      更新 state 完毕      收尾工作

我们将每个 middleware 真正带来副作用的部分(在这里副作用是好的,我们需要的就是中间件的副作用),称为M?副作用,它的函数签名是 (action) => {}(记住这个名字)。

image

对这个示例代码来说,Redux 中间件的洋葱模型运行过程就是:

用户派发 action → action 传入 M1 副作用 → 打印 A → 执行 M1 的 next(这个 next 指向 M2 副作用)→ 打印 C → 执行 M2 的 next(这个 next 指向 M3 副作用)→ 打印 E → 执行 M3 的 next(这个 next 指向store.dispatch)→ 执行完毕返回到 M3 副作用打印 F → 返回到 M2 打印 E → 返回到 M1 副作用打印 B -> dispatch 执行完毕。

那么问题来了,M1 M2 M3的 next 是如何绑定的呢?

答:柯里化绑定,一个中间件完整的函数签名是 store => next => action {},但是最后执行的洋葱模型只剩下了 action,外层的 store 和 next 经过了柯里化绑定了对应的函数,接下来看一下 next 是如何绑定的。

const store = createStore(...args)
let chain = []
const middlewareAPI = {
    getState: store.getState,
    dispatch: (...args) => dispatch(...args)
}
chain = middlewares.map(middleware => middleware(middlewareAPI)) // 绑定 {dispatch和getState}
dispatch = compose(...chain)(store.dispatch) // 绑定 next

关键点就是两句绑定,先来看第一句

chain = middlewares.map(middleware => middleware(middlewareAPI)) // 绑定 {dispatch和getState}

为什么要绑定 getState?因为中间件需要随时拿到当前的 state,为什么要拿到 dispatch?因为中间件中可能会存在派发 action 的行为(比如 redux-thunk),所以用这个 map 函数柯里化绑定了 getStatedispatch

此时 chain = [(next)=>(action)=>{…}, (next)=>(action)=>{…}, (next)=>(action)=>{…}] 里闭包引用着 dispatchgetState

接下来 dispatch = compose(...chain)(store.dispatch),先了解一下 compose 函数

compose(A, B, C)(arg) === A(B(C(arg)))

这就是 compose 的作用,从右至左依次将右边的返回值作为左边的参数传入,层层包裹起来,在 React 中嵌套 Decorator 就是这么写,比如:

compose(D1, D2, D3)(Button)
// 层层包裹后的组件就是
<D1>
    <D2>
        <D3>
        	<Button />
        </D3>
    </D2>
</D1>

再说回 Redux

dispatch = compose(...chain)(store.dispatch) 

在实例代码中相当于

dispatch = MC1(MC2(MC3(store.dispatch)))

MC就是 chain 中的元素,没错,这又是一次柯里化。

image

至此,真相大白,dispatch 做了一点微小的贡献,一共干了两件事:1. 绑定了各个中间件的 next。2. 暴露出一个接口用来接收 action。其实说了这么多,middleware 就是在自定义一个dispatch,这个 dispatch 会按照洋葱模型来进行 pipe。

OK,到现在我们已经拿到了想要的 dispatch,返回就可以收工了,来看最终执行的灵魂一图流:

wx20180424-001706 2x

细节

然而可达鸭眉头一皱,发现事情还没这么简单,有几个问题要想一下

dispatch

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }

在这里 dispatch 使用匿名函数是为了能在 middleware 中调用 compose 的最新的 dispatch(闭包),必须是匿名函数而不是直接写成 store.dispatch。

如果直接写成 store.dispatch,那么在某个 middleware(除最后一个,最后一个middleware拿到的是原始的 store.dispatch)dispatch 一个 action,比如 redux-thunk

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

就是拦截函数类型的 action,再能够对函数形式的 action(其实是个 actionCreator)暴露 API 再执行一次,如果这个 actionCreator 是多层函数的嵌套,则必须每次执行 actionCreator 后的 actionCreator 都可以引用最新的 dispatch 才行。如果不写成匿名函数,那这个 actionCreator 又走了没有经过任何中间件修饰的 store.dispatch,这显然是不行的。所以要写成匿名函数的闭包引用。

还有,这里使用了 ...args 而不是 action,是因为有个 PR,这个 PR 的作者认为在 dispatch 时需要提供多个参数,像这样 dispatch(action, option) ,这种情况确实存在,但是只有当这个需提供多参数的中间件是第一个被调用的中间件时(即在 middlewares 数组中排最后)才肯定有效 ,因为无法保证上一个调用这个多参数中间件的中间件是使用的 next(action) 或是 next(...args) 来调用,所以被改成了 next(…args) ,在这个 PR 的讨论中可以看到 Dan 对这个改动持保留意见(但他还是改了),这个改动其实真的挺蛋疼的,我作为一个纯良的第三方中间件,怎么能知道你上个中间件传了什么乱七八糟的属性呢,再说传了我也不知道是什么意思啊大哥。感觉这就是为了某些 middleware 能够配合使用,不想往 action 里加东西,就加在参数中了,到底是什么参数只有这些有约定好参数的 middleware 才能知道了。

redux-logger

Note: logger must be the last middleware in chain, otherwise it will log thunk and promise, not actual actions (#20).

要求必须把自己放在 middleware 的最后一个,理由是

Otherwise it'll log thunks and promises but not actual actions.

试想,logger 想 log 什么?就是 store.dispatch 时的信息,所以 logger 肯定要在 store.dispatch 的前后 console,还记不记得上面哪个中间件拿到了 store.dispatch,就是最后一个,如果把 logger 放在第一个的话你就能打出所有的 action 了,比如 redux-thunk 的 actionCreator,打印的数量肯定比放在最后一个多,因为并不是所有的 action 都能走到最后,也有新的 action 在 middleware 在中间被派发。

参考

redux middleware 详解

Redux 进阶教程

redux applyMiddleware 原理剖析

绘图

ProcessOn