浅谈 vue3 响应式原理 第二篇 记录与重放

浅谈 vue3 响应式原理 第二篇 记录与重放

上一篇简单讲了一下什么是响应式,实现响应式的基本套路:在变量被设置新值的时候,重新执行一次副作用。

但是副作用有很多,我们不可能把每个副作用的执行都在 Proxy 中硬编码一遍,如何设计才能管理这么多副作用,这次来讲 vue3 实现响应式需要的数据结构。

副作用的存储

要完成响应式,我们上一篇讲了在变量执行 set 操作时将副作用重放一遍。但是一个程序有这么多变量,这么多副作用,vue3 是如何管理的呢?

首先这里引入一个新名词:depsMap

depsMap 是一个绑定属性与副作用的 Map。他的 key 是变量的属性名,他的 value 是属性对应的副作用。

一个属性不止一个副作用,因此 value 是一个 Set,这样就能存储多个副作用。

我们给这个 Set 取一个新名字:dep

由此可见,depsideEffect 的集合。这样一来,我们就能管理整个对象的副作用了。

record 和 playback

接下来我们要把上一篇的响应式代码魔改一下。

我们需要增加一个新方法:track

track 的作用是把副作用存储到结构中, trigger 的作用是从结构中找到副作用然后重放。

这2者,一个 写入sideEffectrecord ,一个 读取sideEffectplayback ,是我们实现响应式的基本思路。

下面代码中通过 track 一步一步构造出 depsMapdep,构建出属性、副作用之间的关系。

当属性发生变更时,再通过 trigger 重放副作用。

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
const depsMap = new Map()
let p = {
price:10,
quantity:2
}
let total = 0
function effect(){
total = p.price * p.quantity
}
function track(key:string){
let dep = depsMap.get(key)
if(!dep){
depsMap.set(key,(dep = new Set()))
}
dep.add(effect)
}
function trigger(key:string){
const dep = depsMap.get(key)
if(dep){
dep.forEach((effect:any)=>{
effect()
})
}
}
track('quantity')
effect()
total//20
p.quantity = 3
trigger('quantity')
total//30

管理多个对象

为了管理更多响应式对象,我们使用一个新的 Map 记录所有的响应式对象。

引入新名词:targetMap

targetMap 是绑定对象和depsMap的 WeakMap。他的 key 是对象,而 value 是 depsMap。

这么一来,所有的对象都被记录在 targetMap,每个对象都有自己的 depsMap,每个 depsMap 都记录着对象的每个属性应该有哪些副作用。

加入 targetMap 之后,代码如下:

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
const targetMap = new WeakMap()
let p = {
price:10,
quantity:2
}
let total = 0
function effect(){
total = p.price * p.quantity
}
function track(target:any,key:string){
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()))
}
dep.add(effect)
}
function trigger(target:any,key:string){
const depsMap = targetMap.get(target)
if(!depsMap){
return
}
const dep = depsMap.get(key)
if(dep){
dep.forEach((effect:any)=>{
effect()
})
}
}
track(p,'quantity')
effect()
total//20
p.quantity = 3
trigger(p,'quantity')
total//30

结合 Proxy 拦截实现自动 track 和 trigger

以上面的代码为基础,我们将 Proxy 封装成 reactive 函数,实现“自动”启动 tracktrigger

reactive 的作用是通过 Proxy 构造拦截器,把 tracktrigger 两个方法放在 get,set 拦截器中调用。

因为执行 effect 时,会执行属性的读取,因此触发 track 将对象添加到 targetMap,然后构建关联的 depsMap,再构建 dep 存储当前 effect

后来为属性设置新值时,就会触发 trigger 将对应的副作用执行一遍。

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
const targetMap = new WeakMap()

function track(target:any,key:string){
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()))
}
dep.add(effect)
}
function trigger(target:any,key:string){
const depsMap = targetMap.get(target)
if(!depsMap){
return
}
const dep = depsMap.get(key)
if(dep){
dep.forEach((effect:any)=>{
effect()
})
}
}
function reactive(target:any){
const handler:any = {
get(target:any,key:string,receiver:any){
const result = Reflect.get(target,key,receiver)
track(target,key)
return result
},
set(target:any,key:string,value:any,receiver:any){
const oldVal = target[key]
const result = Reflect.set(target,key,value,receiver)
if(result && oldVal !== value){
trigger(target,key)
}
return result
}
}
return new Proxy(target,handler)
}

let p = {
price:10,
quantity:2
}
let product = reactive(p)
let total = 0
function effect(){
total = product.price * product.quantity
}
effect()
total//20
product.quantity = 3
total//30

限制 track 的执行

我们发现,程序中读取属性是非常普遍的操作,甚至在副作用中也会读取属性。这样很容易形成一个闭环:trigger->track->trigger->track->…

原因是执行 trigger 时,我们是在一个 forEach 循环里,而 track 的作用是往 Set 中添加副作用,这样导致了执行 effect,然后添加 effect,再执行刚刚添加的 effect,再添加 effect,最后递归下去没有出口。

我们下面添加一个控制变量 activeEffect,让我们的 track 只在首次执行副作用时才被调用。

为此,我们重写了 effect 方法,effect 不再是副作用,而是一个传入副作用的函数,里面不仅管理了 activeEffect 的状态,还调用了副作用本身。

我们还修改了 track 方法,当 activeEffect 有值时,才会往下执行,并且最后 dep 添加的是 activeEffect

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
const targetMap = new WeakMap()
let activeEffect:any = null

function track(target:any,key:string){
if(!activeEffect){
return
}
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()))
}
dep.add(activeEffect)
}
function trigger(target:any,key:string){
const depsMap = targetMap.get(target)
if(!depsMap){
return
}
const dep = depsMap.get(key)
if(dep){
dep.forEach((effect:any)=>{
effect()
})
}
}
function reactive(target:any){
const handler:any = {
get(target:any,key:string,receiver:any){
const result = Reflect.get(target,key,receiver)
track(target,key)
return result
},
set(target:any,key:string,value:any,receiver:any){
const oldVal = target[key]
const result = Reflect.set(target,key,value,receiver)
if(result && oldVal !== value){
trigger(target,key)
}
return result
}
}
return new Proxy(target,handler)
}

function effect(eff:any){
activeEffect = eff
activeEffect()
activeEffect = null
}

let p = {
price:10,
quantity:2
}
let product = reactive(p)
let total = 0
effect(()=>{
total = product.price * product.quantity
})

total//20
product.quantity = 3
total//30

未完持续O(∩_∩)O