我们先来实现一下 runner runner是 effect 的功能 调用effect 之后 会返回一个 runner 当我们再去调用这个 runner 函数的时候 他还是 会去继续执行这个这个effect 我们先看一下 runner的 test 而且 如果我们又返回值的话 我们调用函数 回去执行这个effect 而且 会返回 effect 返回的值
it("show return runner when call effect", () => {
let foo = 10;
const runner = effect(() => {
foo++;
return "foo";
});
expect(foo).toBe(11);
const r = runner();
expect(foo).toBe(12);
expect(r).toBe("foo");
});
我们 调用 runner 的时候 就是把effect 中的fn 再次调用一边 我们就可以直接 返回 一个调用的run方法 但是我们当前 effect实例需要 传一个this 所以我们需要显示绑定this 保证我们返回的是 当前的 那个effect
export function effect(fn, options: any = {}) {
const _effect = new ReactiveEffect(fn, options.scheduler);
const runner = _effect.run.bind(_effect);
return runner;
}
而且我们在调用 用户 的 fn之后 需要返回当前实例的 值 所以我们可以直接 在run方法中直接返回
class ReactiveEffect {
private _fn:any;
constructor(fn) {
this._fn = fn
}
run() {
activeEffect = this
return this._fn()
}
}
我们接下来 实现 sechduler api 同样我们先来看单元测试
test("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);
obj.foo++;
expect(scheduler).toHaveBeenCalledTimes(1);
expect(dummy).toBe(1);
run();
expect(dummy).toBe(2);
});
effect函数中传递的第二个参数sechduler 一开始不回去被调用 默认 effect 中的fn 会被调用一次 所以第一次 我们可以直接 拿到 dummy 等于一 然后 当响应式 的 值 发生改变的时候 我们才回去调用这个sechduler 然后我们再去执行 run方法 调用runner dummy 等于二 重点就在于 我们 set 触发依赖的时候 我们如果判断又sechduler 那么我们就会去执行 sechduler
export function triggerEffects(dep) {
for (const effect of dep) {
if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
}
}
我们的目标就是 在我们 tigger的时候 我们就去执行这个 sechduler 那么我们如何 能让 effect 中拿到这个sechduler呢
export class ReactiveEffect {
private _fn;
deps = [];
onStop?: () => void;
active = true;
constructor(fn, public scheduler?) {
this._fn = fn;
this.scheduler = scheduler;
}
run() {
if (!this.active) {
return this._fn();
}
shouldTrack = true;
activeEffect = this;
const result = this._fn();
shouldTrack = false;
return result;
}
}
我们可以在effect 类中定义一个 public 的 scheduler 就可以直接获取了 到时候 就是 我们在传递fn的时候 多加一个参数 就是 option.scheduler
const _effect = new ReactiveEffect(fn, options.scheduler);
接下来我们去写 stop , onStop 功能 首先我们先看stop 的单元测试
1. 我们的 reactive 内部存在一个容器 deps 我们的effect 是 传入fn 我们把 fn存在了deps
2. 在我们触发set操作的时候就会把 deps里面所有的 effect 中的fn 拿来全部执行
3. 那么我们如何 让 调用stop的时候 不再去 调用整个 fn 呢 我们就可以 删除 整个dep
test("stop", () => {
let dummy;
const obj = reactive({ prop: 1 });
const runner = effect(() => {
dummy = obj.prop;
});
obj.prop = 2;
stop(runner);
obj.prop++;
expect(dummy).toBe(2);
});
我们实现stop 函数 里面传递的参数 就是 runner 函数的返回值 那么我们 如何 给 runner 添加 stop 方法呢 我们可以 给 runner传递一个 effect这样就能拿到 当前实例对象 然后去声明一个 stop 方法 执行
export function stop(runner) {
return runner._effect.stop();
}
export function effect(fn, options: any = {}) {
const _effect = new ReactiveEffect(fn, options.scheduler);
extend(_effect, options);
_effect.run();
const runner = _effect.run.bind(_effect);
runner._effect = _effect;
return runner;
}
然后我们实现stop方法 我们需要获取 当前 runner 对应的这个effect 才能 对应删除 我们如何 获取呢 我们实例dep 实在 trigger 的时候 添加的 那么 我们新建一个 数组 也同样保存一下
export class ReactiveEffect {
private _fn;
deps = [];
onStop?: () => void;
active = true;
constructor(fn, public scheduler?) {
this._fn = fn;
this.scheduler = scheduler;
}
run() {
if (!this.active) {
return this._fn();
}
shouldTrack = true;
activeEffect = this;
const result = this._fn();
shouldTrack = false;
return result;
}
stop() {
cleanupeffect(this)
}
}
}
function cleanupEffect(currentEffect) {
currentEffect.deps.forEach((activeEffect) => {
activeEffect.delete(currentEffect);
});
currentEffect.deps.length = 0;
}
我们现在就是传入了 一个 cleaneffect 方法 传入一个 参数 代表 当前effect实例fn 我们只需要每次他在调用stop的时候 我们就可以在trigger的时候删除 这个effect 但是我们可以想一下 如果我们 频繁的调用stop 那么他每次都会区调用这个cleanupeffect函数 每次都会去循环 但是 其实 当前 实例 已经在第一次的时候 已经被删除了 所以我们可以 用一个变量来判断当前我们是否需要继续 去 调用这个stop里面的 clleanupeffect函数 我们 直接定义一个 类的变量 active
stop() {
if (this.active) {
cleanupEffect(this);
if (this.onStop) {
this.onStop();
}
this.active = false;
}
}