如何理解响应式
# 需求背景
我们先看这样的一个场景:我们已知商品的单价 price
,数量quantity
,求商品的总价total
。这是个再简单不过的需求,于是我们自信的写出代码:
let price = 10
let quantity = 2
let total = 0
total = price * quantity // 20
2
3
4
5
但是,有一天我们需要进行促销活动,商品的价格在某个时间段内不断的波动,此时我们上面的代码还满足需求吗?
let price = 10
let quantity = 2
let total = 0
total = price * quantity // 20
// 促销修改价格
price = 20
console.log(total) // 20
2
3
4
5
6
7
8
9
10
我们打印 total
,仍然是 20
, 因为这是JavaScript正常的行为,total
和 price
并没有必然的联系,price
的改变并不能使得total
再次被计算,有聪明的同学可能就想到了,那我们把total
的计算逻辑封装成函数,在price
改变的时候,再次调用不就好了吗?是的,我们来试试。那我们暂且将这个函数命名为 effect
: 也就是说,total
的计算需要依赖price
的改变,所以称之为依赖函数
。
let price = 10
let quantity = 2
let total = 0
let effect = () => {
total = price * quantity
}
// 首次获取total
effect() // 20
// 促销修改价格 获取total
price = 20
effect()
console.log(total) // 40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
这样做只能说满足了部分需求。但是价格是不断波动的,每调整一次价格就需要手动调用一次effect
函数,不高效,而且也显得程序很'笨'。有没有别的更智慧
一点的方案呢?这个时候有同学可能就提出了这样的设想:
- 那我们能不能做到当
price
值改变的时候,自动去执行effect
函数呢?这样当price
的值被修改,total
的值也就自动计算好了。有点像Excel表格中的计算函数,比如求和函数,输入a,b两个数字,和就自动帮你计算出来了。
想法很好,我们如何实现呢?首先需要解决第一个问题,我们需要侦测price
的改变,进一步说,我们需要侦测在JavaScript中对象的属性被修改的时机。JavaScript提供了这样的能力吗?你别说,还真有。
- Object.defineProperty
- proxy
这两种方式都能拦截属性的获取和设置,比如:
const person = {};
let age = 18
Object.defineProperty(person, 'age', {
get: function () {
console.log('属性被获取')
return age
},
set: function (newValue) {
console.log('属性被设置')
age = newValue
}
});
console.log(person.age)
// 18
// 属性被获取
person.age = 23
// 属性被设置
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
如果不懂Object.defineProperty
的基本概念和用法,可以去Object.defineProperty (opens new window)了解学习。而对于 proxy
就更强大了,Object.defineProperty
只能对对象的属性一个一个拦截,proxy
则能完成对整个对象的拦截,我们来看例子:
const person = {
age: 18
}
const handler = {
get: function(target ,key) {
console.log(`${key}属性被获取`)
return target[key]
},
set: function(terget, key, value) {
console.log(`${key}属性被设置为${value}`)
person[key] = value
}
}
const proxyPerson = new Proxy(person, handler)
console.log(proxyPerson.age)
// 18
// age 属性被获取
proxyPerson.age = 23
// age 属性被设置
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
同样的,关于proxy
的更多的用法,你可以去proxy (opens new window)学习。那么回到我们的需求,我们现在已经能侦测属性修改的时机,接下来,我们需要做的就是侦测到属性修改的时候,自动执行我们的 effect
函数。
let total = 0
const product = {
price: 10,
quantity: 2
}
let effect = () => {
total = product.price * product.quantity
}
const handler = {
get: function(target ,key) {
return target[key]
},
set: function(target, key, value) {
target[key] = value
// 这里调用 efect函数
effect()
}
}
const proxyProduct = new Proxy(product, handler)
// 第一次需要手动调用 effect
effect()
console.log(total) // 20
// 会自动执行
proxyProduct.price = 13
console.log(total) // 26
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
这样似乎完成了我们上述的需求。但是此时我们会发现,不管是修改price
还是quantity
,都会触发我们的effect
函数,但是实际场景中,我们可能希望属性
和effect
对应起来,也就是:
- 修改
price
,我们只想触发price
对应的effect
- 修改
quantity
, 我们只想触发quantity
对应的effect
- 或者说,修改
price
和quantity
其中一个,执行相同的effect
基于上述的场景,我们最好是有一个依赖的收集过程,进行统一的管理,这样方便我们更好的控制依赖的执行。那么如何进行依赖的收集呢?我们又在什么时候进行依赖收集呢?通过上面的例子,我们会很快发现,这两个问题很好解决:
- 使用数组这样的数据结构用来收集依赖
- 在属性被读取的时候收集依赖
const effects = []
let effect = () => {
total = product.price * product.quantity
}
let track = () => {
effects.push(effect)
}
const handler = {
get: function(target ,key) {
track()
return target[key]
},
set: function(target, key, value) {
target[key] = value
// 这里调用 efect函数
effect()
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
这样我们就实现了依赖的收集,这时候有同学就提出疑问了,那我们执行effect
的时候是不是就不能直接effect()
了,因为是多个依赖,我们需要将数组里面的所有effect
都执行一遍。确实是这样的,我们把之前的代码完善一下:
let total = 0
const effects = []
const product = {
price: 10,
quantity: 2
}
let track = () => {
effects.push(effect)
}
let effect = () => {
total = product.price * product.quantity
}
const handler = {
get: function(target ,key) {
track()
return target[key]
},
set: function(target, key, value) {
target[key] = value
// 这里调用 efect函数
effect()
}
}
const proxyProduct = new Proxy(product, handler)
// 会自动执行
proxyProduct.price = 13
console.log(total) // 26
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
这样就解决了一半上述的依赖收集问题,但是呢?细心的同学又发现问题了,我们并没有解决peice
和quantity
的依赖对应问题啊。是这样的,接着往下看,我们后面的文章将解决这个问题。
# 总结
我们上述实现的需求,其实就是Vue3响应式实现的思想。通过使用proxy
进行对象属性获取和设置的拦截器,进行依赖的收集和执行,看完本篇文章,你应该明白了下述的概念:
- effect 依赖函数
- track 用来收集依赖
- trigger 触发依赖
在Vue3中也存在effect
、track
、trigger
函数,理解这这三个函数的概念,对你后面理解Vue3的reactivity
的源码有着至关重要的作用。下一篇我们将会按照Vue3中响应式实现的思路实现一个最简单的响应式。