vue3源码学习-4-effect编写以及依赖收集

前言

经过上文的响应式编写之后,实现了数据包裹之后变成了响应式数据,用户修改数据的时候能监听到操作。

但是实际编写的响应式 reactive.ts 中最核心的是 Proxy 中的 get 和 set 方法。为此我们本次需要将核心代码抽离,并且编写 effect 副作用函数和依赖收集功能,这样函数依赖发生改变,他就重新执行。

reactive.ts 核心代码抽离

响应式代码最重要的式 get 和 set 函数,那么对这一块逻辑抽离。并命名为 baseHandles.ts,然后抛出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// baseHandle.ts
export const enum ReactiveFlags {
IS_RECEIVE = `__v_isReactive`,
}
export let baseHandles = {
// 第一次是普通对象,只是代理,在取值的时候会调用get
// 下一次你传入的是proxy的时候,可以看一下时候代理过,如果有,那么他一定走到了get方法,并且我们访问了ReactiveFlags.IS_RECEIVE,
// 那么就表示这个是被代理过的,就直接返回 target
get(target, key, recevier) {
// return target[key]
return Reflect.get(target, key, recevier)
},
set(target, key, value, recevier) {
// target[key] = value
return result
},
}

然后在 reactive.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
import { isObject } from "@vue/shared";
import { baseHandles, ReactiveFlags } from "./baseHandles";
const reactiveMap = new WeakMap(); // key只能是对象

// 将数据转化成响应式数据,只能做对象的代理
// 同一个对象呗代理多次返回同一个代理
// 代理再次被代理,返回原代理
export function reactive(target: object) {
if (!isObject(target)) {
return;
}
//
if (target[ReactiveFlags.IS_RECEIVE]) {
return target;
}

let existingProxy = reactiveMap.get(target);
if (existingProxy) {
return existingProxy;
}
// 并没有重新定义属性,只是代理,在取值的时候会调用get,同理赋值调用set
const proxy = new Proxy(target, baseHandles);
reactiveMap.set(target, proxy);
return proxy;
}

注意这里出现的一个循环引用逻辑,但是这个不会再 ES6 中造成问题。reactive.ts中引入了baseHandles.ts,但是reactive.ts中对对象的代理包裹,我们应用了reactive.ts,并且包裹了数据,当数据改变的时候,就会触发 set,而这个时候baseHandles.ts又依赖到了reactive.ts。造成了循环引用,但是这个不会导致任何的问题出现。

effect 功能的编写

在测试官方代码的时候,是这么操作 effect 的。effect(() =>{document.getElementById("app").innerHTML = state.name+'今年'+state.age})

通过对上面的操作分析,可以知道通过effect运行了它里面的回调函数,也就是执行了渲染一个谁今年多少岁的一段文字。当被effect包裹的回调函数中state.namestate.age参数改变的时候,我们还要更新下这一段文字。所以有了当前的effect对应这个stage上的 nage 和 age 的映射关系。

再往深处思考,也会出现这种代码effect(() => {stage.name;effect(() => {stage.age})}),这种嵌套的写法,这就是组件的写法了。

上面的实例代码中外层 effect1 可以对应上stage.name,而里面的effect2对应上了stage.age。执行到当前的effect上的时候就能找到对应关联的属性。

所以我们需要这么一个操作,对象的属性->effect,而且如果一个属性可能在多个 effect,那么对象作为 key,最好的是自然就是 WeakMap 了,它还有个好处就是,当 value 为空的时候,垃圾回收机制对它进行回收。

那么对上面对思路进行整合,得出我们需要一个这样对数据结构{对象:Map{name:Set}}。那么现在来编写一下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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
export let activeEffect = undefined //抛出当前运行的是哪个effect

class ReactiveEffect {
// 这里代表在实例上新增active属性
public active = true // 这个effect默认是激活状态
public parent = null // 记录当前effect的父亲是谁,用作返回
public deps = [] // 记录当前的effect都记录了哪些属性
constructor(public fn) {} // 用户传递的参数也会绑定在this上 相当于this.fn = fn;
run() {
// run就是执行effect
if (!this.active) {
// 如果是非激活状态就是非激活状态,只需要执行函数,不需要进行依赖收集
this.fn()
}
// 这里就要依赖收集了,核心就是当前的effect和稍后渲染的属性关联在一起
try {
this.parent = activeEffect
activeEffect = this
this.fn() // 当稍后调用取只操作的时候就可以获取到这个全局的activeEffect了
} finally {
activeEffect = this.parent
}
}
}
export function effect(fn) {
// 这里的fn可以根据状态的变化,重新执行,effect可以嵌套着写
const _effect = new ReactiveEffect(fn) //创建响应式的effect
_effect.run() //默认先执行一次
}
// 实例代码
// effect(() => { age => e1
// state.age;

// effect(() => { name => e2
// stage.name;
// })

// stage.name; name => e1
// })
// 以前呢vue3.0的时候采用栈的方法将对象压栈,然后执行完成之后弹出这样就能关联
// 对应的effect
// 现在的做法是记录effect的父亲是谁,这样每次执行之后就把activeEffect 赋值为父亲对象
let targetMap = new WeakMap()
//跟踪收集依赖
export function track(target, type, key) {
// 在effect中的回调函数中,我们通过语句中执行的target属性收集到effect
// 那么就有了target属性指到哪个effect,
// 那么我们就明确了对象 某个属性-> 多个effect
// 对象作为key,那么第一眼想到WeakMap,并且它还有个好处,当value为空的时候会被
// 垃圾回收机制会回收它
// 那么上述的数据结构应该是 {对象:Map{name:Set}}
if (!activeEffect) return // 如果你不是在模版中触发了get,那么这个依赖就不要收集
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
let shouldTrack = !dep.has(activeEffect) //一个属性多次依赖同一个effect那么去重
if (shouldTrack) {
dep.add(activeEffect)
activeEffect.deps.push(dep) // 让deps记录住对应的dep,稍后在清理的地方用到
}
// 这里单向收集了这个依赖,对象的属性->effect
// 但是这样不方便。例如你有这么一个模版渲染
// effect(() => {flag ? state.age : state.name})
// 那么在你flag判断为true和false的时候依赖的关联是不一样的
// 所以我们也需要收集effect -> 属性
// 在 ReactiveEffect上添加一个数组,来收集当前effect记录了哪些属性
}

//属性改变触发重新渲染
export function trigger(target, type, key, value, oldValue) {
const depsMap = targetMap.get(target)
if (!depsMap) return //触发的值不在模版中
const effects = depsMap.get(key)
effects &&
effects.forEach((effect) => {
if (activeEffect !== effect) effect.run() // 如果这里直接就写effect.run(),那么会遇到这种情况,在模版中赋值,那么也会触发这个,
// 然后又通过了依赖收集的时候,运行它的第一次run()。就会导致循环调用,爆栈,
//所以这里需要加一个判断是否是当前的effect,如果是的话,就忽略这一次的赋值触发的run();
//注意目前的代码是不支持异步的
})
}

上面的代码除了基础的分析之外,还有对 3.0 初期如何找到当前的 effec 的 2 种做法之外,还解决了模版渲染的时候又触发了 run 函数,导致循环调用的问题。

那么上面的主要代码解释了收集依赖的过程,那么现在要对 baseHandles.ts 做一个依赖收集的入口。

baseHandles.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
import { isObject } from "@vue/shared";
import { baseHandles, ReactiveFlags } from "./baseHandles";
const reactiveMap = new WeakMap(); // key只能是对象

// 将数据转化成响应式数据,只能做对象的代理
// 同一个对象呗代理多次返回同一个代理
// 代理再次被代理,返回原代理
export function reactive(target: object) {
if (!isObject(target)) {
return;
}
//
if (target[ReactiveFlags.IS_RECEIVE]) {
return target;
}

let existingProxy = reactiveMap.get(target);
if (existingProxy) {
return existingProxy;
}
// 并没有重新定义属性,只是代理,在取值的时候会调用get,同理赋值调用set
const proxy = new Proxy(target, baseHandles);
reactiveMap.set(target, proxy);
return proxy;
}

结尾

到目前为止,完成了所需要做的工作,这个时候可以在index.html上引入编写好的effect功能,先运行项目

npm run dev,然后倒入编译好的文件。从对象中获取编写的功能

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<!-- <script src="./dist/reactivity.global.js"></script> -->
<div id="app"></div>

<!-- 官方的 -->
<!-- <script src="../../node_modules/@vue/reactivity/dist/reactivity.global.js"></script> -->

<!-- 自己实现的 -->
<script src="./dist/reactivity.global.js"></script>
<script>
// effect 代表的是副作用函数,如果函数依赖发生改变,他就重新执行
// reactive 将数据变成响应式 相当于proxy
// shallowRactive,readonly,shallowReadonly
const { effect, reactive } = VueReactivity;
let target = { name: "david", age: 12, address: { num: 567 } };
const state = reactive(target);
const state2 = reactive(target);
console.log(state === state2);
//set 和 map 也可以劫持

effect(() => {
state.age = Math.random(10);
document.getElementById(
"app"
).innerHTML = `${state.name}今年${state.age}`;
});
setTimeout(() => {
state.age = 13;
}, 1000);
</script>
</body>
</html>
1
git:[@github/MicroMatrixOrg/vue3-plan/tree/effect_schedule]