使用TDD(测试驱动开发)的方式手写Vue,一起学习Vue3源码,感谢@阿崔cxr。
基本的 Reactive & Effect
先提供单元测试:
describe('reactive', () => {
it('happy path', () => {
const user = { age: 10 };
const reactiveUser = reactive(user);
// 原对象和套了reactived的对象不是同一个
expect(reactiveUser).not.toBe(user);
// 被reactive包裹的对象,能访问到原对象上的属性
expect(reactiveUser.age).toBe(10);
});
});
这里的原理比较简单,大家也都清楚,用了Proxy。然后需要重写getter和setter,其中在getter中做依赖收集,在setter中做依赖触发。
export function reactive(target) {
return new Proxy(target, {
get(target, key) {
// 依赖收集
track(target, key);
return Reflect.get(target, key);
},
set(target, key, value) {
// 依赖触发
const res = Reflect.set(target, key, value);
trigger(target, key);
return res;
}
});
}
在实现依赖收集和触发之前,先写好单元测试:
describe('effect', () => {
it('happy path', () => {
const user = reactive({ age: 10 });
let nextAge;
effect(() => {
nextAge = user.age + 1;
console.log(nextAge);
});
// effect中的fn先执行一次
expect(nextAge).toBe(11);
user.age++;
// age自增的同时,effect又调用一次
expect(nextAge).toBe(12);
});
})
需要先实现effect函数,目前它暂时只接受fn一个参数,表示回调函数。根据测试的逻辑,effect中需要调用一次fn。
对于reactive响应式对象,如果在effect的回调函数中使用到它的属性,那么getter中需要做依赖收集。我们维护全局变量targetMap,键为target,值为depsMap。而depsMap的键为key,值为dep,dep是集合类型。
假设A是响应式对象,从targetMap可以取出A的depsMap。depsMap的key是A的各个属性,value是保存了依赖该属性的effect集合。
全局变量activeEffect表示当前活跃的effect,在执行run()时就已经把activeEffect设置为当前的effect,之后运行回调函数时,可能访问了响应式对象上的属性,于是进入getter中的依赖收集逻辑,执行到track()函数,再使用activeEffect。
let activeEffect;
export function effect(fn) {
// 创建effect
const _effect = new ReactiveEffect(fn);
// 执行回调
_effect.run();
}
class ReactiveEffect {
private _fn: Function;
constructor(fn) {
this._fn = fn;
}
run() {
// 重要!
activeEffect = this;
// 执行回调
this._fn();
}
}
export function track(target, key) {
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);
}
// 直接使用activeEffect
dep.add(activeEffect);
}
export function trigger(target, key) {
const depsMap = targetMap.get(target);
const dep = depsMap.get(key);
dep.forEach((effect) => {
// 运行依赖该属性的所有回调函数
effect.run();
});
}
Effect 返回 Runner
单元测试:
it('should return runner when call effect', () => {
let foo = 10;
// runner可以拿到回调函数
const runner = effect(() => {
foo++;
return foo + 10;
});
expect(foo).toBe(11);
const returnValue = runner();
expect(foo).toBe(12);
// 执行runner也能获取到回调函数的返回值
expect(returnValue).toBe(22);
});
可见,effect需要返回回调函数并赋值给runner,且runner要返回和回调函数一样的值。稍加修改如下:
class ReactiveEffect {
// ...
run() {
activeEffect = this;
// 返回回调函数
return this._fn();
}
}
export function effect(fn, options: effectOptions = {}) {
const _effect = new ReactiveEffect(fn, scheduler, onStop);
_effect.run();
// 因为上面用到this,所以要用bind绑定this
const runner: any = _effect.run.bind(_effect);
// 返回回调函数
return runner;
}
实现 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).toHaveBeenCalledTimes(1);
// should not run yet
expect(dummy).toBe(1);
// manually run
run();
// should have run
expect(dummy).toBe(2);
});
即,effect可以接受一个options参数,options里面有很多属性,其中包括了Scheduler,它也是一个回调函数。当提供Scheduler时,effect的回调函数(第一个参数)立刻执行一次,但是Scheduler不执行。响应式对象触发依赖时,执行的是Scheduler。
使用Scheduler的目的是回调不立即执行,后续用来实现异步更新。
代码修改如下:
class ReactiveEffect {
public scheduler: Function | undefined;
constructor(fn, scheduler?) {
this.scheduler = scheduler;
}
// ...
}
export function effect(fn, options: effectOptions = {}) {
const { scheduler } = options;
const _effect = new ReactiveEffect(fn, scheduler, onStop);
// 第一次还是执行fn回调
_effect.run();
// ...
}
export function trigger(target, key) {
const depsMap = targetMap.get(target);
const dep = depsMap.get(key);
dep.forEach((effect) => {
if (!effect.scheduler) {
effect.run();
} else {
// 依赖触发的时候执行Scheduler
effect.scheduler();
}
});
}
实现 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++
// obj.prop++;
expect(dummy).toBe(2);
// stopped effect should still be manually callable
runner();
expect(dummy).toBe(3);
});
it('onStop', () => {
const obj = reactive({
foo: 1
});
const onStop = jest.fn(); // 在stop时会执行
let dummy;
const runner = effect(
() => {
dummy = obj.foo;
},
{
onStop
}
);
// 响应式对象改变后,停止运行回调(fn和scheduler)
// 即:effect从该属性的dep集合中移除
// 这样就无法通过effect.run()或effect.scheduler()执行回调了
stop(runner);
// stop时触发onStop回调
expect(onStop).toBeCalledTimes(1);
});
主要是在effect中保存deps,在runner中保存effect。调用stop时,首先从runner中获取到effect,然后调用effect类的stop()方法,然后进入cleanEffect,逻辑是遍历deps,把effect从每个集合中删除。
class ReactiveEffect {
// 在effect中保存deps
// 该effect的回调函数中都访问了响应式对象的哪些属性,把这些属性对应的dep集合加入deps
deps = [];
active = true;
onStop: Function | undefined;
constructor(fn, scheduler?, onStop?) {
this.onStop = onStop;
}
stop() {
// 如果多次调用stop可能引起性能问题,使用active保证只成功调用一次
if (this.active) {
cleanEffect(this);
if (this.onStop) {
// 执行onStop回调
this.onStop();
}
}
this.active = false;
}
}
function cleanEffect(effect) {
// 遍历deps,将effect从中移除
effect.deps.forEach((dep: Set<ReactiveEffect>) => {
dep.delete(effect);
});
}
export function stop(runner) {
runner.effect.stop();
}
export function effect(fn, options: effectOptions = {}) {
const { scheduler, onStop } = options;
const _effect = new ReactiveEffect(fn, scheduler, onStop);
// 把effect作为runner的属性保存,从而能在stop函数中获取effect
runner.effect = _effect;
}
export function track(target, key) {
// ...
dep.add(activeEffect);
// 如果没有调用过effect函数,activeEffect为undefined,访问其deps属性会报错
if (!activeEffect) return;
activeEffect.deps.push(dep);
}