实现自动执行的响应式
上篇文章我们通过手动调用track
函数进行依赖的收集,手动调用trigger
函数进行依赖的执行,显然这是不符合我们的使用场景的,我们更希望这一过程是无感知的自动执行的。接下来我们来看如何实现。
我们首先实现依赖的自动收集,前文说到我们的依赖产生的时机其实是在属性被读取的时候,比如:
let total = 0
let product = {
price: 10,
quantity: 2
}
// 属性被读取 应该产生依赖
console.log(product.price)
2
3
4
5
6
7
8
那么这里其实就有文章可做了,前文同时也说到我们可以通过 ptoxy
或者Object.defineProperty
侦测到属性的读取和设置,那么我们就知道依赖收集的时机了,也就是在属性被读取的时候收集就可以了,如下:
function reactive(target) {
const handler = {
get: function(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
// 收集依赖
track()
console.log('收集依赖')
return res
}
}
const proxy = new Proxy(target, handler)
return proxy
}
2
3
4
5
6
7
8
9
10
11
12
13
14
我们姑且将这个代理函数命名为reactive
,通过proxy
定义的get
拦截器,利用track
进行依赖的收集。注意我们这里没有简单的将属性值返回,比如:
function reactive(target) {
const handler = {
get: function(target, key, receiver) {
// const res = Reflect.get(target, key, receiver)
// 收集依赖
track()
console.log('收集依赖')
return target[key]
}
}
const proxy = new Proxy(target, handler)
return proxy
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
而是使用了Reflect (opens new window)函数获取了原始对象的值,然后再返回。为什么要这样做呢?从功能上来说,Reflect.get()
和target[key]
基本是等效。相比于target[key]
,Reflect
有如下的优点:
- Reflect上的操作能返回操作成功与否的反馈
- 能够确保this的指向正确,从而总能在Reflect上获取对象的原始行为
也就是说,不管是你如何使用proxy
对对象进行代理,Reflect
总能保证原始对象的行为,这样我们就没必要在进行拦截操作的同时还要考虑兼容原始对象的行为。
现在我们依赖已经收集好了,那么接下来就是在合适的时机执行依赖了。那么什么时候是合适的时机呢?这里大家应该想到了,肯定是我们读取属性的时机啊,没错。因为当我们修改了某个属性,我们自然希望它所依赖的依赖函数,自动执行一遍,当然也有不想执行的,在真正的Vue3响应式源码中,是有这样的控制机制的,大概是会使用一个active
的标识符标志当前收集的依赖是否是只收集,不触发。好,明确了依赖触发的思路,代码也就出来了:
function reactive(target) {
const handler = {
set: function(target, key, value) {
Reflect.set(target, key, value)
// 这里触发依赖
trigger()
return true
}
}
const proxy = new Proxy(target, handler)
return proxy
}
2
3
4
5
6
7
8
9
10
11
12
13
这样就实现了依赖的自动触发。结合前面的实现,完整的代码如下:
// 保存effect
let dep = new Set()
// 引用类型的代理函数
function reactive(target) {
const handler = {
get: function(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
// 收集依赖
track()
console.log('收集依赖')
return res
},
set: function(target, key, value) {
Reflect.set(target, key, value)
// 这里触发依赖
trigger()
console.log('触发依赖')
return true
}
}
const proxy = new Proxy(target, handler)
return proxy
}
// 收集依赖函数
function track() {
dep.add(effect)
}
// 触发依赖函数
function trigger() {
dep.forEach(effect => effect())
}
// 测试例子
let total = 0
let product = {
price: 10,
quantity: 2
}
const proxyProduct = reactive(product)
let effect = () => {
total = proxyProduct.quantity * proxyProduct.price
}
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
测试一下:
// 读取属性 触发依赖的收集
console.log(proxyProduct.quantity)
console.log(proxyProduct.price)
// 设置属性 触发依赖的执行
proxyProduct.price = 100
console.log(total) // 200
2
3
4
5
6
7
8
9
但是细心的同学很快就发现了一个问题,我们的依赖并没有和属性对应起来,也就是我们第一篇文章提到的场景:
price
对应一个依赖quantity
可能对应另一个依赖
按照我们目前的实现来说,我们并不是在读取price
的时候收集price
的依赖,在设置price
新值的时候执行price
对应的依赖。换句话说,我们需要根据属性去收集依赖,将属性和依赖对应起来,这样才能选择性的收集和执行依赖。同时要考虑到,实际应用场景中,一个属性对应的依赖可能不只一个,可能是多个。那么我们如何去描述这种一对多的对应关系呢?我们下一篇文章将会揭晓答案。