实现多个对象对应的依赖的收集
上篇文章实现对象多个属性对应的依赖的收集 (opens new window)我们将 属性
和 依赖
的关系维护了起来,并且读取该属性的时候只会收集该属性所对应的依赖,设置该属性的时候只会触发该属性所对应的依赖。同时,我们也发现了一个问题,我们需要将响应式对象
和该对象的属性对应的所有的依赖
关系维护起来。那么如何做呢?我们来看。
首先我们可以明确一点,这肯定也是一个Map
结构,因为很明确的key
和value
的关系。只不过此时我们的key
是一个对象,value
是一个Map
。什么样的数据结构符合这样的对应关系呢?你别说,还真有,这就是WeakMap (opens new window),使用WeakMap
不只是因为它很适合这种场景,还有别的优点:
WeakMap 持有的是每个键对象的“弱引用”,这意味着在没有其他引用存在时垃圾回收能正确进行。
基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。 也就是说,对于
WeakMap
保存的对象的引用,垃圾回收器会监听这个对象是否是在使用,如果没有,就会自动回收这个对象。避免内存溢出和不必要的空间占用。这对于Vue3提高性能也是很有帮助的。可能还有同学不太明白,我们可以画个简单的图表示目前我们依赖收集的对应关系:
我们略微解释下这个图:
- targetMap 表示多个对象的依赖对应关系
- depsMap 表示多个属性的依赖对应关系
- dep 当前属性收集的依赖集合
首先我们需要在track
函数里面将depsMap
收集进我们定义的WeakMap
容器:
// 使用 WeakMap 描述多个对象的多个属性对应多个依赖的对应关系
const targrtMap = new WeakMap()
// 收集依赖函数
function track(target, key) {
let depsMap = targrtMap.get(target)
if (!depsMap) {
targrtMap.set(target, depsMap = new Map())
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, dep = new Set())
}
// 收集依赖
dep.add(effect)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
同时,reactive
函数中调用的时候,就需要传入当前需要进行响应式包装的对象:
function reactive(target) {
// ...
const handles = {
get: function(target, key, receiver) {
// ...
}
// 新增 传入响应式对象 target
track(target, key)
},
// ...
}
2
3
4
5
6
7
8
9
10
11
这样我们就将对象和依赖收集容器的关系维护好了,同样的我们去将trigger
函数也进行改进:
function trigger(target, key) {
// 新增 先根据对象取出所有属性对应的依赖容器
let depsMap = targrtMap.get(target)
let dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
2
3
4
5
6
7
8
trigger
函数执行的时候,也需要传入该响应式对象:
function reactive(target) {
const handles = {
// ...
set: function(target, key, value) {
// 新增 传入 target
trigger(target, key)
}
}
// ...
}
2
3
4
5
6
7
8
9
10
好了,这样我们就解决对象
对应依赖收集容器
的对应关系。但这就完美了吗?相信有的同学很早就发现了一个问题,依赖收集和执行的依赖关系倒是维护好了,但是收集的依赖我们并没有做区分啊,按照我们当前的实现,我们是直接使用effect
定义依赖函数:
let effect1 = () => {
// ...
}
let effect2 = () => {
// ...
}
2
3
4
5
6
7
但是我们收集的时候,是暴力收集,没有做任何区分:
function track(target, key) {
let depsMap = targrtMap.get(target)
if (!depsMap) {
targrtMap.set(target, depsMap = new Map())
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, dep = new Set())
}
// 收集依赖
dep.add(effect)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
我们只能收集一个effect
函数,那我们就得思考一下了,这里肯定不能直接收集effect
,那我们需要改造下effect
函数了,我们需要使用一个全局变量activeEffect
标识当前正在收集的依赖,然后需要改造下effect
函数,如下:
// 使用 activeEffect 变量保存当前激活的 effect
let activeEffect = null
// 副作用函数
function effect(eff) {
activeEffect = eff
activeEffect()
activeEffect = null
}
2
3
4
5
6
7
8
9
可以看到,我们改造了effect
函数,我们此时并不是使用effect
去定义依赖函数,而是采用高级函数的思想,将依赖函数作为参数eff
传入effect
,
并且使用全局变量activeEffect
保存起来,这里为啥使用activeEffect
这个全局变量呢?我们分析下:
function a () {
// ...
}
function b () {
// ...
}
2
3
4
5
6
7
8
可以看到我们定义了两个函数a
,b
,那我们我们如何在a
函数和b
函数中共享变量呢?它们属于不同的作用域,却要共享变量,那只能在他们的父级作用域定义该变量了:
let count = 0
function a () {
// 这里能拿到
console.log(count)
}
function b () {
// 这里也能拿到
console.log(count)
}
2
3
4
5
6
7
8
9
10
11
现在想必大家明白activeEffect
的作用了吧,实际就是方便在track
和trigger
之间共享依赖函数。再回到effect
函数的实现:
function effect(eff) {
activeEffect = eff
activeEffect()
activeEffect = null
}
2
3
4
5
我们使用activeEffect
保存了依赖函数后,立即调用了一次,为什么会立即调用一次呢?答案很简单:为了兼顾大多数大应用场景。对于Vue3来说,reactivity
模块来说,已经没有和Vue强耦合了,你可以在任何框架或者库中使用reactivity
模块,比如说我们只是在一个单独的js文件中引用reactivity
,就正如我们目前做的这样,我们没有渲染任何视图,仅仅是验证输出是否符合我们的期望。比如没有立即调用activeEffect
:
function effect(eff) {
activeEffect = eff
// 不进行手动调用
// activeEffect()
activeEffect = null
}
2
3
4
5
6
7
当你修改值的时候,大家可以想一下,我们收集的依赖函数会执行吗?比如:
let total = 0
const proxyProduct = reactive({ price: 10, quantity: 20 })
effect(() => {
total = price * quantity
})
// 设置新值
proxyProduct.price = 30
console.log(total) // 是200 而不是正确的300
2
3
4
5
6
7
8
9
可以看到,我们的依赖函数并不会执行,看到这里大家可以思考下,为什么修改了值,但是依赖函数并没有执行。其实,细心的同学可能很快就发现了问题的关键,我们并没有触发收集依赖的动作啊。也就是说,上述例子的依赖并没有被收集。原理很简单,我们没有读取响应式对象proxyProduct
的属性。所以我们需要在依赖被收集的时候,手动触发一次依赖函数。这样就会触发属性的读取,依赖也就被正常收集了。再回到effect
的代码:
function effect(eff) {
activeEffect = eff
activeEffect()
activeEffect = null
}
2
3
4
5
最后我们将activeEffect
重置为null
,方便下一次的依赖收集。这样我们看到,实际上我们要收集的依赖就是activeEffect
。所以在track
函数中
需要收集的就是activeEffect
:
function track(target, key) {
let depsMap = targrtMap.get(target)
if (!depsMap) {
targrtMap.set(target, depsMap = new Map())
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, dep = new Set())
}
// 新增 收集的依赖就是 activeEffect
dep.add(activeEffect)
}
2
3
4
5
6
7
8
9
10
11
12
这样我们完美了收集了依赖函数了,完整代码如下:
// 使用 WeakMap 描述多个对象的多个属性对应多个依赖的对应关系
const targrtMap = new WeakMap()
// 使用 activeEffect 变量保存当前激活的 effect
let activeEffect = null
// 引用类型的代理函数
function reactive(target) {
const handler = {
get: function(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
// 收集依赖
track(target, key)
return res
},
set: function(target, key, value) {
let oldValue = target[key]
let res = Reflect.set(target, key, value)
// 这里触发依赖
if (res && oldValue !== value) {
trigger(target, key)
}
return res
}
}
const proxy = new Proxy(target, handler)
return proxy
}
// 依赖收集函数
function track(target, key) {
let depsMap = targrtMap.get(target)
if (!depsMap) {
targrtMap.set(target, depsMap = new Map())
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, dep = new Set())
}
// 收集依赖
dep.add(activeEffect)
}
// 触发依赖函数
function trigger(target, key) {
let depsMap = targrtMap.get(target)
if (!depsMap) return
let dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
// 副作用函数
function effect(eff) {
activeEffect = eff
activeEffect()
activeEffect = null
}
// 测试例子
let total = 0
let total1 = 0
let discount = 0.8
let product = {
price: 10,
quantity: 2
}
let goods = {
price: 10,
quantity: 2
}
const proxyProduct = reactive(product)
const proxyGoods = reactive(goods)
effect(() => {
total = proxyProduct.quantity * proxyProduct.price * discount
})
effect(() => {
total1 = proxyGoods.price * proxyGoods.quantity * discount
})
proxyProduct.price = 500
proxyGoods.quantity = 500
console.log('product的total的值为:', total) // 800
console.log('goods的total1的值为:', total1) // 4000
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
87
88
89
90
完美,我们修改了不同对象的不同属性,执行的依赖函数是该属性对应的依赖函数,所以最后打印800
和4000
,这符合我们的预期。其实说到这里,已经去Vue3尝过鲜的同学,立马就能想到,这不正是Vue3给我们提供的响应式API reactive (opens new window)吗?是的,这正是Vue3实现reactive
API的思路。
但是,当我们修改了discount
的值,并不会重新执行依赖函数,测试一下:
discount = 0.5
console.log('product的total的值为:', total) // 800
console.log('goods的total1的值为:', total1) // 4000
2
3
4
我们会发现,并没有重新计算值。因为目前我们的代理函数reactive
只能代理对象,我们不能代理基本类型。最本质的原因其实是proxy
本身的限制,只能代理引用类型。所以我们也需要提供一个代理基本类型的响应式API。大家估计也想到我说的啥了,对,就是Vue3提供的另一个响应式APIref
。