基于上一篇文章中实现的effect
方法,根据 Vue3 源码中单测,完善该方法的三点功能,分别是:
- runner :
effect
可以返回自执行的入参runner
函数 - scheduler :
effect
支持添加第二个参数选项中的scheduler
功能 - stop :
effect
添加stop
功能
runner
单测
在effect.spec.ts
文件中添加关于runner
的测试用例。
ts
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
函数需要可以返回它的入参执行函数,且内部执行函数可以返回。
ts
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
单测
ts
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
函数可以接收第二个对象参数。
ts
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
ts
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
,存在即执行。
ts
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
单测
ts
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
函数作为参数。
ts
export function stop(runner) {
runner.effect.stop();
}
在runner
函数上挂载一个effect
实例,就可以获取到 Class 类中定义的stop
方法。
ts
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
实例方法。
ts
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
方法中逻辑可以抽离成一个单独函数。
ts
class ReactiveEffect {
...
stop() {
cleanupEffect(this);
}
}
function cleanupEffect(effect) {
effect.deps.forEach((dep: any) => {
dep.delete(effect);
});
}
性能上的优化,当用户一直调用stop方法,会导致这儿一直无故循环遍历,因此可以设置一个标志位来判断是否已经调用过执行了删除操作。
ts
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
实例上。
ts
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
函数会被执行。
ts
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
ts
export const extend = Object.assign;
那在effect
中就可以使用extend
方法更语义化表达。
ts
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
状态。
ts
export function track(target, key) {
...
if (!reactiveEffect) return; // 边界处理
dep.add(reactiveEffect);
reactiveEffect.deps.push(dep);
}
最后再次验证,测试通过,功能完善成功。