基于上一篇文章中实现的effect方法,根据 Vue3 源码中单测,完善该方法的三点功能,分别是:
- runner:
effect可以返回自执行的入参runner函数 - scheduler:
effect支持添加第二个参数选项中的scheduler功能 - stop:
effect添加stop功能
runner
单测
在effect.spec.ts文件中添加关于runner的测试用例。
it("should be return runner when call effect", () => {
let foo = 1;
const runner = effect(() => {
foo++;
return "foo";
});
expect(foo).toBe(2);
const r = runner();
expect(foo).toBe(3);
expect(r).toBe("foo");
});
上面测试用例的意思是,effect内部的函数会自执行一次,foo的值变成2。effect是一个可执行函数runner,执行runner时effect内部函数也会执行,因此foo的值会再次自增变成3,并且runner的返回值就是effect内部函数的返回值。
实现
effect函数需要可以返回它的入参执行函数,且内部执行函数可以返回。
class ReactiveEffect {
private _fn: any;
constructor(fn) {
this._fn = fn;
}
run() {
reactiveEffect = this;
return this._fn();
}
}
export function effect(fn) {
let _effect = new ReactiveEffect(fn);
_effect.run();
const runner = _effect.run.bind(_effect)
return runner;
}
需要注意的是,这里存在this指向的问题,在返回_effect.run函数时需要绑定当前实例。
验证
执行yarn test effect

scheduler
单测
it("scheduler", () => {
let dummy;
let run: any;
const scheduler = jest.fn(() => {
run = runner;
});
const obj = reactive({ foo: 1 });
const runner = effect(
() => {
dummy = obj.foo;
},
{
scheduler,
}
);
expect(scheduler).not.toHaveBeenCalled();
expect(dummy).toBe(1);
// should be called on first trigger
obj.foo++;
expect(scheduler).toHaveBeenCalled();
// should not run yet
expect(dummy).toBe(1);
// manually run
run();
// should have run
expect(dummy).toBe(2);
});
上面测试用例代码的意思是:effect方法接收第二个参数,是一个选项列表对象,其中有一个是scheduler,是一个函数。这里用jest.fn模拟了一个函数将变量run赋值成runner函数。在第一次执行的时候,scheduler函数不调用执行,effect的第一个参数函数自执行,所以dummy赋值为1;当响应式对象变化时,也就是obj.foo++时,scheduler会被执行,但是dummy的值还是1,说明第一个参数函数并没有执行;run执行,也就是effect返回函数runner执行时,第一个参数函数执行,因为obj.foo++,所以dummy变成2。
可以总结出scheduler包含的需求点:
- 通过
effect的第二个参数给定一个scheduler的fn effect第一次执行的时候,执行第一个参数function- 当响应式对象触发
set操作时,不会执行function,而执行scheduler - 当执行
runner时,会再次执行function
实现
首先是effect函数可以接收第二个对象参数。
export function effect(fn, options: any = {}) {
let _effect = new ReactiveEffect(fn, options.scheduler);
_effect.run();
const runner = _effect.run.bind(_effect)
return runner;
}
Class类中也要相应的接收scheduler
class ReactiveEffect {
private _fn: any;
public scheduler: Function | undefined;
constructor(fn, scheduler) {
this._fn = fn;
this.scheduler = scheduler;
}
run() {
reactiveEffect = this;
return this._fn();
}
}
当响应式对象触发set操作时,也就是触发依赖时,在trigger方法中,执行scheduler,只需要判断是否存在scheduler,存在即执行。
export function trigger(target, key) {
let depMap = targetMap.get(target);
let dep = depMap.get(key);
for (const effect of dep) {
if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
}
}
验证

stop
单测
import { effect, stop } from "../reactivity/effect";
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);
// stopped effect should still be manually callable
runner();
expect(dummy).toBe(3);
});
it("onStop", () => {
const onStop = jest.fn();
const runner = effect(() => {}, { onStop });
stop(runner);
expect(onStop).toHaveBeenCalled();
});
stop功能有两个测试用例,对应不同的功能,我们逐个分析。
"stop"中,effect内函数自执行一次,所以第一次断言dummy为上面赋值的2;执行stop方法,stop方法是来自effect对外暴露的方法,它接收runner函数作为参数,即便再更新响应式对象,effect内函数也不执行,dummy仍然是2;再次执行runner,恢复执行effect内函数,dummy变成了3。
总结来说,stop可以阻止effect内函数执行。
"onStop"中,effect函数接收第二个参数对象中有个属性是onStop,且接收一个函数,当执行stop时,onStop函数会被执行。
实现
触发依赖时,trigger方法中循环执行了dep中所有的effect内方法,那需要阻止执行,就可以从dep中删除该项。
首先stop方法接收runner函数作为参数。
export function stop(runner) {
runner.effect.stop();
}
在runner函数上挂载一个effect实例,就可以获取到 Class 类中定义的stop方法。
class ReactiveEffect {
private _fn: any;
public scheduler: Function | undefined;
constructor(fn, scheduler) {
this._fn = fn;
this.scheduler = scheduler;
}
run() {
reactiveEffect = this;
return this._fn();
}
stop() {}
}
export function effect(fn, options: any = {}) {
let _effect = new ReactiveEffect(fn, options.scheduler);
extend(_effect, options);
_effect.run();
const runner: any = _effect.run.bind(_effect);
runner.effect = _effect; // 挂载effect实例
return runner;
}
那如何从dep中删除需要阻止执行的一项呢?
在track方法中dep.add(reactiveEffect)建立了dep这个Set结构和effect实例的关系,但是在 Class 类中并没有实例和dep的映射关系,因此可以Class类中定义一个deps数组用来存放该实例的所有dep,在需要调用stop方法时将删除dep中的该effect实例方法。
class ReactiveEffect {
private _fn: any;
public scheduler: Function | undefined;
deps = [];
constructor(fn, scheduler) {
this._fn = fn;
this.scheduler = scheduler;
}
run() {
reactiveEffect = this;
return this._fn();
}
stop() {
this.deps.forEach((dep: any) => {
dep.delete(this);
});
}
}
export function track(target, key) {
...
dep.add(reactiveEffect);
reactiveEffect.deps.push(dep); // 存放deps
}
验证

优化
虽然单测通过了,但是代码是有优化空间的,我们来重构一下。
stop方法中逻辑可以抽离成一个单独函数。
class ReactiveEffect {
...
stop() {
cleanupEffect(this);
}
}
function cleanupEffect(effect) {
effect.deps.forEach((dep: any) => {
dep.delete(effect);
});
}
性能上的优化,当用户一直调用stop方法,会导致这儿一直无故循环遍历,因此可以设置一个标志位来判断是否已经调用过执行了删除操作。
class ReactiveEffect {
private _fn: any;
public scheduler: Function | undefined;
deps = [];
active = true;
constructor(fn, scheduler) {
this._fn = fn;
this.scheduler = scheduler;
}
run() {
reactiveEffect = this;
return this._fn();
}
stop() {
if (this.active) {
cleanupEffect(this);
this.active = false;
}
}
}
重构后需要再次执行单测,确保没有破坏功能。
实现
来实现stop的第二个功能onStop。
首先将onStop方法挂载effect实例上。
export function effect(fn, options: any = {}) {
let _effect = new ReactiveEffect(fn, options.scheduler);
_effect.onStop = options.onStop
_effect.run();
const runner: any = _effect.run.bind(_effect);
runner.effect = _effect;
return runner;
}
当执行stop时,onStop函数会被执行。
class ReactiveEffect {
private _fn: any;
public scheduler: Function | undefined;
deps = [];
active = true;
onStop?: () => void;
constructor(fn, scheduler) {
this._fn = fn;
this.scheduler = scheduler;
}
run() {
reactiveEffect = this;
return this._fn();
}
stop() {
if (this.active) {
cleanupEffect(this);
if (this.onStop) {
this.onStop();
}
this.active = false;
}
}
}
验证

优化
effect方法的第二个参数options可能存在很多选项,那每次都通过_effect.onStop = options.onStop挂载到实例上是不优雅的,因此可以抽离这块的逻辑,作为一个公共的方法。
在 src 下新建文件夹 shared,新建index.ts
export const extend = Object.assign;
那在effect中就可以使用extend方法更语义化表达。
export function effect(fn, options: any = {}) {
let _effect = new ReactiveEffect(fn, options.scheduler);
extend(_effect, options);
_effect.run();
const runner: any = _effect.run.bind(_effect);
runner.effect = _effect;
return runner;
}
重构完再次执行yarn test effect验证是否破坏功能。
验证
最后需要执行全部的单测,验证新增功能对原有代码是否有破坏,执行yarn test

在执行reactive单测时,出现了如上的报错,提示reactiveEffect可能是undefined不存在deps。
在reactive.spec.ts中只是单纯的测试了reactive的核心功能,此时还没有涉及到effect方法,reactiveEffect的赋值是在effect自执行时触发的,因此是初始undefined状态。
export function track(target, key) {
...
if (!reactiveEffect) return; // 边界处理
dep.add(reactiveEffect);
reactiveEffect.deps.push(dep);
}
最后再次验证,测试通过,功能完善成功。
2023/11/13更新
修改stop单测
在原本的基础上,修改effect中stop测试用例。
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;
obj.prop++;
expect(dummy).toBe(2);
// stopped effect should still be manually callable
runner();
expect(dummy).toBe(3);
});
运行单测yarn test effect

报错分析
简单分析一下报错的原因。
obj.prop++可以理解成obj.prop = obj.prop + 1,存在get和set两个操作,触发get操作会重新收集依赖,导致stop中cleanupEffect方法删除所有effect失效。
实现
知道了根本原因是先触发get操作重新执行了effect中函数,也就是调用了track方法,那需要完善的逻辑应该这个方法入手。我们可以定义一个全局变量shouldTrack来判断是否需要进行track操作。
let reactiveEffect;
let shouldTrack; // 定义
export function track(target, key) {
...
if(!shouldTrack) return // 直接return不进行依赖收集
if (!reactiveEffect) return;
dep.add(reactiveEffect);
reactiveEffect.deps.push(dep);
}
进行赋值的时候触发set操作,执行trigger函数,最终调用的是 Class 类ReactiveEffect中run方法。run方法中原本是直接返回了入参函数的执行结果,这里就需要判断一下stop的情况,可以依据active来判断。
如果是调用了stop方法之后,active赋值为false,这时候直接返回fn;
如果没有调用stop方法,先将shouldTrack设为true,表示可以进行track调用,然后执行fn,并将执行结果返回,但是在返回之前需要重置操作,将shouldTrack设置成false,因为如果在遇到stop之后,run函数中会直接return,不会将shouldTrack设为true,那在track时,就会走!shouldTrack直接return不再收集依赖。
run() {
if (!this.active) {
return this._fn();
}
shouldTrack = true;
reactiveEffect = this;
const result = this._fn();
shouldTrack = false;
return result;
}
重构
track中shouldTrack和reactiveEffect的边界判断,可以提到track函数体内顶部,单独封装一个函数合成这两个判断。
依赖收集这儿可以优化的点,当dep中存在的reactiveEffect就不再重复收集了。
export function track(target, key) {
if (!isTracking()) return;
...
if (dep.has(reactiveEffect)) return;
dep.add(reactiveEffect);
reactiveEffect.deps.push(dep);
}
function isTracking() {
return shouldTrack && reactiveEffect !== undefined;
}
调试
修改一下单测,更简单的单测来通过调试清晰看一下上述流程。
it("stop", () => {
let dummy;
const obj = reactive({ prop: 1 });
const runner = effect(() => {
dummy = obj.prop;
});
stop(runner);
obj.prop++;
expect(dummy).toBe(1);
});
这里通过一个视频讲解来更形象的了解,视频详情查看
Vue3Effect方法增强:runner,scheduler&stop功能实现与测试
文章详细介绍了如何根据Vue3源码完善effect方法,包括添加runner函数返回、scheduler调度器和stop功能,通过测试用例验证这些新功能。
1677

被折叠的 条评论
为什么被折叠?



