原文发布于我的 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) => {}(记住这个名字)。

对这个示例代码来说,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 中的元素,没错,这又是一次柯里化。

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

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

细节

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

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

图解Redux中middleware的洋葱模型相关推荐

  1. 理解redux中Middleware

    如果你使用过 Express 或者 Koa 等服务端框架, 那么应该对 middleware 的概念不会陌生. 在这类框架中,middleware 是指可以被嵌入在框架接收请求到产生响应过程之中的代码 ...

  2. 如何更好地理解中间件和洋葱模型

    相信用过 Koa.Redux 或 Express 的小伙伴对中间件都不会陌生,特别是在学习 Koa 的过程中,还会接触到 "洋葱模型". 本文阿宝哥将跟大家一起来学习 Koa 的中 ...

  3. Laravel Onion洋葱模型

    前言 因为对框架源码的生疏,笔者最近在看博文视点的陈昊的<Laravel框架关键技术解析>. 看到书中反复提及的管道处理,写下一些自己的所思所想. 书中一直在描述Laravel框架如何优雅 ...

  4. 分析洋葱模型实现原理,在自己项目中接入洋葱模型

    分析洋葱模型实现原理,在自己项目中接入洋葱模型 上一篇文章初识洋葱模型,分析中间件执行过程,浅析koa中间件源码简单的介绍了 基于 koa 的洋葱模型的中间件的运行过程,了解了一下中间件的写法 不过基 ...

  5. 说说你对koa中洋葱模型的理解?

    用过Koa的,肯定对 Middleware(中间件) 有所了解,那我们就用中间件从现象出发,理解洋葱模型 先自定义两个中间件: logTime:打印时间戳 module.exports=functio ...

  6. js reduce实现中间件_实现redux中间件-洋葱模型

    //----------------------------------------------------------------------------- //假设 存在一个核心操作 接收参数co ...

  7. 50行代码串行Promise,koa洋葱模型原来这么有趣?

    1. 前言 大家好,我是若川,最近组织了源码共读活动<1个月,200+人,一起读了4周源码>,感兴趣的可以加我微信 ruochuan12 参与,长期交流学习. 之前写的<学习源码整体 ...

  8. 学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理

    前言 这是学习源码整体架构系列第七篇.整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现.本篇文章学习的是实际仓库的代码. 学习源码整体 ...

  9. 【Web技术】988- 深入理解洋葱模型

    作者:掘金@苏里 https://juejin.im/post/6844904025767280648 前言 本文来由,希望可以剖析中间件的组合原理,从而帮助大家更加理解洋葱模型. 话不多说,正文如下 ...

最新文章

  1. 比较好用的web打印控件——Lodop
  2. Matlab实用程序--图形应用-图形的隐藏属性
  3. Git remote 修改源
  4. linux四种集群是什么,lvs四种集群特点及使用场景
  5. 不等式约束的拉格朗日乘数法_Abaqus血管支架仿真|接触约束执行方式
  6. tcl学习---windows下安装及运行环境
  7. linux命令中的cp,Linux高级技术:关于cp命令中拷贝所有的写法
  8. 修改weblogic部署的应用名称
  9. c语言网络定向拉取数据,用C模拟了一个http请求,但是recv函数接收的数据不完整且欠安顺序获取信息...
  10. 【机器学习】今天详细谈下Soft Margin SVM和 SVM正则化
  11. 求100以内的素数,全部打印出来
  12. mysql 存微信表情_MySQL保存 emoji 表情(微信昵称表情)
  13. html 轮播图左右切换代码,js实现左右轮播图
  14. win10显示隐藏文件_u盘内隐藏文件怎么显示 u盘内隐藏文件显示方法【详细步骤】...
  15. pdf转换成jpg python_Python Wand将PDF转换为JPG background
  16. 如何查看自己电脑显卡对应的cuda版本
  17. evernote国际版不可用
  18. Canvas 文字对齐方式
  19. Linux服务器上设置全局代理访问外网并验证
  20. 移动端大规模草渲染的实现(精简版)

热门文章

  1. 【亲测有效】PD虚拟机 17 for Mac功能 pd虚拟机支持m1和12系统
  2. 1)photoshop cc2017教程之软件优化
  3. 关于Class中的Signature属性
  4. 微信内置浏览器内置方法WeixinJSBridge
  5. 为什么特斯拉这么“嫌弃” 拼多多?
  6. 【微信小程序---绑定事件bindtap跳转】
  7. 小米5点android版本,小米5的手机系统是什么?小米5能升级安卓5.0吗?
  8. h5游戏突然自动打开迅雷下载配置的zip文件进不了游戏can't find central directory is this a zip file
  9. Linux——shuf sed
  10. android计算器求余,我的小计算器快完成了,就差一个取余和幂的运算了,下午搞定它...