前言
在本文,我们能够学习到响应系统的核心设计原则,一步步地学习响应性:初步的响应性理解->Vue2的响应性->Vue2的响应性缺陷->Vue3的响应性->完善的Vue3响应性。🔥
认识下JS的程序性🤖
先来了解一下 JS的程序性:
"看到下面这段代码,在第二次打印时你真的希望它还是输出20吗?"
html
<script>
// 定义一个商品对象
let product = {
price: 10,
quantity: 2
}
// 总价格
let total = product.price * product.quantity
console.log(`总价格:${total}`) //20
// 修改商品的数量
product.quantity = 5
console.log(`总价格:${total}`) //20
</script>
我们希望的是如果商品数量发生变化了,如果总价格能够自己跟随变化 ,这就是我们所期盼的响应性。
但是 js
本身具备程序性,所谓程序性 指的就是:一套固定的,不会发生变化的执行流程 ,在这样的一个程序性之下,我们是不可能 拿到想要的50
的。
响应性的一步步实现🐾
初步的响应性理解->Vue2的响应性->Vue2的响应性缺陷->Vue3的响应性->完善的Vue3响应性
初步的响应性理解💬
对于product.quantity
而言,第二次打印要想实现响应式的话,必须按照一套逻辑:"首先是修改商品数量触发setter
行为,接着手动调用effect(),触发商品数量的getter
行为"
然而每次都手动调用effect()
,也太麻烦了吧!
html
<script>
// 定义一个商品对象
let product = {
price: 10,
quantity: 2
}
// 总价格
let total = 0
// 计算总价格
let effect = () => {
total = product.price * product.quantity // product.quantity触发getter行为
}
effect()
console.log(`总价格:${total}`) //20
// 修改商品的数量
product.quantity = 5 // product.quantity触发setter行为
effect()
console.log(`总价格:${total}`) //50
</script>
Vue2中的响应性
Vue2 使用 Object.defineProperty作为响应性的核心API,Object.defineProperty()
用于监听指定对象上指定属性的getter
行为和setter
行为。
"那么就意味着我们可以不用手动调用effect()
了,让api自己帮我们监听并调用。"
html
<script>
// 定义一个商品对象
let quantity = 2
let product = {
price: 10,
quantity: quantity
}
// 总价格
let total = 0
// 计算总价格
let effect = () => {
total = product.price * product.quantity
}
// 第一次打印
effect()
console.log(`总价格:${total}`) //20
Object.defineProperty(product, 'quantity', {
set(newVal) {
console.log('setter')
quantity = newVal
// 调用effect()
effect()
},
get() {
console.log('getter')
return quantity
}
})
</script>
去浏览器中输出试一下:
为什么Vue3放弃了这种方式呢?
这是因为 Object.defineProperty 存在一个致命的缺陷!
vue官网中存在这样的一段描述:
由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。
"这是什么意思呢?"
对象的变化
对于对象而言,例如在你的页面上通过v-for
循环渲染出了{name:'张三',age:30}
这个对象的数据,
同时你定义了一个方法,能够为这个obj对象增添一个gender
属性,并且打印出obj
在按下按钮后,我们发现虽然输出的obj
包含了gender
属性,但是页面的视图并没有更新!即这个gender并不是响应性的!也就是说Vue不能检测 对象 的变化 !
数组的变化
对于数组而言,还是同样的例子,你定义了一个方法通过数组下标新增数组元素,并且打印出arr
现象与上面一致,也就是说Vue不能检测 数组 的变化 !
在上面的例子中,我们呈现了vue2中响应性的限制:
- 当为 对象 新增一个没有在data中声明的属性时,新增的属性不是响应性的
- 当为 数组 通过下标的形式新增一个元素时,新增的元素不是响应性的
总结:Object.defineProperty()
用于监听指定对象上指定属性 的getter
行为和setter
行为,那么这就意味着:我们必须要知道指定对象中存在该属性,才可以为该属性指定响应性。但是由于javaScript的限制,我们无法监听到 为某一个对象新增了某一个属性的行为 ,那么新增的这个属性就无法通过Object.defineProperty()
来监听getter
和setter
,所以新增的属性不具备响应性数据
Vue3中的响应性
因为 Object.defineProperty 存在的问题,所以Vue3中修改了这个核心API,改为使用 Proxy 进行实现。
proxy 顾名思义就是代理的意思。我们来看如下代码:
html
<script>
// 定义一个商品对象
let product = {
price: 10,
quantity: 2
}
// product:被代理对象
// proxyProduct:代理对象
const proxyProduct = new Proxy(product, {
set(target, key, newVal, receiver) {
console.log('setter')
},
get(target, key, receiver) {
console.log('getter')
}
})
// 总价格
let total = 0
// 计算总价格
let effect = () => {
total = proxyProduct.price * proxyProduct.quantity
}
// 第一次打印
effect()
console.log(`总价格:${total}`) //20
</script>
一定要使用代理对象,而不是被代理对象(只有代理对象才会触发getter
和setter
)
Vue3基本的响应性 Proxy
html
<script>
// 定义一个商品对象
let product = {
price: 10,
quantity: 2
}
// product:被代理对象
// proxyProduct:代理对象
const proxyProduct = new Proxy(product, {
set(target, key, newVal, receiver) {
// console.log('setter')
target[key] = newVal
// 触发effect()
effect()
return true //默认setter行为完成后返回true
},
get(target, key, receiver) {
// console.log('getter')
return target[key]
}
})
// 总价格
let total = 0
// 计算总价格
let effect = () => {
total = proxyProduct.price * proxyProduct.quantity //改为代理对象才会触发getter和setter
}
// 第一次打印
effect()
console.log(`总价格:${total}`) //20
</script>
Vue2和Vue3响应性区别
proxy:
- Proxy将代理一个对象(被代理对象),得到一个新的对象(代理对象),同时拥有被代理对象中所有的属性。
- 当想要修改对象的指定属性时,我们应该使用 代理对象 进行修改
- 代理对象 的任何一个属性都可以触发 handler 的 getter 和 setter
Object.defineProperty():
- Object.defineProperty为 指定对象的指定属性 设置 属性描述符
- 当想要修改对象的指定属性时,可以使用原对象进行修改
- 通过属性描述符,只有 **被监听 **的指定属性,才可以触发getter和setter
如此看来,由于 proxy
没有指定属性 这个概念,而是使用代理对象 的概念,所以 vue3 将 不会 再存在新增属性时失去响应性的问题!
Vue3完善的响应性 Proxy+Reflect
当我们了解了Proxy之后,那么接下来我们需要了解另外一个Proxy的"伴生对象":Reflect
说起Reflect.get
,总感觉他是个多余的Api,就像如下代码:
javascript
const obj = {
name: '张三'
}
console.log(obj.name);//张三
console.log(Reflect.get(obj,'name'));//张三
但其实Reflect.get
重要的地方在于它的第三个参数 Reflect.get(target,propertyKey[,receiver])
官方的介绍为:
如果target对象中指定了getter,receiver则为getter调用时的this值。
html
<script>
const p1 = {
lastName: '张',
firstName: '三',
get fullName() {
return this.lastName + this.firstName
}
}
const p2 = {
lastName: '李',
firstName: '四',
get fullName() {
return this.lastName + this.firstName
}
}
console.log(p1.fullName) //张三
console.log(Reflect.get(p1, 'fullName')) //张三
console.log(Reflect.get(p1, 'fullName', p2)) //李四
</script>
此时触发的 fullName 不是p1的而是p2的。
接下来我们看看下面代码的打印情况:
javascript
<script>
const p1 = {
lastName: '张',
firstName: '三',
get fullName() {
console.log(this)
return this.lastName + this.firstName
}
}
const proxy = new Proxy(p1, {
get(target, key, receiver) {
console.log('getter行为被触发')
return target[key]
}
})
console.log(proxy.fullName)
</script>
这是我们预想要得到的打印结果吗?我们触发了proxy.fullName
,fullName
中又触发了this.lastName
+ this.firstName
,那么问:getter应该被触发几次?
此时应该为触发3次getter!但实际只触发了1次!为什么?
因为this.lastName+ this.firstName
中的 **this**
** 为 **p1**
,而非 **proxy**
!**所以不会再次触发getter。
所以,如果我们想要"安全"的使用Proxy
,还需要配合Reflect
一起才可以,因为一旦我们在被代理对象的内部通过this
触发getter
和setter
时,也需要被监听到。
那么这时候就可以用到Reflect.get
的第三个参数了
javascript
<script>
const p1 = {
lastName: '张',
firstName: '三',
get fullName() {
console.log(this)
return this.lastName + this.firstName
}
}
const proxy = new Proxy(p1, {
get(target, key, receiver) {
console.log('getter行为被触发')
// return target[key]
return Reflect.get(target, key, receiver)
}
})
console.log(proxy.fullName)
</script>
总结:
当我们期望监听代理对象的getter
和setter
时,不应该使用target[key]
,因为它在某些时
刻(比如fullName)下是不可靠的 。而应该使用Reflect
,借助它的get
和set
方法,使用receiver
(proxy实例) 作为this
,已达到期望的结果(触发三次getter)。