1、前言
上篇文章中,我们详细介绍了redux的相关知识和如何使用,最后使用中间件Middleware来帮助我们完成异步操作,如下图
上面是很典型的一次 redux 的数据流的过程,在增加了 middleware 后,我们就可以在这途中对 action 进行截获,并进行改变,进行其他操作。
同时,在使用 middleware 时,我们可以通过串联不同的 middleware
来满足日常的开发,每一个 middleware 都可以处理一个相对独立的业务需求且相互串联。
如上图所示,派发给 redux Store 的 action 对象,会被 Store 上的多个中间件依次处理,如果把 action 和当前的 state 交给 reducer 处理的过程看做默认存在的中间件,那么其实所有的对 action 的处理都可以有中间件组成的。值得注意的是这些中间件
会按照指定的顺序一次处理
传入的 action,只有排在前面的中间件完成任务之后,后面的中间件才有机会继续处理 action
,同样的,每个中间件都有自己的“熔断”处理
,当它认为这个 action 不需要后面的中间件进行处理时,后面的中间件也就不能再对这个 action 进行处理了
下面我们来研究研究Middleware。
2、正文
2.1、redux-thunk源码
我们以redux-thunk
为例,从node_modules文件夹下面找到redux-thunk文件夹,查看其源码(下图为redux-thunk源码,一共12行)
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
可以看出,thunk是createThunkMiddleware()运行的结果,而该函数里面还包裹了3层函数(柯里化),函数一层一层向下执行。
我们将其中的ES6的箭头函数换成普通函数,再观察
function createThunkMiddleware (extraArgument){
// 第一层
/* getState 可以返回最新的应用 store 数据 */
return function ({dispatch, getState}){
// 第二层
/* next 表示执行后续的中间件,中间件有可能有多个 */
return function (next){
// 第三层
/*中间件处理函数,参数为当前执行的 action */
return function (action){
if (typeof action === 'function'){
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
}
}
let thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
- 首先是
外层
,从thunk最后两行源码可知,这一层存在的主要目的是支持在调用applyMiddleware并传入thunk的时候可以不直接传入thunk本身
,而是先调用包裹了thunk的函数
(第一层柯里化的父函数),并传入需要的额外参数
,再将该函数调用的后返回的值
(也就是真正的thunk)传给applyMiddleware
,从而实现对额外参数
传入的支持,使用方式如下:
const store = createStore(reducer, applyMiddleware(thunk.withExtraArgument({api, whatever})));
- 如果
无需额外参数
则用法如下:
const store = createStore(reducer, applyMiddleware(thunk));
- 接着看
第一层
,这一层是真正applyMiddleware能够调用的一层,从形参
来看,这个函数接收了一个类似于store
的对象,因为这个对象被结构
以后获取
了它的dispatch
和getState
这两个方法,巧的是store也有这两方法,但这个对象到底是不是store,还是只借用了store的这两方法合成的一个新对象?这个问题在我们后面分析applyMiddleware源码时,自会有分晓 - 再来看
第二层
,我们接收的一个名为next
的参数,并在第三层函数
内的最后一行代码中用它去调用了一个action对象
,感觉有点 dispatch({type: 'XX_ACTION', data: {}}) 的意
思,因为我们可以怀疑它就是一个dispatch方法,或者说是其他中间件处理过的dispatch方法,似乎能通过这行代码链接上所有的中间件,并在所有只能中间件自身逻辑处理完成后,最终调用真实的store.dispath去dispatch一个action对象,再走到下一步,也就是reducer内 - 最后看
第三层
,在这一层函数的内部源码中首先判断了action的类型
:如果action是一个方法
,我们就调用它
,并传入dispatch、getState、extraArgument三个参数
,因为在这个方法内部,我们可能需要调用到这些参数,至少dispatch是必须的。这三行源码才是真正的thunk核心所在,简直是太简单了。所有中间件的自身功能逻辑也是在这里实现的。如果action不是一个函数
,就走之前解析第二层
时提到的步骤。
2.2、ApplyMiddleware源码
applyMiddleware函数共十来行代码,这里将其完整复制出来。
import compose from './compose'
export default function applyMiddleware(...middlewares) {
return (createStore) => (...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args),
}
// 1、将store对象的基本方法传递给中间件并依次调用中间件
const chain = middlewares.map((middleware) => middleware(middlewareAPI))
// 2、改变dispatch指向,并将最初的dispatch传递给compose
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch,
}
}
}
同样,我们将applyMiddleware的ES6箭头函数形式转换成ES5普通函数的形式
function applyMiddleware (...middlewares){
return function (createStore){
return function (reducer, preloadedState, enhancer){
const store = createStore(reducer, preloadedState, enhancer);
let dispatch = function (){
throw new Error('Dispatching while constructing your middleware is not allowed. Other middleware would not be applied to this dispatch.')
};
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
};
// 1、将store对象的基本方法传递给中间件并依次调用中间件
const chain = middlewares.map(middleware => middleware(middlewareAPI));
// 2、改变dispatch指向,并将最初的dispatch传递给compose
dispatch = compose(...chain)(store.dispatch);
return {
...store,
dispatch
};
}
}
}
从其源码可以看出,applyMiddleware内部一开始也是两层柯里化
,所以我们看看和applyMiddleware最有关系的createStore的主要源码。
2.3、CreateStore源码
在平时业务中,我们创建store时,一般这样写
const store = createStore(reducer,initial_state,applyMiddleware(···));
或者
const store = createStore(reducer, applyMiddleware(...));
所以我们也要关注createStore
和applyMiddleware
的源码
createStore部分源码:
// 摘至createStore
export function createStore(reducer, preloadedState, enhancer) {
...
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
/*
若使用中间件,这里 enhancer 即为 applyMiddleware()
若有enhance,直接返回一个增强的createStore方法,可以类比成react的高阶函数
*/
return enhancer(createStore)(reducer, preloadedState)
}
......
......
dispatch({ type: ActionTypes.INIT })
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable,
}
}
对于createStore的源码我们只需要关注和applyMiddleware有关
的地方。从其内部前面一部分代码来看,其实很简单,就是对调用createStore时传入的参数进行一个判断
,并对参数做矫正
,再决定以哪种方式来执行后续代码
。据此可以得出createStore有多种使用方法,根据第一段参数判断规则,我们可以得出createStore的两种使用方式:
const store = createStore(reducer, {a: 1, b: 2}, applyMiddleware(...));
以及
const store = createStore(reducer, applyMiddleware(...));
- 根据第一段参数判断规则,我们可以肯定的是:
applyMiddleware返回的一定是一个函数
- 经过createStore中的第一个参数判断规则后,对参数进行了校正,得到了新的enhancer得值:如果
新的enhancer
的值不为undeifined
,便将createStore传入enhancer
(即applyMiddleware调用后返回的函数)内,让enhancer执行创建store的过程
。也就时说这里的:
enhancer(createStore)(reducer, preloadedState);
实际上等同于:
applyMiddleware(mdw1, mdw2, mdw3)(createStore)(reducer, preloadedState);
这也解释了为啥applyMiddleware会有两层柯里化
,同时表明它还有一种很函数式编程的用法,即 :
const store = applyMiddleware(mdw1, mdw2, mdw3)(createStore);
这种方式将创建store
的步骤完全放在了applyMiddleware内部
,并在其内第二层柯里化
的函数内执行创建store的过程即调用createStore
,调用后程序将跳转至createStore
走参数判断流程最后再创建store
。
无论哪一种执行createStore的方式,我们都终将得到store,也就是在creaeStore内部最后返回的那个包含dispatch
、subscribe
、getState
等方法的对象。
2.4、回看ApplyMiddleware源码
对于applyMiddleware开头的两层柯里化的出现原因以及和createStore有关的方面,在前面分析过。同时,我们之前在redux-thunk里的第一层柯里化中猜测传入的对象是一个类似于store的对象,通过上个章节中applyMiddleware的确实可以确认了。
这里我们主要讨论中间件是如何通过applyMiddleware的工作起来并实现挨个串联的。
接下来这几段代码是整个applyMiddleware的核心部分
,也解释了在第二章节中,我们对thunk中间件为啥有三层柯里化的疑虑
// ...
// 1、将store对象的基本方法传递给中间件并依次调用中间件
const chain = middlewares.map(middleware => middleware(middlewareAPI));
// 2、改变dispatch指向,并将最初的dispatch传递给compose
dispatch = compose(...chain)(store.dispatch);
return {
...store,
dispatch
};
// ...
- 首先,我们可以直观的看到,applyMiddleware的执行结果最终返回的是:
store的所有方法
和一个dispatch方法
。
2.4.1、redux-thunk的第一层柯里化
这个dispatch方法是怎么来的呢?我们来看头两行代码,这两行代码也是所有中间件被串联起来的核心部分实现,它们也决定了中间件内部为啥会有我们在之前章节中提到的三层柯里化的固定格式,先看第一行代码:
const chain = middlewares.map(middleware => middleware(middlewareAPI));
- 遍历所有的中间件,并调用它们,传入那个类似于store的对象middlewareAPI,这会
导致
中间件(redux-thunk)中第一层柯里化函数被调用
,并返回
一个接收next(即dispatch)方法作为参数的新函数
- 这一层柯里化主要原因,还是考虑到中间件内部会有调用store方法的需求,所以我们需要在此注入相关的方法,其内存函数可以通过闭包的方式来获取并调用,若有需要的话
- 遍历结束以后,我们拿到了一个
包含所有中间件
新返回的函数的一个数组
,将其赋值给变量chain
,译为函数链
2.4.2、redux-thunk的第二层柯里化
再来看第二句代码:
dispatch = compose(...chain)(store.dispatch);
- 我们展开了这个数组,并将其内部的元素(函数)传给了compose函数,compose函数又返回了我们一个新函数。然后我们再调用这个新函数并传入了原始的未经任何修改的dispatch方法,最后返回一个经过了修改的新的dispatch方法
- 先说一句,
compose是从右到左依次调用传入其内部的函数链
- thunk中间件的
第二层柯里化函数即在compose内部被调用
,并接收了经其右边那个中间函数改造并返回dispatch方法作为入参,并返回一个新的函数,再在该函数内部添加自己的逻辑,最后调用右边那个中间函数改造
并返回dispatch方法
接着执行前一个中间件
的逻辑(当然如果只有一个thunk中间件被应用了,或者他出入传入compose时的最后一个中间件,那么传入的dispatch方法即为原始的store.dispatch方法)
2.4.3、redux-thunk的第三层柯里化
thunk的第三层柯里化函数,即为被thunk改造后的dispatch方法:
// ...
return function (action){
// thunk的内部逻辑
if (typeof action === 'function'){
return action(dispatch, getState, extraArgument);
}
// 调用经下一个中间件(在compose中为之前的中间件)改造后的dispatch方法(本层洋葱壳的下一层),并传入action
return next(action);
};
// ...
- 这个改造后的dispatch函数将通过compose传入thunk左边的那个中间件作为入参
2.4.4、总结
经上述分析,我们可以得出一个中间件的串联和执行时的流程,以下面这段使用applyMiddleware的代码为例:
export default createStore(reducer, applyMiddleware(middleware1, middleware2, middleware3));
- 在applyMiddlware内部的compose
串联中间件
时,顺序是从右至左
,就是先调用middleware3、再middleware2、最后middleware1 - middleware3最开始接收真正的store.dispatch作为入参,并返回改造的的dispatch函数作为入参传给middleware2,这个改造后的函数内部包含有对原始store.dispatch的调用。依次内推知道从右到左走完所有的中间件
- 整个过程就像是给原始的store.dispatch方法套上了一层又一层的壳子,最后得到了一个类似于
洋葱结构
的东西,也就是下面源码中的dispatch,这个经过中间件改造
并返回的dispatch方法将替换store被展开后的原始的dispatch方法
:
// ...
return {
...store,
dispatch
};
// ...
- 而
原始的store.dispatch
就像这洋葱内部的芯
,被覆盖在了一层又一层的壳的最里面 - 而当我们剥壳的时候,剥一层壳,执行一层的逻辑,即走一层中间件的功能,直至调用藏在
最里边的原始的store.dispatch方法去派发action
。这样一来我们就不需要在每次派发action的时候再写单独的代码逻辑的
如上图所示:
- 在
中间件串联
的时候,middleware1-3
的串联顺序
是从右至左
的,也就是middleware3
被包裹在了最里面
,它内部含有对原始的store.dispatch的调用
,middleware1
被包裹在了最外边
- 在
执行业务代码中dispatch一个action
时,也就是中间件执行
的时候,middleware1-3
的执行顺序
是从左至右
的,因为最后被包裹的中间件,将被最先执行
2.5、总体流程
进过上述分析,我们可以将其主要功能按步骤划分如下:
1、依次执行middleware:
将middleware
执行后返回的函数合并
到一个chain数组
,这里我们有必要看看标准middleware的定义格式,如下
**加粗样式**export default store => next => action => {}
// 即
function (store) {
return function(next) {
return function (action) {
return {}
}
}
}
那么此时合并的chain结构如下
[ ...,
function(next) {
return function (action) {
return {}
}
}
]
2、改变dispatch指向:
想必你也注意到了compose函数
,compose函数如下:
[...chain].reduce((a, b) => (...args) => a(b(...args)))
实际就是一个柯里化函数
,即将所有的middleware合并成一个middleware
,并在最后一个middleware中传入当前的dispatch
。
// 假设chain如下:
chain = [
a: next => action => { console.log('第1层中间件') return next(action) }
b: next => action => { console.log('第2层中间件') return next(action) }
c: next => action => { console.log('根dispatch') return next(action) }
]
调用compose(...chain)(store.dispatch)
后返回a(b(c(dispatch)))
。
可以发现已经将所有middleware串联
起来了,并同时修改了dispatch的指向
。最后看一下这时候compose执行返回,如下:
dispatch = a(b(c(dispatch)))
调用dispatch(action),执行循序:
1. 调用 a(b(c(dispatch)))(action) __print__: 第1层中间件
2. 返回 a: next(action) 即b(c(dispatch))(action)
3. 调用 b(c(dispatch))(action) __print__: 第2层中间件
4. 返回 b: next(action) 即c(dispatch)(action)
5. 调用 c(dispatch)(action) __print__: 根dispatch
6. 返回 c: next(action) 即dispatch(action)
7. 调用 dispatch(action)
本博客参考文章: