redux源码分析:subscribe/unsubscribe
redux 源码之 subscribe方法
先看精简版源码
const nextListeners = [];
function subscribe(listener) {
nextListeners.push(listener);
return function unsubscribe() {
const index = nextListeners.indexOf(listener);
nextListeners.splice(index, 1);
};
}
功能分析
- 将开发者扔进来的
fn
添加到nextListeners
中去 - 并返回
unsubscribe
方法,方便用户可以直接调用这个清理方法
原始代码之 listener 必须为 function
if (typeof listener !== "function") {
throw new Error("Expected listener to be a function.");
}
为什么createStore中既存在currentListeners也存在nextListeners?
我的观点:
- 因为 subscribe 方法的入参是一个函数 ,所以,决定了使用者可以在里面各种操作,包括继续 xx.subscribe/xx.unsubscribe
- 由于 subscribe/unsubscribe 这种嵌套回调的时候出现操作同一个数组
- 如果不加以控制,就会出现 listeners[i],执行的时候取不到;然后各种报错
- 结论:真有人会玩这种嵌套这么操作? ??
网友回复:
关于nextListener的理解好像有点问题,源码中的那段注释并不是说明增加一个nextListeners的原因的,增加nextListener这个副本是为了避免在遍历listeners的过程中由于subscribe或者unsubscribe对listeners进行的修改而引起的某个listener被漏掉了。比如你在遍历到某个listener的时候,在这个listener中unsubscribe了一个在当前listener之前的listener,这个时候继续i ++的时候就会直接跳过当前listener的下一个listener,不知道有没有描述清楚
进行新的赋值开始for循环会引起重复执行订阅回调。
为什么需要 nextListeners ?
因为 订阅回调可以产生多个订阅或者嵌套订阅,而任何一个订阅回调中又可以取消平级或外层订阅,这会导致什么问题呢,for循环 遍历时 订阅数组 listeners 长度改变,遍历到的索引位置 index 没变。
如果取消的订阅索引位置在当前索引之后,说明此时被取消的订阅回调尚未执行,属于成功退订。
可是如果取消的订阅索引位置在当前索引之前呢,说明此时被取消的订阅回调已经执行,并且,此时由于listeners数组长度改变,所有数组元素下标发生 -1,当前索引 index却没改变,导致跳过当前索引后的下个订阅回调,执行了下下个订阅回调,发生了错误的退订。所以,重点来了,通过引入 nextListeners,订阅或退订操作的是 nextListeners 数组,而每次dispacth action后 执行的订阅回调数组是 currentListeners,也就是先获取 nextListener的一份快照,再遍历执行订阅回调数组的过程中发生的 订阅/退订 影响的都是nextListeners 数组,只需要在下一次 dispacth action 后遍历执行订阅回调数组前再获取一次 nextListeners 的快照,就可以保证
在订阅回调中订阅和退订的正确性。
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
解析
subscribe
和unsubscribe
本质上都在操作listeners
(这里先不管:currentListeners/nextListeners
) 的一个数组- 由于
subscribe/unsubscribe
都是同步函数,所以,只有下面这种情况会出现操作同一个数组的情况(现实中真有人会这么干吗?)
store.subscribe(()=>{
store.unsubscribe(); // 其实这种情况也可以忽略,看下面的分析
store.subscribe(()=>{
//...
});
//....
});
再看,这个决定了上面 store.unsubscribe(); 这一句代码也是废代码了
var isSubscribed = true;
// 无情的省略号...
return function unsubscribe() {
if (!isSubscribed) {
return;
}
isSubscribed = false;
ensureCanMutateNextListeners();
var index = nextListeners.indexOf(listener);
nextListeners.splice(index, 1);
};