开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第10天,点击查看活动详情
09_优化stop功能
🙋 Hello,I'm IamZJT!
✍️ 一名菜鸟前端开发工程师!📦 Github项目地址:zjt-mini-vue3。
🖐️ 欢迎点赞➕star,期盼与您并肩前行...
一、定位问题
既然标题是优化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() {
// ...
}
}
ps
🎯 如果您看到这里,请不要走开。
🎉 这是一个早起俱乐部:三更灯火五更鸡!
⭐️ 寻找 志同道合 的小伙伴,我们一起 早起。