实现对象多个属性对应的依赖的收集
上篇文章实现手动执行的响应式 (opens new window)实现了自动收集依赖和自动执行依赖,但是很显然, 还存在问题: 我们如何将数据的属性和依赖对应起来。举个例子:
const product = {
price: 10,
quantity: 20
}
2
3
4
product
有两个属性: price
和quantity
。然后使用前面实现的reactive
的函数将product
转换成响应式对象:
const proxyProduct = reactive(product)
然后分别定义price
和quantity
的依赖函数:
// price 的依赖函数
let pEffect1 = () => {
// ....
}
let pEffect2 = () => {
// ....
}
// quantity的依赖函数
let qEffect1 = () => {
// ...
}
let qEffect2 = () => {
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
然后我们试图去收集依赖函数的时候,就出现文首的问题了。我们之前的实现并没有将对象的属性和依赖对应的收集起来。也就是说,我们期望的是:
- 读取
price
属性,只收集price
相关联的依赖,可能是多个 - 读取
quantity
属性,只收集quantity
相关联的依赖,可能是多个 - 设置
price
为新值的时候,只触发price
相关联的依赖 - 设置
quantity
为新值的时候,只触发quantity
相关联的依赖
前文我们已经提到了,会使用Set
集合保存收集的effect,那么再结合我们本文的需求,我们可以很容易画出下面的示意图:
其实说到这里,想必大家都已经知道使用什么数据结构维护这个关系了。是的,其实就是用Map
。key
其实就是对象的属性,value
就是
Set
,Set
里面保存的就是收集的effect
。正如上面的图所示。明确了思路,那我们现在就来用代码实现一下(为了更直观,之前实现的代码没有展示出来):
// 使用map描述一个对象多个属性对应多个依赖的对应关系
const depsMap = new Map()
2
3
然后在track
函数收集依赖的时候,将依赖收集进depsMap
:
// 收集依赖函数
function track(key) {
// 新增 需要根据 key 收集对应的依赖
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
同时,由于我们收集依赖的时候需要对象的属性作为key
,调用track
函数的地方也需要传入key
:
function reactive(target) {
const handler = {
get: function(target, key, receiver) {
// ...
// 传入key值
track(key)
//...
},
}
}
2
3
4
5
6
7
8
9
10
同样的,我们之前调用trigger
函数的时候,是直接调用依赖函数的,现在也需要修改下逻辑,我们需要传入需要调用的依赖的key
:
// 引用类型的代理函数
function reactive(target) {
const handler = {
// ...
set: function(target, key, value) {
// ...
}
// 这里触发依赖
trigger(key)
// ...
}
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
然后在trigger
函数中需要根据传入的key
,取出对应的依赖函数并执行:
function trigger(key) {
let dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
2
3
4
5
6
这样我们就解决了属性和依赖之间的对应关系
的问题,完整代码如下:
// 使用map描述一个对象多个属性对应多个依赖的对应关系
const depsMap = new Map()
// 引用类型的代理函数
function reactive(target) {
const handler = {
get: function(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
// 收集依赖
track(key)
return res
},
set: function(target, key, value) {
Reflect.set(target, key, value)
// 这里触发依赖
trigger(key)
return true
}
}
const proxy = new Proxy(target, handler)
return proxy
}
// 收集依赖函数
function track(key) {
// 新增 需要根据 key 收集对应的依赖
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, dep = new Set())
}
// 收集依赖
dep.add(effect)
}
// 触发依赖函数
function trigger(key) {
let dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
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
最后我们写个测试例子测试一下:
let total = 0
let product = {
price: 10,
quantity: 2
}
const proxyObj = reactive(product)
// 这个就是副作用函数
let effect = () => {
total = proxyObj.quantity * proxyObj.price
}
console.log(proxyObj.price) // 10
// console.log(proxyObj.quantity)
proxyObj.price = 20
console.log(total) // 40
console.log(depsMap)
// Map(2) {'price' => Set(1) { [Function: effect] }, 'quantity' => Set(1) { [Function: effect] } }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
是不是很完美?其实并不是,这里我们已经明确知道了某个属性的所有依赖,因为我们使用Map
结构对应好了这种关系。但是我们在实际应用中
并不只是会定义一个响应式对象,而是多个,这是再正常不过的场景了。说到这里,可能有的同学会说,根据我们目前的实现,也可以啊,比如:
const proxyProduct1 = reactive({name: '张三', age: 11})
const proxyProduct2 = reactive({name: '李四', age: 12})
const proxyProduct3 = reactive({name: '王五', age: 13})
// ...
2
3
4
5
我们每需要使用一个对象就用reactive
代理一下不就行了吗?理论是这样的,但是这样做代码维护性并不是很好,我们还是希望使用某种数据结构将这种关系也维护起来,进行统一的依赖管理。好了,明确了为什么需要这样做,接下来就看如何实现了。