L
O
A
D
I
N
G

《Vue.js的设计与实现》阅读笔记(二)

前言

数接上文继续看《Vue.js的设计与实现》,第四章 响应系统的作用与实现部分内容笔记,老实说还有些地方不太通透,需要回头完善下

第四章 响应系统的作用与实现

1.响应式数据与副作用函数

副作用函数:函数的执行会直接或间接影响其他函数的执行

1
2
3
4
5
const obj = {text:'hello world'}
function effect() {
//effect 函数的执行会读取 obj.text
document.body.innerText = obj.text
}

2.响应式数据的基本实现

如何实现响应式数据集?

如果我们能拦截一个对象的读取和设置操作,就能实现响应式,ES2015之前通过Object.defineProperty函数实现,这也是vue2.js采用的方式,在ES2015+中,我们可以使用代理对象Proxy来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 存储副作用函数的桶
const bucket = new set()
// 原始数据
const obj = {text:'hello world'}
// 对原始数据代理
const obj = new Proxy(data,{
get(target,key){
// 将副作用函数effect添加到存储副作用函数的桶里
bucket.add(effect)
// 返回属性值
return target[key]
}
set(target,key,newval) {
// 设置属性值
target[key] = newVal
// 把副作用函数从桶里取出并执行
bucket,forEach(fn=>fn())
// 返回true代表设置操作成功
retrun true
}
}
1
2
3
4
5
6
7
8
9
function effect() {
document.body.innerText = obj.text
}
// 执行副作用函数,触发读取
effect()
// 1秒后修改响应式数据
setTimeout(()=>{
obj.text = 'hello vue3'
},1000)

如上通过拦截一个对象的读取和设置操作,可以实现一个简单的响应式数据。

并有上面例子,一个响应式系统的工作流程:

当读取操作发生时,将副作用函数收集到‘桶’中;

当设置操作发生时,从‘桶’中取出副作用函数并执行

3.实现一个相对完善的响应式系统

首先,你可以使用 WeakMap 来创建一个存储响应式数据的桶。WeakMap 是一个键值对的集合,其中键是对象,值是任意类型的数据。由于 WeakMap 的键是弱引用,不会对对象的垃圾回收产生影响,适合用于存储响应式数据。

然后,你可以使用 Map 来构建一个依赖收集系统,用于追踪依赖关系。在 Vue 3 中,响应式数据变化时需要通知相关的依赖进行更新,而 Map 可以帮助我们记录依赖关系,并在数据变化时触发更新。

如何使用 WeakMap 和 Map 来实现一个简单的响应系统?

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
// 创建一个全局的响应式数据桶
const reactiveMap = new WeakMap();

// 创建一个全局的依赖收集的 Map
const depMap = new Map();

// 通过该函数包装对象并实现响应化
function reactive(obj) {
if (reactiveMap.has(obj)) {
return reactiveMap.get(obj);
}

const proxy = new Proxy(obj, {
get(target, key, receiver) {
// 收集依赖
track(target, key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
// 触发更新
trigger(target, key);
return result;
},
});

reactiveMap.set(obj, proxy);
return proxy;
}

// 收集依赖
function track(target, key) {
const depKey = getDepKey(target, key);
if (depMap.has(depKey)) {
const deps = depMap.get(depKey);
if (!deps.has(activeEffect)) {
deps.add(activeEffect);
}
} else {
const deps = new Set([activeEffect]);
depMap.set(depKey, deps);
}
}

// 触发更新
function trigger(target, key) {
const depKey = getDepKey(target, key);
const deps = depMap.get(depKey);
if (deps) {
deps.forEach(effect => {
effect();
});
}
}

// 获取依赖键
function getDepKey(target, key) {
return `${target}-${key}`;
}

// 在此处定义一个全局变量,它将被用于存储当前的依赖
let activeEffect = null;

// 在你需要追踪依赖关系的代码块中,使用 effect 函数包裹
function effect(fn) {
activeEffect = fn;
fn(); // 第一次执行以收集依赖
activeEffect = null;
}

// 示例实现

const state = reactive({ count: 0 });

effect(() => {
console.log(`Count: ${state.count}`);
});

state.count++; // 触发更新并打印 "Count: 1"

我们使用了 reactive 函数来实现对象的响应化,track 函数用于收集依赖,trigger 函数用于触发更新。我们还使用了 effect 函数来包裹需要追踪依赖关系的代码块。

state.count 发生变化时,trigger 函数会触发更新,并执行相关的依赖函数,从而实现响应式的更新机制。

总结来说,使用 WeakMap 结合 Map 可以很好地构建一个相对完善的响应系统,在 Vue 3 中可以更高效地管理响应式数据和依赖关系。

4.分支切换与cleanup

分支切换导致冗余副作用函数进行不必要的更新。vue3中为了解决这个问题,我们需要在每次副作用函数执行之前,清除上一次建立的响应联系(笔记有待完善,没太懂这一块的)

5.避免无限递归循环

假如在同一个副作用函数中同时读取和设置某个响应式数据的值,会产生什么结果呢?

1
2
3
effectRegister(() => {
objProxy.count = objProxy.count + 1
})

image.png
浏览器控制台报错了,出现无限递归循环,栈溢出了。

原因很明显了,首先读取objProxy.count,并把副作用函数存储到依赖中,紧接着又修改objProxy.count,把副作用函数取出来执行,其结果就是,副作用函数在自己内部递归调用自己,栈就溢出了。

解决方法:

通过分析可以发现,读取和设置操作都是在同一个副作用函数中进行的,此时无论track收集的副作用函数还是trigger要触发执行的副作用函数,其实都是同一个,也就是当前的 activeEffect。因此,可以增加守卫条件,当 trigger 要触发执行的副作用函数就是当前正在执行的副作用函数(activeEffect)时,则不触发执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
function trigger (target, key) {
const depsMap = bucket.get(target)
if (!depsMap) {
return
}

const deps = depsMap.get(key)
const depsToRun = new Set(deps)
deps && deps.forEach((effectFn) => {
(effectFn !== activeEffect) && depsToRun.add(effectFn)
})
depsToRun && depsToRun.forEach(effectFn => effectFn())
}

6.调度执行

什么是可调度

可调度性是响应式系统非常重要的特性。所谓可调度,指的是当trigger动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式

调度器

用户在调用 effectRegister 函数注册副作用函数时,可以传递第二个参数 options。options 中允许指定一个 scheduler 调度函数。

1
2
3
4
5
6
7
8
effectRegister(() => {
console.log(objProxy.count)
}, {
// 调度器函数,fn是副作用函数
scheduler (fn) {
// 控制执行时机
}
})

同时,在 effectRegister 函数中我们需要把 options 挂载到副作用函数上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function effectRegister (fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
// 将当前副作用函数压入栈中
effectStack.push(activeEffect)
// 执行 fn(),进行依赖收集(track)
fn()
// 执行完毕,弹出
effectStack.pop()
// 让 activeEffect 始终指向栈顶的副作用函数,若effectStack中无值,则activeEffect还原为undefined
activeEffect = effectStack[effectStack.length -1]
}

// 将 options 挂载到effectFn上
effectFn.options = options
effectFn.deps = []
effectFn()
}

有了调度函数,我们在 trigger 函数中触发副作用函数重新执行时,就可以直接调用用户传进来的调度函数,从而把控制权交给用户:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function trigger (target, key) {
const depsMap = bucket.get(target)
if (!depsMap) {
return
}

const deps = depsMap.get(key)
const depsToRun = new Set(deps)
deps && deps.forEach((effectFn) => {
(effectFn !== activeEffect) && depsToRun.add(effectFn)
})
depsToRun && depsToRun.forEach(effectFn => {
// 存在调度器时,则调用调度器,由调度器决定执行 effectFn 的时机
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
effectFn(
)
}
})
}

个人博客

耀耀切克闹 (yaoyaoqiekenao.com)

其他相关文章

《Vue.js的设计与实现》阅读笔记(一) - 掘金 (juejin.cn)

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

我是穷比,在线乞讨!

支付宝
微信