原文发布于个人 GitHub 博客,欢迎 star 😳react
最近翻出了以前分析的 applyMiddleware 发现本身又看不懂了😳,从新看了一遍源代码,梳理了洋葱模型的实现方法,在这里分享一下。git
applyMiddleware 函数最短可是最 Redux 最精髓的地方,成功的让 Redux 有了极大的可拓展空间,在 action 传递的过程当中带来无数的“反作用”,虽然这每每也是麻烦所在。 这个 middleware 的洋葱模型思想是从 koa 的中间件拿过来的,用图来表示最直观。github
上图以前先上一段用来示例的代码(via 中间件的洋葱模型),咱们会围绕这段代码理解 applyMiddleware 的洋葱模型机制:redux
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) => {}
(记住这个名字)。promise
对这个示例代码来讲,Redux 中间件的洋葱模型运行过程就是:bash
用户派发 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 执行完毕。markdown
那么问题来了,M1 M2 M3的 next 是如何绑定的呢?闭包
答:柯里化绑定,一个中间件完整的函数签名是 store => next => action {}
,可是最后执行的洋葱模型只剩下了 action,外层的 store 和 next 通过了柯里化绑定了对应的函数,接下来看一下 next 是如何绑定的。app
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 函数柯里化绑定了 getState
和 dispatch
。
此时 chain = [(next)=>(action)=>{…}, (next)=>(action)=>{…}, (next)=>(action)=>{…}]
,…
里闭包引用着 dispatch
和 getState
。
接下来 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,返回就能够收工了,来看最终执行的灵魂一图流:
然而可达鸭眉头一皱,发现事情还没这么简单,有几个问题要想一下
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 才能知道了。
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 在中间被派发。