用法
为了对中间件有一个整体的认识,先从用法开始分析。调用中间件的代码如下:
export default function createStore(reducer, preloadedState, enhancer) {
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
}
enhancer
是中间件,且第二个参数为 Function
且没有第三个参数时,可以转移到第二个参数,那么就有两种方式设置中间件:
const store = createStore(reducer, null, applyMiddleware(...))
const store = createStore(reducer, applyMiddleware(...))
再看 源码 中间件的传参:
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
var store = createStore(reducer, preloadedState, enhancer)
...
}
就是为了得到 store
,并通过 createStore
创建,上述两种方法因为在 createStore
函数内部传入了自身函数才得以实现 :
export default function createStore(reducer, preloadedState, enhancer) {
...
if (typeof enhancer !== 'undefined') {
return enhancer(createStore)(reducer, preloadedState)
}
...
}
上述代码可以看出,创建 store 的过程完全交给中间件了,因此开启了中间件第三种使用方式:
const store = applyMiddleware(...)(createStore)
applyMiddleware 源码解析
大家对剖析 applyMiddleware
源码都非常感兴趣,因为它实现精简,但含义甚广,再重温其源码:
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
var store = createStore(reducer, preloadedState, enhancer)
var dispatch = store.dispatch
var chain = []
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
假设大家都已了解 ES6 7 语法,懂得 compose
函数的含义,并且看过一些源码剖析了,我们才能把重点放在核心原理上:为什么中间件函数有三个传参 store => next => action
,第二个参数 next
为什么拥有神奇的作用?
store
代码前几行创建了 store
(如果第三个参数是中间件,就会出现中间件 store 包中间件 store 的情况,但效果是完全 打平 的), middlewareAPI
这个变量,其实就是精简的 store
, 因为它提供了 getState
获取数据,dispatch
派发动作。
下一行,middlewares.map
将这个 store
作为参数执行了一遍中间件,所以中间件第一级参数 store
就是这么来的。
next
下一步我们得到了 chain
, 倒推来看,其中每个中间件只有 next => action
两级参数了。我们假设只有一个中间件 fn
,因此 compose
的效果是:
dispatch = fn(store.dispatch)
那么 next
参数也知道了,就是 store.dispatch
这个原始的 dispatch
.
action
代码的最后,返回了 dispatch
,我们一般会这么用:
store.dispatch(action)
等价于
fn(store.dispatch)(action)
第三个参数也来了,它就是用户自己传的 action
.
单一中间件的场景
我们展开代码来查看一个中间件的运行情况:
fn(middlewareAPI)(store.dispatch)(action)
对应 fn
的代码可能是:
export default store => next => action => {
console.log('beforeState', store.getState())
next(action)
console.log('nextState', store.getState())
}
当我们执行了 next(action)
后,相当于调用了原始 store
dispatch
方法,并将 action
传入其中,可想而知,下一行输出的 state
已经是更新后的了。
但是 next
仅仅是 store.dispatch
, 为什么叫做 next
我们现在还看不出来。
function dispatch(action) {
...
currentState = currentReducer(currentState, action)
...
}
其中还有一段更新监听数组对象,以达到 dispatch
过程不受干扰(快照效果) 作为课后作业大家独立研究:主要思考这段代码的意图:https://github.com/reactjs/redux/blob/master/src/createStore.js#L63
多中间件的场景
我们假设有三个中间件 fn1
fn2
fn3
, 从源码的这两句入手:
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
第一行代码,我们得到了只剩 next => action
参数的 chain
, 暂且叫做:
cfn1
cfn2
cfn3
, 并且有如下对应关系
cfnx = fnx(middlewareAPI)
第二行代码展开后是这样的:
dispatch = cfn1(cfn2(cfn3(store.dispatch)))
可以看到最后传入的中间件 fn3
最先执行。
为了便于后面理解,我先把上面代码的含义写出来:通过传入原始的 store.dispatch
, 希望通过层层中间件的调用,最后产生一个新的 dispatch
. 那么实际上,中间件所组成的 dispatch
, 从函数角度看,就是被执行过一次的 cfn1
cfn2
cfn3
函数。
我们就算不理解新 dispatch
的含义,也可以从代码角度理解:只要执行了新的 dispatch
, 中间件函数 cfnx
系列就要被执行一次,所以 cfnx
的函数本身就是中间件的 dispatch
。
对应 cfn3
的代码可能是:
export default next => action => {
next(action)
}
这就是这个中间件的 dispatch
.
那么执行了 cfn3
后,也就是 dispatch
了之后,其内部可能没有返回值,我们叫做 ncfn3
,大概如下:
export default action => {}
但其函数自身就是返回值 返回给了 cfn2
作为第一个参数,替代了 cnf3
参数 store.dispatch
的位置。
我们再想想,store.dispatch
的返回值是什么?不就是 action => {}
这样的函数吗?这样,一个中间件的 dispatch
传递完成了。我们理解了多中间件 compose
后可以为什么可以组成一个新的 dispatch
了(其实单一中间件也一样,但因为步骤只有一步,让人会想到直接触发 store.dispatch
上,多中间件提炼了这个行为,上升到组合为新的 dispatch
)。
再解释 next 的含义
为什么我们在中间件中执行 next(action)
,下一步就能拿到修改过的 store
?
对于 cfn3
来说, next
就是 store.dispatch
。我们先不考虑它为什么是 next
, 但执行它了就会直接执行 store.dispatch
,后面立马拿到修改后的数据不奇怪吧。
对于 cfn2
来说,next
就是 cfn3
执行后的返回值(执行后也还是个函数,内层并没有执行),我们分为两种情况:
-
cfn3
没有执行next(action)
,那cfn1
cfn2
都没法执行store.dispatch
,因为原始的dispatch
没有传递下去,你会发现dispatch
函数被中间件搞失效了(所以中间件还可以捣乱)。为了防止中间件瞎捣乱,在中间件正常的情况请执行next(action)
.
这就是
redux-thunk
的核心思想,如果action
是个function
,就故意执行action
, 而不执行next(action)
, 等于让store.dispatch
失效了!但其目的是明确的,因为会把dispatch
返回给用户,让用户自己调用,正常使用是不会把流程停下来的。
-
cfn3
执行了next(action)
, 那cfn2
什么时候执行next(action)
,cfn3
就什么时候执行next(action) => store.dispatch(action)
, 所以这一步的next
效果与cfn3
相同,继续传递下去也同理。我看了下redux-logger
的文档,果然央求用户把自己放在最后一个,其原因是害怕最右边的中间件『捣乱』,不执行next(action)
, 那logger
再执行next(action)
也无法真正触发dispatch
.
我在考虑这样会不会有很大的局限性,但后来发现,只要中间件常规情况执行了
next(action)
就能保证原始的dispatch
可以被继续分发下去。只要每个中间件都按照这个套路来,next(action)
的效果就与yield
类似。
所以 next
并不是完全意义上的洋葱模型,只能说符合规范(默认都执行了 next(action)
)的中间件才符合洋葱模型。
koa 的洋葱模型可是有技术保证的,
generator
可不会受到代码的影响,而redux
中间件的洋葱模型,会因为某一层不执行next(action)
而中断,而且从右开始直接切断。
为什么在中间件直接 store.dispatch(action)
,传递就会中断?
理解了上面说的话,就很简单了,并不是 store.dispatch(action)
中断了原始 dispatch
的传递,而是你执行完以后不调用 next
函数中断了传递。
总结
还是要画个图总结一下,在不想看文字的时候: