一起学Vue3源码,实现最简Vue3【05】 - 实现 effect 的 stop 功能
实现 effect 的 stop 功能
本章继续来完善effect 功能。 什么是stop,简而言之就是拿到调用 effect 返回的runner,把当前返回runner的effect 实例从 依赖收集中清空掉。还是老样子,测试驱动开发。
effect.spec.ts
it("stop", () => {
let dummy;
const obj = reactive({ prop: 1 });
const runner = effect(() => {
dummy = obj.prop;
});
// 只触发了 set 操作,即触发依赖
obj.prop = 2;
expect(dummy).toBe(2);
stop(runner);
// get => set
obj.prop++;
expect(dummy).toBe(2);
runner();
expect(dummy).toBe(3);
});
从测试来看,会拿到当前的effect 返回的runner作为参数传值,但是runner 下的effect实例从哪里获取呢? 上一讲中,调用effect 时,会把runner 返回,在返回runner时候把当前effect实例挂载到 runner上,就可以拿到了。
effect.ts
// 执行函数
function effect(fn, options: any = {}) {
const _effect = new ReactiveEffect(fn, options?.scheduler);
// options 继承给 _effect,其中就包括了onStop fn
extend(_effect, options);
_effect.run();
const runner: any = _effect.run.bind(_effect);
// 把 effect 挂载到runner上
runner.effect = _effect;
return runner;
}
// stop
export function stop(runner) {
runner.effect.stop();
}
下面来实现stop()
// 全局 stop 状态, true 没触发 false 为触发
let shouldTrack: Boolean;
export class ReactiveEffect {
private _fn: Function;
// 用来收集 effect 上所有dep 的数组
deps: Array<ReactiveEffect> = [];
active: Boolean = true;
public scheduler: Function | undefined;
// 公共属性 scheduler 才会被允许在类外部执行
constructor(fn, scheduler?: Function) {
this._fn = fn;
this.scheduler = scheduler;
}
run() {
// 如果调用了 stop,就直接调用 函数
if (!this.active) {
return this._fn();
}
// 应该收集依赖
shouldTrack = true;
activeEffect = this;
const res = this._fn();
// reset
shouldTrack = false;
return res;
}
stop() {
// 外部不管调用n次,节约性能调用一次即可
if (this.active) {
// 语义化抽离出功能, 清除依赖中的effect
cleanupEffect(this);
this.active = false;
}
}
}
function cleanupEffect(effect) {
// 从effect上的依赖中删除当前的 effect
effect.deps.forEach((dep: any) => {
dep.delete(effect);
});
// 清空空 Set,节约空间
effect.deps.length = 0;
}
那么effect上的依赖deps 又是怎么来的呢? 答案是收集依赖的时候最后在把依赖全部push到deps数组中
// 收集依赖
function track(target, key) {
if (!isTracking()) return;
// target => key => dep
// target 目标对象
// key 目标对象中的属性
// dep 属性关联的函数(不可以重复)
let depsMap = targetsMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetsMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
// 避免重复收集依赖
if (dep.has(activeEffect)) return;
dep.add(activeEffect);
// 把依赖全部push到effect.deps上
activeEffect.deps.push(dep);
}
export function isTracking() {
// 单纯的走reactive/ref测试,执行fn,后会触发get操作,即收集依赖函数会被调用,而此时,没有执行effect,所以是没有effect 实例的, 即 activeEffect = undefined
return shouldTrack && activeEffect !== undefined;
}
ok, 测试通过,下面再实现一个简单的 onStop 即,在ReactiveEffect 类上 加一个onStop 属性,只要调用了stop他就会执行一次
effect.spec.ts
it("onStop", () => {
let dummy;
const obj = reactive({ prop: 1 });
const onStop = jest.fn();
const runner = effect(
() => {
dummy = obj.prop;
},
{ onStop }
);
stop(runner);
expect(onStop).toHaveBeenCalledTimes(1);
});
effect.ts
export class ReactiveEffect {
private _fn: Function;
deps: Array<ReactiveEffect> = [];
active: Boolean = true;
public scheduler: Function | undefined;
// 不是必传属性
onStop?: () => void;
......
stop() {
// 外部不管调用n次,节约性能调用一次即可
if (this.active) {
// 语义化抽离出功能, 清除依赖中的effect
cleanupEffect(this);
// 如果有onStop 就执行
if (this.onStop) this.onStop();
this.active = false;
}
}
}
以上,执行全部测试,全部通过。
总结
本章实现stop方法难点在于,什么时候该收集依赖,什么时候不应该收集依赖。 全局设置shouldTrack属性,用来监控是否应该收集依赖的; 调用 effect 中fn(也就是effect中的 run )就会 触发收集依赖操作,初始化的时候,先把shouldTrack置为 true,然后执行fn,fn中先触发收集依赖,然后赋值给 dummy,然后把fn作为返回值返回给runner,之后把全局属性shouldTrack 置为false。 obj.prop = 2,会重新执行fn,然后回重新给dummy 赋值,赋值过程中会重新收集依赖把 当前2收起来 后面执行 stop 会把当前effect实例从dep中删除,且停止依赖收集,后续dummy就还是2,直到重新执行runner,才被返回当前实际的值。
最后,希望能和每一位读者一起讨论问题,共同进步,详细代码全在这里 => git地址:mini_vue
如果对你有帮助的话,麻烦帮忙点下git star,感谢
转载自:https://juejin.cn/post/7206958960771432504