Skip to content
On this page

09_优化stop功能

一、定位问题

既然标题是优化stop功能,那就意为着我们之前实现的stop功能是存在一定的缺陷了,或者说是不满足某些特定情况的,也就是边缘case。

先来回顾一下之前的测试案例:

ts
it('stop', () => {
  let dummy;
  const obj = reactive({ prop: 1 });
  const runner = effect(() => {
    dummy = obj.prop;
  });

  obj.prop = 2;
  expect(dummy).toBe(2);

  stop(runner);
  obj.prop = 3;
  expect(dummy).toBe(2);

  runner();
  expect(dummy).toBe(3);
});

其实眼尖的小朋友应该发现了,你stop完以后,obj.prop++呢,响应式还是可以正确的被停止掉吗?

那既然这样,我们就更新一下测试案例,然后重新跑一遍。

09_01_边缘case首次单测结果

通过结果,可以看出,期望值Expected是2,而实际收到的值Received却是3,那就意味着响应式并没有被正确停止,那具体是什么原因呢,我们不妨来调试一下看看过程。

首先在关键节点,打上断点。

09_02_断点位置109_03_断点位置2

接下来用webstorm开始调试:

09_04_cleanEffect()中this存在

首先当我们走到cleanEffect(this)这一步时,会发现this是存在的,且deps里面也是有值的。

09_05_deps被成功清空

继续往下走,当cleanEffect(this)这一步执行完后,会发现deps中的Set都被清空了,也就是这个依赖也都从收集到的dep中被正确删除了。

乍一看,好像没啥问题,继续往下走。

09_06_重新触发get进入track

发现又触发了get操作,读取的是prop这个属性。

09_07_被重新收集依赖和反向收集

再往下走,会发现,又进入了trackdep中又被重新收集了依赖,activeEffect.deps又重新反向收集,所以我们之前的清空都白做了。 然后,又触发set,走trigger,执行run的时候,又触发了get,继续收集依赖,反向收集,然后dummy被更新成3,所以上面实际值是3,也就清晰了。

抓到元凶了!

ts
obj.prop = 3;
obj.prop++;

两种操作的区别就是:

  • obj.prop = 3;只触发了set,并没有触发get
  • obj.prop++可以分解来看,obj.prop = obj.prop + 1;,所以既触发了set,又触发了get

二、解决问题

清空过后的依赖,由于触发了get,导致又被重新收集回去。

既然定位到了问题所在,那接下来的难点就是如何解决这个问题?

那就由我们手动判断是否应该去收集这个依赖。很显然,当++的时候,我们并不希望去收集这个依赖。

ts
// src/reactivity/effect.ts

let activeEffect;
let shouldTrack = false; // + 是否应该收集依赖

// * ============================== ↓ 依赖收集 track ↓ ============================== * //
// * targetMap: target -> key
const targetMap = new WeakMap();

// * target -> key -> dep
export function track(target, key) {
  // * depsMap: key -> dep
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }

  // * dep
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }

  if (!activeEffect) return;
  if (!shouldTrack) return;

  dep.add(activeEffect);
  activeEffect.deps.push(dep);
}

那什么时候不应该去收集这个依赖呢,其实就是当我们stop过以后,这个依赖就不应该被收集了。

而且我们知道,dep收集到的依赖其实就是activeEffect,而activeEffect是在run的时候去赋值的。

那我们只需要根据是否已经被stop,来区分run的时候是否给activeEffect赋值。

然而ReactiveEffect类中的active状态就是用来判断是否已经被stop过,那么问题就迎刃而解了。

接下来进行处理:

ts
// src/reactivity/effect.ts

let shouldTrack;

class ReactiveEffect {
  private _fn: any;
  deps = [];
  active = true; // 是否已经 stop 过,true 为 未stop
  onStop?: () => void;

  // 在构造函数的参数上使用public等同于创建了同名的成员变量
  constructor(fn, public scheduler?) {
    this._fn = fn;
  }

  run() {
    // 已经被stop,那就直接返回结果
    if (!this.active) {
      return this._fn();
    }
    // 未stop,继续往下走
    // 此时应该被收集依赖,可以给activeEffect赋值,去运行原始依赖
    shouldTrack = true;
    activeEffect = this;
    const result = this._fn();
    // 由于运行原始依赖的时候,必然会触发代理对象的get操作,会重复进行依赖收集,所以调用完以后就关上开关,不允许再次收集依赖
    shouldTrack = false;

    return result;
  }

  stop() {
    // ...
  }
}