applyMiddleware
允许使用中间件middleware增强Redex的store的 dispatch
方法,这些中间件通过组合的方式构成一条中间件的链,通过这种方式,每个中间件无需关注其他中间件的操作。
接口
applyMiddleware(…middlewares)
每个中间件 middleware
的方法签名要求为 ({ getState, dispatch }) => next => action
。
applyMiddleware 的整体结构
函数的源代码非常简洁只有不到20行的代码,函数的整体结构如下:
function applyMiddleware(...middlewares) { |
代码结构看起来很是复杂,实际上应用了函数式编程的柯里化技术,实际上柯里化是把一个接收多个参数的函数转换成接收一个参数的技巧,为了便于理解,对函数去柯里化如下:
function applyMiddleware(...middlewares, createStore, reducer, preloadedState, enhancer) { |
这下很容易看出,applyMiddleware
实质是使用 createStore
创建 store
,然后修改增强 store
的 dispatch
函数,store
的其他内容保留。
返回值这里使用了 es6
的新特性,可以翻译为:
return Object.assign({}, store, {dispatch: dispatch}); |
返回一个新对象,将 store
的所有属性复制进去,并使用新的 dispatch
函数替代 store
原有的 dispatch
函数。
注意到 applyMiddleware
的返回值为 (createStore) => (reducer, preloadedState, enhancer) => newStore
, 如果阅读过 createStore 的源代码就会发现有这么几行:
function createStore(reducer, preloadedState, enhancer) { |
如果传递了 enhancer参数,那么就会使用 enhancer 创建 store, 而 enhancer 实际上就和 applyMiddleware 的返回值几乎一样。正是因为 applyMiddleware 采用了柯里化,才保证了可以调用 applyMiddle(middleware1, middleware2, ...middlewares)
作为 createStore 的 enhancer 参数。
综上,使用中间件函数创建 store 有两种方式:
// 1.使用 applyMiddle: |
唯一的区别在于,使用 applyMiddle 还可以再次应用其他的 store enhancer
中间件。
dispatch 的增强
如果使用过其他的中间件结构比如nodejs的可以知道,中间件实际上是把 action-->reducer
变成: action -> middleware1 -> middleware2 -> ... -> reducer
,继续阅读代码:
var dispatch = store.dispatch |
首先定义了中间件的需要传递给中间件的接口 getState 和 dispatch, 这也定义了中间件的签名必定是 ({ dispatch, getState }) => next => action => {//do sth}
,这里有个细节,middlewareAPI 为什么不写成:dispatch: dispatch
而是写成: dispatch: (action) => dispatch(action)
,这个细节将在后面详细说明。
然后继续看代码,可以把chain的存储的内容转换为:
chain = middlewares.map(middleware => middleware(middlewareAPI)) |
为了后面描述的方便,这里为 chain 的成员分别命名 f1,f2, … fn 接着看 compose
:
dispatch = compose(...chain)(store.dispatch) |
compose
将函数从右向左组合:
compose([f1, f2, ..., fn]) |
返回继续阅读 applyMiddleware
并把转换后的 compose 代入:
dispatch = compose(...chain)(store.dispatch) |
把这里的 f1, f2, ..., fn
分别替换回去:
dispatch = f1(f2(action => { |
其中 "pre do sth"
指的是中间件函数定义中的 next(action)
前面的代码,在 action
被处理前调用,而 "after do sth"
指的是中间件调用 next(action)
后的代码,在 action
被处理后调用,而 next(action)
实际上也是中间件将控制权移交下一个中间件,也即调用 applyMiddleware(middleware1, middleware2, ... middlewaren)
的执行过程为:middleware1 pre -> middleware2 pre -> ... -> middlewaren pre -> store.dispatch -> middlewaren ->... -> middleware2 -> middleware1
函数。
注意: 中间件并不总是包含 "pre do sth"
和 "after do sth"
结构的,此外这里仅讨论只有同步操作的中间件,异步的情况后面讨论。
Middleware 中间件
经过上面分析,可以知道中间件的基本结构如下:
const Middleware = ({dispatch, action}) => next => action => { |
比如,可以编写日志中间件:
const logMiddleware = ({dispatch, action}) => next => action => { |
中间件的函数签名实际上是有 dispatch
的,这个 dispatch
和 next
都是接收 action
作为参数的,他们有什么区别呢?
前面已经说过,next
将 action
控制权移交下一个组件,其实质是被下一个中间件增强过的 “store.dispatch”。而回过头来再看一下 applyMiddleware 的源代码,可以发现中间件函数拿到的 dispatch实际是 :dispatch: (action) => dispatch(action)
传递给每个 middleware 的 dispatch 被一层匿名函数包裹,这样最终导致传递给每个中间件的 dispatch 实际上是组合了所有的中间件的dispatch,而不是 “store.dispatch” 这个初始的旧的 “dispatch”
而在中间件可以调用 next(action)
和 dispatch(action)
的区别可以看下图:
使用 next(action)
效果如图左,从外层逐层处理action,并将处理过的action传递给下一层,处理完毕后再逐层返回,而使用 dispatch(action)
效果如图右,调用以后重新返回到外层,相当于再次从头开始应用中间件,这种情况通常用于场景是中断当前流程,将控制权移交其他部分,待其他内容处理完成再跳回正常流程,比如异步请求场景。
下面以 redux-thunk 为例说一下这种情况,首先看一下源代码:
function createThunkMiddleware(extraArgument) { |
redux-thunk
判断 action
是否为函数,如果是,将控制权移交给 action
处理,把 dispatch
、 getState
传递给 action
,待 action
处理完毕调用 dispatch(action)
此时 action
为正常的 action对象
,回到正常中间件流程,然后redux-thunk
执行 next(action)
进入下一个中间件。
而这个 action 基本结构如下:
//action 定义 |
如上示例,asyncFetch({})
返回值不是对象而是函数,thunk
中间件拿到这个 “函数action” 后,把控制权移交给这个函数,这个函数执行 fetch
异步获取数据,获取数据成功后通过 dispatch(successAction)
再次返回 thunk中间件
,此时 successAction
是对象,因此执行 next(action)
进入下一个中间件处理,异常流程与之类似不再说明。
注意: 中间件是根据声明顺序从左到右调用的,所以如果使用到类似于 thunk
中间件的异步类中间件,一般要在applyMiddleware
参数的最前面声明,以免其他中间件被执行多次。
源码注释
/** |
相关
Redux
源码阅读笔记:
- createStore.js 源码阅读笔记
- combineReducers.js 源码阅读笔记
- bindActionCreators.js 源码阅读笔记
- applyMiddleware.js 源码阅读笔记
- compose.js 源码阅读笔记
参考
本文阅读代码版本 3.5.2