effect 实现

第一步

effect.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
export function effect(fn, options?) {
// fn 是用户传进来的要执行的函数
const _effect = new ReactiveEffect(fn, () => {
_effect.run();
});

_effect.run();

return _effect;
}

class ReactiveEffect {
// 默认创建的 effect 是响应式的
public active = true;

// fn 是用户编写的函数
// 如果 fn 中依赖的数据发生变化后,需要重新调用 run
constructor(public fn, public scheduler) {
}

run() {
// 不是激活的,就什么都不做
if (!this.active) {
return this.fn();
}
return this.fn();
}
}

第二步

做依赖收集

第一步中情况,如果像下面这样使用的话,就会出现在非 effect 中使用代理后的对象,也会触发依赖收集

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app"></div>

<script type="module">
// import { reactive, effect } from '/node_modules/@vue/reactivity/dist/reactivity.esm-browser.js';
import { reactive, effect } from './reactivity.js';

let obj = { name: 'xiaoming', age: 18, flag: true, address: { name: 1 } };
const state = reactive(obj);
console.log(state);
const _effect = effect(() => {
app.innerHTML = `姓名${state.name}, 年龄:${state.age}`
})
setTimeout(() => {
state.age++
}, 1000)
</script>
</body>
</html>

依赖收集是放在属性的访问中做的,即 baseHandler.ts 中的 mutableHandlers

baseHandler.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { ReactiveFlags } from './constants';
import { activeEffect } from './effect';

export const mutableHandlers: ProxyHandler<any> = {
get(target, key, receiver) {
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}

// 导入当前的 effect
// 依赖收集
console.log(activeEffect, key);

const res = Reflect.get(target, key, receiver);
return res;
},

set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
return result;
}
};

此时按照上面的代码,控制台会输出三次,但正常来说应该只有两个输出才对,因为最后一次在 setTimeout 中访问 age 这部分逻辑并没有在 effect 中,不应该收集

因此,新增 try finally,结束的时候,将当前活动的 effect 置为空,这样就不会触发了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
export function effect(fn, options?) {
// fn 是用户传进来的要执行的函数
const _effect = new ReactiveEffect(fn, () => {
_effect.run();
});

_effect.run();

return _effect;
}

// -----------------------------新增开始-----------------------------
export let activeEffect;
// -----------------------------新增结束-----------------------------

class ReactiveEffect {

// 默认创建的 effect 是响应式的
public active = true;

// fn 是用户编写的函数
// 如果 fn 中依赖的数据发生变化后,需要重新调用 run
constructor(public fn, public scheduler) {
}

run() {
// 不是激活的,就什么都不做
if (!this.active) {
return this.fn();
}
// -----------------------------新增开始-----------------------------
try {
activeEffect = this;
// 如果是激活的,则做依赖收集
return this.fn();
} finally {
// 当 effect 函数执行完毕后,应该将其置为空,否则的话,只是单纯的使用代理后的对象的时候,也会触发依赖收集
activeEffect = undefined;
}
// -----------------------------新增结束-----------------------------
}
}

第三步

但是上面的写法,在 activeEffect 位置还有一个问题,如下:

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app"></div>

<script type="module">
import { reactive, effect } from './reactivity.js';

let obj = { name: 'xiaoming', age: 18, flag: true, address: { name: 1 } };
const state = reactive(obj);
effect(() => { // f1
console.log(state.name);

effect(() => { // f2
console.log(state.name);
});

console.log(state.age);
});
</script>
</body>
</html>

当我像上面这样使用的时候,在走最外层的 effect 的时候,activeEffect 的值是 f1,当走到里面的 effect 的时候,activeEffect 变成了 f2,但是,当 f2 执行完毕后,activeEffect 就变成 undefined 了,这样就导致在最后的 age 属性收集不到了

因此新增 lastEffect 属性,用来记录上一次的 effect,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
export function effect(fn, options?) {
// fn 是用户传进来的要执行的函数
const _effect = new ReactiveEffect(fn, () => {
_effect.run();
});

_effect.run();

return _effect;
}

export let activeEffect;

class ReactiveEffect {

// 默认创建的 effect 是响应式的
public active = true;

// fn 是用户编写的函数
// 如果 fn 中依赖的数据发生变化后,需要重新调用 run
constructor(public fn, public scheduler) {
}

run() {
// 不是激活的,就什么都不做
if (!this.active) {
return this.fn();
}
// -----------------------------新增开始-----------------------------
let lastEffect = activeEffect;
// -----------------------------新增结束-----------------------------
try {
activeEffect = this;
// 如果是激活的,则做依赖收集
return this.fn();
} finally {
// 当 effect 函数执行完毕后,应该将其置为空,否则的话,只是单纯的使用代理后的对象的时候,也会触发依赖收集
// -----------------------------新增开始-----------------------------
activeEffect = lastEffect;
// -----------------------------新增结束-----------------------------
}
}
}

此处稍微有点绕,可以结合以下内容理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app"></div>

<script type="module">
import { reactive, effect } from './reactivity.js';

let obj = { name: 'xiaoming', age: 18, flag: true, address: { name: 1 } };
const state = reactive(obj);
effect(() => { // f1
console.log(state.name);

effect(() => { // f2
console.log(state.name);
});

console.log(state.age);
});

let lastEffect = activeEffect;
let activeEffect = this;
let activeEffect = lastEffect;

// 没有执行 f1 的时候,activeEffect 的值是 undefined,第一次执行 f1 的时候,activeEffect 为 f1,则 lastEffect 也是 undefined
// 然后 f1 还没有执行结束,开始执行 f2,此时执行了这一步,let lastEffect = activeEffect; 上面 activeEffect 的值是 f1,所以此时 lastEffect 的值是 f1
// 当 f2 执行完毕后,执行了这段代码,activeEffect = lastEffect,由于 lastEffect 的值是 f1,所以此时 activeEffect 的值也是 f1,而 console.log(state.age) 所需要的 effect 就是 f1
</script>
</body>
</html>

整理代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
export function effect(fn, options?) {
// fn 是用户传进来的要执行的函数
const _effect = new ReactiveEffect(fn, () => {
_effect.run();
});

_effect.run();

return _effect;
}

export let activeEffect;

class ReactiveEffect {

// 默认创建的 effect 是响应式的
public active = true;

// fn 是用户编写的函数
// 如果 fn 中依赖的数据发生变化后,需要重新调用 run
constructor(public fn, public scheduler) {
}

run() {
// 不是激活的,就什么都不做
if (!this.active) {
return this.fn();
}

let lastEffect = activeEffect;
try {
activeEffect = this;
// 如果是激活的,则做依赖收集
return this.fn();
} finally {
// 当 effect 函数执行完毕后,应该将其置为空,否则的话,只是单纯的使用代理后的对象的时候,也会触发依赖收集
activeEffect = lastEffect;
}
}
}