09_优化stop功能
一、定位问题
既然标题是优化stop
功能,那就意为着我们之前实现的stop
功能是存在一定的缺陷了,或者说是不满足某些特定情况的,也就是边缘case。
先来回顾一下之前的测试案例:
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++
呢,响应式还是可以正确的被停止掉吗?
那既然这样,我们就更新一下测试案例,然后重新跑一遍。

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


接下来用webstorm开始调试:

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

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

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

再往下走,会发现,又进入了track
,dep
中又被重新收集了依赖,activeEffect.deps
又重新反向收集,所以我们之前的清空都白做了。 然后,又触发set
,走trigger
,执行run
的时候,又触发了get
,继续收集依赖,反向收集,然后dummy
被更新成3,所以上面实际值是3,也就清晰了。
抓到元凶了!
obj.prop = 3;
obj.prop++;
两种操作的区别就是:
obj.prop = 3;
只触发了set
,并没有触发get
。obj.prop++
可以分解来看,obj.prop = obj.prop + 1;
,所以既触发了set
,又触发了get
。
二、解决问题
清空过后的依赖,由于触发了get
,导致又被重新收集回去。
既然定位到了问题所在,那接下来的难点就是如何解决这个问题?
那就由我们手动判断是否应该去收集这个依赖。很显然,当++的时候,我们并不希望去收集这个依赖。
// 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
过,那么问题就迎刃而解了。
接下来进行处理:
// 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() {
// ...
}
}