Open MrErHu opened 7 years ago
你好,文章的第三部分有一个问题。闭包不是保存的本地变量吗?为什么middlewareAPI
中的dispatch会是获取最后增强的dispatch
呢?因为dispatch = store.dispatch
吗?但是store
在该上下文中不是由createStore()
创建的吗?为什么获取的是最终store
?
@GeekaholicLin
我理解了一下你的问题,你是问为什么middlewareAPI
中的dispatch
是增强的dispatch
吗?
看代码:
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
};
chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain, store.dispatch);
传递给middlewares
的API中的dispatch
是一个匿名函数对吧。匿名函数通过闭包去访问变量dispatch
。
可是后面又有dispatch = compose(...chain, store.dispatch);
这不就把变量的值改变了吗,以后通过闭包访问的变量肯定是增强的函数。
😭 好像是的。光顾着看middlewareAPI
之前的代码了。谢谢解答~
@MrErHu 关于nextListener的理解好像有点问题,源码中的那段注释并不是说明增加一个nextListeners的原因的,增加nextListener这个副本是为了避免在遍历listeners的过程中由于subscribe或者unsubscribe对listeners进行的修改而引起的某个listener被漏掉了。比如你在遍历到某个listener的时候,在这个listener中unsubscribe了一个在当前listener之前的listener,这个时候继续i ++的时候就会直接跳过当前listener的下一个listener,不知道有没有描述清楚
@lakb248 受教了,明白了!感谢୧(๑•̀◡•́๑)૭
爱了爱了
@MrErHu 关于nextListener的理解好像有点问题,源码中的那段注释并不是说明增加一个nextListeners的原因的,增加nextListener这个副本是为了避免在遍历listeners的过程中由于subscribe或者unsubscribe对listeners进行的修改而引起的某个listener被漏掉了。比如你在遍历到某个listener的时候,在这个listener中unsubscribe了一个在当前listener之前的listener,这个时候继续i ++的时候就会直接跳过当前listener的下一个listener,不知道有没有描述清楚
如果是为了避免这种case的话,也没有必要增加nextListeners呀,通过listeners = currentListeners就能解决问题。 即便是在某个listener执行中出现了subscribe/unsubscribe , 只要对currentListeners进行新的赋值操作即可:或使currentListeners数组+1,或使currentListeners数组-1。可是无论如何,listeners依旧保持着循环之前的订阅数组的引用,也就是说用于循环的这个数组在这种情况下是稳定的,也就不会发生“跳过”。
@MrErHu 关于nextListener的理解好像有点问题,源码中的那段注释并不是说明增加一个nextListeners的原因的,增加nextListener这个副本是为了避免在遍历listeners的过程中由于subscribe或者unsubscribe对listeners进行的修改而引起的某个listener被漏掉了。比如你在遍历到某个listener的时候,在这个listener中unsubscribe了一个在当前listener之前的listener,这个时候继续i ++的时候就会直接跳过当前listener的下一个listener,不知道有没有描述清楚
如果是为了避免这种case的话,也没有必要增加nextListeners呀,通过listeners = currentListeners就能解决问题。 即便是在某个listener执行中出现了subscribe/unsubscribe , 只要对currentListeners进行新的赋值操作即可:或使currentListeners数组+1,或使currentListeners数组-1。可是无论如何,listeners依旧保持着循环之前的订阅数组的引用,也就是说用于循环的这个数组在这种情况下是稳定的,也就不会发生“跳过”。
进行新的赋值开始for循环会引起重复执行订阅回调。
为什么需要 nextListeners ? 因为 订阅回调可以产生多个订阅或者嵌套订阅,而任何一个订阅回调中又可以取消平级或外层订阅,这会导致什么问题呢,for循环 遍历时 订阅数组 listeners 长度改变,遍历到的索引位置 index 没变。 如果取消的订阅索引位置在当前索引之后,说明此时被取消的订阅回调尚未执行,属于成功退订。 可是如果取消的订阅索引位置在当前索引之前呢,说明此时被取消的订阅回调已经执行,并且,此时由于listeners数组长度改变,所有数组元素下标发生 -1,当前索引 index却没改变,导致跳过当前索引后的下个订阅回调,执行了下下个订阅回调,发生了错误的退订。
所以,重点来了,通过引入 nextListeners,订阅或退订操作的是 nextListeners 数组,而每次dispacth action后 执行的订阅回调数组是 currentListeners,也就是先获取 nextListener的一份快照,再遍历执行订阅回调数组的过程中发生的 订阅/退订 影响的都是nextListeners 数组,只需要在下一次 dispacth action 后遍历执行订阅回调数组前再获取一次 nextListeners 的快照,就可以保证 在订阅回调中订阅和退订的正确性。
前段时间看了Redux的源码,写了一篇关于Redux的源码分析: Redux:百行代码千行文档,没有看的小伙伴可以看一下,整篇文章主要是对Redux运行的原理进行了大致的解析,但是其实有很多内容并没有明确地深究为什么要这么做本篇文章的内容主要就是我自己提出一些问题,然后试着去回答这个问题,再次做个广告,欢迎大家关注我的掘金账号和我的博客。
为什么createStore中既存在currentListeners也存在nextListeners?
看过源码的同学应该了解,
createStore
函数为了保存store
的订阅者,不仅保存了当前的订阅者currentListeners
而且也保存了nextListeners
。createStore
中有一个内部函数ensureCanMutateNextListeners
:这个函数实质的作用是确保可以改变
nextListeners
,如果nextListeners
与currentListeners
一致的话,将currentListeners
做一个拷贝赋值给nextListeners
,然后所有的操作都会集中在nextListeners
,比如我们看订阅的函数subscribe
:我们发现订阅和解除订阅都是在
nextListeners
做的操作,然后每次dispatch
一个action
都会做如下的操作:我们发现在
dispatch
中做了const listeners = currentListeners = nextListeners
,相当于更新了当前currentListeners
为nextListeners
,然后通知订阅者,到这里我们不禁要问为什么要存在这个nextListeners
? 其实代码中的注释也是做了相关的解释:来让我这个六级没过的渣渣翻译一下: 订阅者(subscriptions)在每次
dispatch()
调用之前都是一份快照(snapshotted)。如果你在listener
被调用期间,进行订阅或者退订,在本次的dispatch()
过程中是不会生效的,然而在下一次的dispatch()
调用中,无论dispatch
是否是嵌套调用的,都将使用最近一次的快照订阅者列表。用图表示的效果如下: 我们从这个图中可以看见,如果不存在这个nextListeners
这份快照的话,因为dispatch
导致的store
的改变,从而进一步通知订阅者,如果在通知订阅者的过程中发生了其他的订阅(subscribe)和退订(unsubscribe),那肯定会发生错误或者不确定性。例如:比如在通知订阅的过程中,如果发生了退订,那就既有可能成功退订(在通知之前就执行了nextListeners.splice(index, 1)
)或者没有成功退订(在已经通知了之后才执行了nextListeners.splice(index, 1)
),这当然是不行的。因为nextListeners
的存在所以通知订阅者的行为是明确的,订阅和退订是不会影响到本次订阅者通知的过程。这都没有问题,可是存在一个问题,JavaScript不是单线程的吗?怎么会出现上述所说的场景呢?百思不得其解的情况下,去Redux项目下开了一个issue,得到了维护者的回答:
得了,我们再来看看测试相关的代码吧。看完之后我了解到了。的确,因为JavaScript是单线程语言,不可能出现出现想上述所说的多线程场景,但是我忽略了一点,执行订阅者函数时,在这个回调函数中可以执行退订或者订阅事件。例如:
这不就实现了在通知listener的过程中混入订阅
subscribe
与退订unsubscribe
吗?为什么Reducer中不能进行dispatch操作?
我们知道在
reducer
函数中是不能执行dispatch
操作的。一方面,reducer
作为计算下一次state
的纯函数是不应该承担执行dispatch
这样的操作。另一方面,即使你尝试着在reducer
中执行dispatch
,也并不会成功,并且会得到"Reducers may not dispatch actions."的提示。因为在dispatch
函数就做了相关的限制:在执行
dispatch
时就会将标志位isDispatching
置为true
。然后如果在currentReducer(currentState, action)
执行的过程中由执行了dispatch
,那么就会抛出错误('Reducers may not dispatch actions.')。之所以做如此的限制,是因为在dispatch
中会引起reducer
的执行,如果此时reducer
中又执行了dispatch
,这样就落入了一个死循环,所以就要避免reducer
中执行dispatch
。为什么applyMiddleware中middlewareAPI中的dispathch要用闭包包裹?
关于Redux的中间件之前我写过一篇相关的文章Redux:Middleware你咋就这么难,没有看过的同学可以了解一下,其实文章中也有一个地方没有明确的解释,当时初学不是很理解,现在来解释一下:
这个问题的就是为什么middlewareAPI中的dispathc要用闭包包裹,而不是直接传入呢?首先用一幅图来解释一下中间件: 如上图所示,中间件的执行过程非常类似于洋葱圈(Onion Rings),假设我们在函数
applyMiddleware
中传入中间件的顺序分别是mid1、mid2、mid3。而中间件函数的结构类似于:那么中间件函数内部代码执行次序分别是:
但是如果在中间件函数中调用了
dispatch
(用mid3-before
中为例),执行的次序就变成了:所以给中间件函数传入的
middlewareAPI
中dispatch
函数是经过applyMiddleware
改造过的dispatch
,而不是redux
原生的store.dispatch
。所以我们通过一个闭包包裹dispatch
:这样我们在后面给
dispatch
赋值为dispatch = compose(...chain, store.dispatch);
,这样只要 dispatch 更新了,middlewareAPI 中的 dispatch 应用也会发生变化。如果我们写成:那中间件函数中接受到的
dispatch
永远只能是最开始的redux
中的dispatch
。最后,如果大家在阅读Redux源码时还有别的疑惑和感受,欢迎大家在评论区相互交流,讨论和学习。