【手写Vue3】实现effect & reactive 依赖收集与触发
前言
这是「手写Vue3」系列第2篇,前面的系列直达链接如下:
在这一篇,我们将实现Vue3最经典依赖收集,依赖触发。
增加测试用例
在上一篇中,我们为了测试jest
的功能加了一个index.spec.ts
,现在可以删掉这个文件了,开始写真实的测试用例。
reactive测试
首先,我们创建一个reactive.spec.ts
的单测文件,来测试reactive
的功能,其实就是测试调用reactive
能够创建一个响应式对象,代码如下:
// reactive.spec.ts
describe('reactive', () => {
it('happy path', () => {
// 创建一个初始对象
const original = { foo: 1 };
// 创建一个响应式对象
const observed = reactive(original);
// 断言二者并不相等
expect(observed).not.toBe(original);
// 断言响应式对象里的属性是正确的
expect(observed.foo).toBe(1);
});
});
effect测试
接着,我们创建一个effect.spec.ts
的单测文件,来测试effect
的功能,其实就是测试effect
能够做到响应式更新,代码如下:
// effect.spec.ts
describe('effect', () => {
it('happy path', () => {
// 创建一个响应式对象
const user = reactive({
age: 23,
});
let nextAge;
effect(() => {
nextAge = user.age + 1;
});
// 断言nextAge值的初始化
expect(nextAge).toBe(24);
// 更新响应式对象
user.age++;
// 断言nextAge也随着更新了
expect(nextAge).toBe(25);
});
});
跑通测试用例
在上面的reactive.spec.ts
中以及effect.spec.ts
中,其实reactive
和effect
是报错的,因为我们还没有实现和引入reactive
和effect
。接下来,我们就来实现一下。
首先,我们知道,reactive
是一个函数,它接收一个对象,返回一个Proxy
,我们现在在src/reactivity
下增加一个文件reactive.ts
:
export const reactive = row => {
return new Proxy(row, {
get(target, key) {
const result = Reflect.get(target, key);
// TODO: 依赖收集
return result;
},
set(target, key, value) {
const result = Reflect.set(target, key, value);
// TODO: 触发依赖
return result;
}
})
}
那么,我们就写了一个最基础版本的reactive
的实现(其实还并没有实现),然后再reactive.spec.ts
中引入:
// 增加引入
import { reactive } from './../reactive';
describe('reactive', () => {
/// ...
});
这个时候,我们测试一下reactive.spec.ts
,运行:
npm run test reactive
这个时候,测试用例应该是通过的。
接下来,我们创建一个effect.ts
,我们知道,effect
是一个函数,同时接收的参数也是一个函数,并且会马上执行这个参数函数。我们简单实现一下:
class ReactiveEffect {
private _fn: any;
constructor(fn) {
this._fn = fn;
}
run() {
return this._fn();
}
}
export function effect(fn) {
const _effect = new ReactiveEffect(fn);
_effect.run();
}
这个时候,我们再在effect.spec.ts
中引入:
import { reactive } from "./../reactive";
import { effect } from './../effect';
describe('effect', () => {
/// ...
});
这个时候,我们运行npm run test
发现测试结果如下:
从结果可以看到,effect.spec.ts
在最后失败了,原因是没有响应式更新,那是因为我们还没有彻底实现reactive
和effect
,但是架子已经搭好了,接下来,我们去实现它们。
实现收集依赖
首先,我们理清思路:
所有的effect(cb)
中的cb
我们都转换成了一个ReactiveEffect
对象,我们需要把这些对象收集起来,那在什么时候搜集呢,因为这个cb
会立即执行,在执行cb
的时候,当调用reactive
构造的响应式对象的get
的时候,我们就去收集。根据这个对象(target
和key
)去「收集池」中找这个ReactiveEffect
对象,如果没有,就收集起来,如果有,就跳过。
首先,我们改造一下reactive.ts
:
import { track } from "./effect";
export const reactive = row => {
return new Proxy(row, {
get(target, key) {
const result = Reflect.get(target, key);
// 依赖收集
track(target, key);
return result;
},
set(target, key, value) {
const result = Reflect.set(target, key, value);
// TODO: 触发依赖
return result;
}
})
}
接着,我们在effect.ts
中增加track
方法,来收集依赖:
class ReactiveEffect {
private _fn: any;
constructor(fn) {
this._fn = fn;
}
run() {
// 在调用的时候,把activeEffect设置成当前的ReactiveEffect
activeEffect = this;
return this._fn();
}
}
// 存储所有target的数据池
// target(这个对象的数据池) -> key(属于这个对象的某个属性的数据池)
const targetMap = new Map();
// 当前的ReactiveEffect对象
let activeEffect;
// 收集依赖
export function track(target, key) {
// 根据target查找
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 根据key在target的数据池中查找
let dep = depsMap.get(key);
if (!dep) {
// 用set存储,因为不需要重复的
dep = new Set();
depsMap.set(key, dep);
}
// 把当前的执行的cb(也就是当前的ReactiveEffect)加入数据池
dep.add(activeEffect);
}
export function effect(fn) {
const _effect = new ReactiveEffect(fn);
_effect.run();
}
那么现在,我们依赖就收集好了。
根据这个收集依赖的逻辑回顾一下测试用例:
- 在执行
effect
的时候,马上执行里面的cb
,也就是nextAge = user.age + 1
,执行之前先把activeEffect
设置成了这个cb
对应的ReactiveEffect
。 - 接着开始执行
nextAge = user.age + 1
,执行的过程中调用了user.age
,user
是一个reactive
,所以走到了user
的get
方法,get
方法触发了track
去收集依赖。 - 收集依赖的时候,我们去
targetMap
数据池根据user
这个对象和age
这个属性去查找是否有对应的dep
,因为是第一次进来,发现没有,所以我们dep = new Set()
并且把这个cb
对应的ReactiveEffect
放入了这个set
。
至此,我们依赖就收集成功了。
实现依赖触发
我们还是先理清一下思路:
在什么时候去触发依赖呢?显然应该是在user.age++
的时候,也就是在reactive
对应的属性发生改变的时候,这个属性对应的依赖应该被触发。
现在,我们改造下reactive.ts
:
import { track, trigger } from "./effect";
export const reactive = row => {
return new Proxy(row, {
get(target, key) {
/// ...
},
set(target, key, value) {
const result = Reflect.set(target, key, value);
// 触发依赖
trigger(target, key);
return result;
}
})
}
接着,我们在effect.ts
中增加trigger
方法,来触发依赖:
class ReactiveEffect {
/// ...
}
/// ...
// 触发依赖
export function trigger(target, key) {
const depsMap = targetMap.get(target);
const dep = depsMap.get(key);
// 找到这个target->key对应的dep,它是一个ReactiveEffect的集合,遍历触发即可
for(let effect of dep) {
effect.run();
}
};
export function effect(fn) {
/// ...
}
触发依赖的逻辑很简单,就不多解释了,那么现在,我们整个的依赖收集和依赖触发就完全搞定了。
再跑测试用例
现在,我们再跑一次测试用例,运行npm run test
,发现测试用例运行全部成功了。
实现effect
返回值
其实,effect
函数也是有返回值的,它的返回值就是callback
函数return
的数据,并且当为runner
命名时,再次执行,会再次调用这个callback
,所以我们为effect.spec.ts
再增加一条测试用例如下:
import { reactive } from "./../reactive";
import { effect } from './../effect';
describe('effect', () => {
it('happy path', () => {
/// ...
});
it('effect should return runner', () => {
let foo = 10;
// 为effect定义变量
const runner = effect(() => {
foo++;
return 'foo';
});
// 断言会立即执行 >> 已经实现了
expect(foo).toBe(11);
// 接受返回值
const res = runner();
// 断言运行了runner()会再次执行cb
expect(foo).toBe(12);
// 断言返回值
expect(res).toBe('foo');
});
});
现在,我们实现一下,其实很简单,只需要在effect
函数把cb
返回出去并且绑定this
为当前的ReactiveEffect
即可,修改effect.ts
:
/// ...
export function effect(fn) {
/// ...
return _effect.run.bind(_effect);
}
再次执行测试用例,发现成功了。
附上完整代码
完整代码如下:
// effect.ts
class ReactiveEffect {
private _fn: any;
constructor(fn) {
this._fn = fn;
}
run() {
// 在调用的时候,把activeEffect设置成当前的ReactiveEffect
activeEffect = this;
return this._fn();
}
}
// 存储所有target的数据池
// target(这个对象的数据池) -> key(属于这个对象的某个属性的数据池)
const targetMap = new Map();
// 当前的ReactiveEffect对象
let activeEffect;
// 收集依赖
export function track(target, key) {
// 根据target查找
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 根据key在target的数据池中查找
let dep = depsMap.get(key);
if (!dep) {
// 用set存储,因为不需要重复的
dep = new Set();
depsMap.set(key, dep);
}
// 把当前的执行的cb(也就是当前的ReactiveEffect)加入数据池
dep.add(activeEffect);
}
// 触发依赖
export function trigger(target, key) {
const depsMap = targetMap.get(target);
const dep = depsMap.get(key);
// 找到这个target->key对应的dep,它是一个ReactiveEffect的集合,遍历触发即可
for(let effect of dep) {
effect.run();
}
};
export function effect(fn) {
const _effect = new ReactiveEffect(fn);
_effect.run();
return _effect.run.bind(_effect);
}
// reactive.ts
import { track, trigger } from "./effect";
export const reactive = row => {
return new Proxy(row, {
get(target, key) {
const result = Reflect.get(target, key);
// 依赖收集
track(target, key);
return result;
},
set(target, key, value) {
const result = Reflect.set(target, key, value);
// 触发依赖
trigger(target, key);
return result;
}
})
}
总结
自此,effect & reactive
依赖收集与触发都已经实现完成了,当然,effect
还有scheduler
,stop
等等功能,我们下期再聊!!!
转载自:https://juejin.cn/post/7249529167397748793