redux源码分析:subscribe/unsubscribe

redux 源码之 subscribe方法
更新于: 2021-12-19 12:57:28

先看精简版源码

const nextListeners = [];

function subscribe(listener) {
  nextListeners.push(listener);

  return function unsubscribe() {
    const index = nextListeners.indexOf(listener);
    nextListeners.splice(index, 1);
  };
}

功能分析

  1. 将开发者扔进来的 fn 添加到  nextListeners 中去
  2. 并返回 unsubscribe 方法,方便用户可以直接调用这个清理方法

原始代码之 listener 必须为 function

if (typeof listener !== "function") {
  throw new Error("Expected listener to be a function.");
}

为什么createStore中既存在currentListeners也存在nextListeners?

我的观点:

  1. 因为 subscribe 方法的入参是一个函数 ,所以,决定了使用者可以在里面各种操作,包括继续 xx.subscribe/xx.unsubscribe
  2. 由于 subscribe/unsubscribe 这种嵌套回调的时候出现操作同一个数组
  3. 如果不加以控制,就会出现 listeners[i],执行的时候取不到;然后各种报错
  4. 结论:真有人会玩这种嵌套这么操作? ??

网友回复:

关于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()
    }
}

解析

  1. subscribeunsubscribe 本质上都在操作 listeners(这里先不管:currentListeners/nextListeners) 的一个数组
  2. 由于 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);
};

参考

https://github.com/MrErHu/blog/issues/18