前言
上一篇文章讲了 effect 和响应式数据的联系,那么在本篇中,会继续完善 effect 。
1、嵌套的effect
effect 是可嵌套的,为什么设计成可嵌套的模式呢,就拿vue的渲染函数来说,它就是在一个effect中执行的。
const Foo = {
render() {
return //...
}
}
const Bar = {
render() {
return <Foo />
}
}
// 单独的一个组件渲染
effect(() => {
Foo.render();
})
// 如果是嵌套组件渲染
effect(() => {
Bar.render();
effect(() => {
Foo.render();
})
})
但是很明显,目前 effect 的设计是不满足嵌套 effect 的功能的。
it("should allow nested effects", () => {
const obj = reactive({
foo: true,
bar: true,
});
let tmp1, tmp2;
const effectFn2 = jest.fn(() => {
console.log("fn2 trigger--");
tmp2 = obj.bar;
});
const effectFn1 = jest.fn(() => {
console.log("fn1 trigger--");
effect(effectFn2);
tmp1 = obj.foo;
});
effect(effectFn1);
expect(effectFn1).toHaveBeenCalledTimes(1);
expect(effectFn2).toHaveBeenCalledTimes(1);
obj.foo = false;
// 我们的期望是 foo的更新 会执行 effectFn1,但是实际的情况并非这样
expect(effectFn1).toHaveBeenCalledTimes(2);
});
但是实际上,foo 触发到了 effectFn2,这是怎么回事。
稍微分析大概就知道,执行完 effectFn2 之后, activeEffect 就指向了 effectFn2,当执行 tmp1 = obj.foo; 这句的时候,依赖收集已经就错了。
现在问题就很明确了:
同一个时刻 activeEffect 所储存的副作用函数只能有一个,当副作用函数发生嵌套的时候,内存副作用函数的执行会覆盖 activeEffect 的值,并且永远不会恢复到原来的值。
那么如何解决?
我们需要一个栈来存储副作用函数,副作用执行时,将副作用压进栈中,执行完毕后出栈,并始终让 activeEffect 指向栈顶副作用。这样就能做到一个响应式数据只会读取其值的副作用函数,而不会出现相互影响的情况。
class ReactiveEffect {
// ...
run() {
cleanupEffect(this);
activeEffect = this;
effectStack.push(this);
this._fn();
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
}
let activeEffect: ReactiveEffect | null = null;
// effect 栈
const effectStack: ReactiveEffect[] = [];
2、避免无限递归循环
目前,我们所设计的 effect 其实有一个比较严重的问题:
it("should avoid implicit infinite recursive loops with itself", () => {
const counter = reactive({ num: 0 });
const counterSpy = jest.fn(() => counter.num++);
effect(counterSpy);
expect(counter.num).toBe(1);
expect(counterSpy).toHaveBeenCalledTimes(1);
counter.num = 4;
expect(counter.num).toBe(5);
expect(counterSpy).toHaveBeenCalledTimes(2);
});
可以看到,我们执行 counter.num++ 这个命令的时候引起栈溢出,这是什么原因导致的呢,我们来分析下。
首先 counter.num++ 可以被拆分成 counter.num = counter.num + 1
这段程序既会读取 counter.num 的值,又会设置 counter.num 的值
还记得我们读取的时候会调用 track,设置的时候调用 trigger 吗
export function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
dep.add(activeEffect);
// dep就是一个与当前副作用函数存在联系的依赖集合
activeEffect.deps.push(dep);
}
export function trigger(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) return;
let dep = depsMap.get(key);
// 为什么需要新建一个Set
// 因为用dep循环的话,run -> cleanEffect清除掉函数 -> fn又将函数加回来 -> 死循环
const depToRun = new Set(dep);
depToRun.forEach((e) => e.run());
// dep && dep.forEach((e) => e.run());
}
注意到,我们访问 counter.num 使副作用被收集到 deps,然后又立刻设置 counter.num 使其取出 deps 中的副作用函数调用。但问题是我们的 副作用函数还没执行完,又被再次调用,这样无限的递归调用自己导致栈溢出。
那么如何解决?
很简单,如果trigger触发的副作用函数和当前执行的副作用函数相同,则不触发执行。
export function trigger(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) return;
let dep = depsMap.get(key);
// 为什么需要新建一个Set
// 因为用dep循环的话,run -> cleanEffect清除掉函数 -> fn又将函数加回来 -> 死循环
// 改:const depToRun = new Set(dep);
const depToRun = new Set<ReactiveEffect>();
// 新增:
dep &&
dep.forEach((e) => {
if (e !== activeEffect) {
depToRun.add(e);
}
});
depToRun.forEach((e) => e.run());
// dep && dep.forEach((e) => e.run());
}
3、调度执行
可调度指的是当
trigger动作触发副作用函数重新执行时,有能力决定副作用执行的时机、次数和方式。
先来看下测试用例:
it("scheduler", () => {
let dummy;
let run: any;
const scheduler = jest.fn(() => {
run = runner;
});
const obj = reactive({ foo: 1 });
// 1--
const runner = effect(
() => {
dummy = obj.foo;
},
{ scheduler }
);
// 2--
expect(scheduler).not.toHaveBeenCalled();
expect(dummy).toBe(1);
// 3--
// should be called on first trigger
obj.foo++;
expect(scheduler).toHaveBeenCalledTimes(1);
// should not run yet
expect(dummy).toBe(1);
// 4--
// manually run
run();
// should have run
expect(dummy).toBe(2);
});
可以观察到几件事:
effect会返回一个课执行函数;并且能传入配置,配置项scheduler是一个函数scheduler一开始不会被触发,一开始被触发的是副作用函数- 依赖触发之后,不再执行副作用函数,转而执行
scheduler effect返回的函数可以手动调用,且该函数与副作用函数等价
针对以上几点,我们来完善effect:
export interface ReactiveEffectOptions {
lazy?: boolean;
scheduler?: EffectScheduler;
}
export interface ReactiveEffectRunner<T = any> {
(): T;
effect: ReactiveEffect;
}
export function effect<T = any>(fn: () => T, options?: ReactiveEffectOptions) {
const _effect = new ReactiveEffect(fn);
// 新增:将scheduler合并到实例_effect中去,后续trigger触发的时候调用scheduler
if (options) {
Object.assign(_effect, options);
}
_effect.run();
// 新增:将run方法返回出去,满足手动调用副作用的功能
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner;
runner.effect = _effect;
return runner;
}
export function trigger(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) return;
let dep = depsMap.get(key);
const depToRun = new Set<ReactiveEffect>();
dep &&
dep.forEach((e) => {
if (e !== activeEffect) {
depToRun.add(e);
}
});
// 改:depToRun.forEach((e) => e.run());
depToRun.forEach((e) => {
// 新增:有scheduler的时候调用scheduler
if (e.scheduler) {
e.scheduler();
} else {
e.run();
}
});
}
总结
- 实现了可嵌套的
effect - 修复了自增时,
effect无限递归调用的bug - 实现了
effect的调度器