effect,reactive(依赖收集 触发依赖)
现在,我们开始写第一个测试。可以把之前初始化项目使用的测试文件给删掉 src\reactivity\tests\index.spec.ts。
新建文件 src\reactivity\tests\effect.spec.ts
先放入核心测试流程
describe("effect", () => {
// 核心代码逻辑
it("happy path", () => {
const user = reactive({
age: 10,
});
let nextAge;
// user.age:依赖收集
// effect一上来会直接调用fn,然后会触发 user.age 的get操作,触发get时进行依赖收集
effect(() => {
nextAge = user.age + 1;
});
expect(nextAge).toBe(11);
// 更新:触发依赖
// 触发依赖:user.age触发set操作时,会把所有收集到的fn拿出来调用一下
user.age++;
expect(nextAge).toBe(12);
});
});
可以看出这个测试代码里面实际上会涉及到两部分内容,一部分是 reactive,一部分是 effect,这里可以把它拆开,分步骤实现。这里体现了任务拆分的思想。
1、reactive
先来编写 reactive 的测试用例:src\reactivity\tests\reactive.spec.ts
describe("reactive", () => {
it("happy path", () => {
const original = { foo: 1 };
const observed = reactive(original);
// 他俩绝对是不相等的
expect(observed).not.toBe(original);
// observed.foo 应该是 original.foo 的值
expect(observed.foo).toBe(1);
});
});
测试用例写好了,我们来实现 reactive
在此之前,我们应该先在 tsconfig.json 中修改 lib,"lib": ["DOM", "ES6"]。否则 Proxy 会报错
新建文件 src\reactivity\reactive.ts
export function reactive(raw) {
// reactive其实就是 Proxy 的代理
return new Proxy(raw, {
get(target, key) {
const res = Reflect.get(target, key)
// TODO 依赖收集
return res;
},
set(target, key, value) {
const res = Reflect.set(target, key, value);
// TODO 触发依赖
return res;
}
})
}
reactive 本质上就是通过 Proxy 来做代理,去拦截;这样我们就知道什么时候触发 get 、set了。
我们先不做依赖收集和触发依赖。
此时,控制台输入命令:yarn test reactive,即可看到 reactive 的测试用例通过了。
2、effect
新建文件 src\reactivity\effect.ts
class ReactiveEffect {
private _fn: any;
constructor(fn) {
this._fn = fn;
}
run() {
this._fn();
}
}
export function effect(fn) {
const _effect = new ReactiveEffect(fn);
// 当调用effect的时候,直接执行内部的fn(封装在run方法中)
_effect.run();
}
effect函数接收一个fn,一上来就需要调用一下fn- 抽离出
ReactiveEffect:面向对象的思想,用一个类来表示
- 抽离出
- 直接执行内部的
run方法run方法中是之前保存的fn函数
此时注释掉 effect 单测中的更新
describe("effect", () => {
it("happy path", () => {
const user = reactive({
age: 10,
});
let nextAge;
effect(() => {
nextAge = user.age + 1;
});
expect(nextAge).toBe(11);
// 更新:触发依赖
// 触发依赖:user.age触发set操作时,会把所有收集到的fn拿出来调用一下
// user.age++;
// expect(nextAge).toBe(12);
});
});
再执行测试命令:yarn test。可以看到测试是通过的。通过后我们再把这两行测试代码打开。
3、依赖收集-触发依赖
我们看回 reactive.ts 文件,之前设置了两个 TODO,将其修改为 track、trigger,这两个函数放在 effect.ts 中
// src/reactivity/reactive.ts
export function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
const res = Reflect.get(target, key);
// 依赖收集
track(target, key);
return res;
},
set(target, key, value) {
const res = Reflect.set(target, key, value);
// 触发依赖
trigger(target, key);
return res;
},
});
}
(1)依赖收集:track
我们每一个对象中的每一个 key,它需要一个依赖收集的容器。我们需要创建这个容器,将 effect 放入其中。
因为我们的 effect 不能重复,所以我们选择 Set 这个数据结构
// src/reactivity/effect.ts
const targetMap = new Map();
let activeEffect;
export function track(target, key) {
// target -> key -> dep
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);
}
// effect收集依赖的过程是在run方法执行中
dep.add(activeEffect);
}
activeEffect:当前正在执行的 effect
- effect收集依赖的过程是在run方法执行中
- 先执行
run方法中用户传入的fn函数 - 触发
reactive对象的get - 执行
get中的依赖收集track - 所以此时
activeEffect应该是当前正在执行的effect
在run的过程中设置 activeEffect
class ReactiveEffect {
...
run() {
// 调用 run 的时候表示当前 effect 是正在执行的状态,把它赋值给 activeEffect
activeEffect = this;
this._fn();
}
}
在调用真正的 fn 函数之前给 activeEffect 赋值,因为调用 fn 过程中,就会直接触发相应的 get(依赖收集) 了。
(2)触发依赖:trigger
// src/reactivity/effect.ts
export function trigger(target, key) {
const depsMap = targetMap.get(target);
const dep = depsMap.get(key);
for (const effect of dep) {
effect.run();
}
}
- 基于
target和key取出dep对象 - 遍历
dep,其中每个元素都是一个effect - 执行
effect的run方法
现在我们再执行一下单测:yarn test,可以看到测试通过了。
全部代码如下:
effect.spec.ts
import { effect } from "../effect";
import { reactive } from "../reactive";
describe("effect", () => {
// 核心代码逻辑
it("happy path", () => {
const user = reactive({
age: 10,
});
let nextAge;
// user.age:依赖收集
// effect一上来会调用fn,然后会触发 user.age 的get操作,触发get时进行依赖收集
effect(() => {
nextAge = user.age + 1;
});
expect(nextAge).toBe(11);
// 更新:触发依赖
// 触发依赖:user.age触发set操作时,会把所有收集到的fn拿出来调用一下
user.age++;
expect(nextAge).toBe(12);
});
});
reactive.spec.ts
import { reactive } from "../reactive";
describe("reactive", () => {
it("happy path", () => {
const original = { foo: 1 };
const observed = reactive(original);
expect(observed).not.toBe(original);
expect(observed.foo).toBe(1);
});
});
reactive.ts
import { track, trigger } from "./effect";
// reactive其实就是 Proxy 的代理
export function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
const res = Reflect.get(target, key);
// 依赖收集
track(target, key);
return res;
},
set(target, key, value) {
const res = Reflect.set(target, key, value);
// 触发依赖
trigger(target, key);
return res;
},
});
}
effect.ts
class ReactiveEffect {
private _fn: any;
constructor(fn) {
this._fn = fn;
}
run() {
// 调用 run 的时候表示当前 effect 是正在执行的状态,把它赋值给 activeEffect
activeEffect = this;
this._fn();
}
}
const targetMap = new Map();
let activeEffect;
export function track(target, key) {
// 依赖不能重复,我们选择 Set 这个数据结构
// target -> key -> dep
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);
}
// effect收集依赖的过程是在run方法执行中
// 所以是先执行run方法,这里可以保证 activeEffect 已经有值了
dep.add(activeEffect);
}
export function trigger(target, key) {
// 基于target和key取出dep,执行effect的run方法(用户传入的fn)
const depsMap = targetMap.get(target);
const dep = depsMap.get(key);
for (const effect of dep) {
effect.run();
}
}
export function effect(fn) {
// 封装,用类进行表示
const _effect = new ReactiveEffect(fn);
// 当调用effect的时候,直接执行内部的fn(封装在run方法中)
_effect.run();
}