第三章:响应系统-响应系统的核心设计原则
01:前言
从本章开始我们将开始陆续实现 Vue 3
的核心内容,那么本章首先来看的就是:响应系统
我们通常把:会影响视图变化的数据称为响应数据,当响应式数据发生变化时,视图理应发生变化。
- 那么
Vue
中这样的响应性数据时如何进行实现的呢? Vue2
和Vue3
之间响应性的设计有什么变化吗?为什么会产生这种变化呢?
如果想要知道这些内容,那么就快快开始本章的学习吧!
02:JS 的程序性
想要了解响应性,那么首先我们先了解什么叫做:JS的程序性
我们来看下面的这段代码:
<script>
// 定义一个商品对象,包含价格和数量
let product = {
price: 10,
quantity: 2
}
// 总价格
let total = product.price * product.quantity;
// 第一次打印
console.log(`总价格:${total}`);
// 修改了商品的数量
product.quantity = 5;
// 第二次打印
console.log(`总价格:${total}`);
</script>
大家可以想一下,在这段代码中,第一次打印的值是什么?第二次打印的值是什么?
这是一个非常简单的 JS
逻辑,两次打印的值应该都是一样的:总价格:20
但是大家有没有想过一个问题?
那就是当我们去进行第二次打印的时候,你真的希望它还是20吗?
我们知道我们最终希望打印的是 总价格,那么当
quantity
由 2 变为 5 的时候,总价格不应该是 50 了吗?我们打印出来的总价格,难道不应该是 50 吗?
那么此时你有没有冒出来一个想法:商品数量发生变化了,如果总价格能够自己跟随变化,那就太好了!
但是 js
本身具备 程序性, 所谓程序性指的就是:一套固定的,不会发生变化的执行流程,在这样的一个程序性之下,我们是不可能拿到想要的 50
的。
那么如果我们想要拿到这个 50
就必须让你的程序变得更加 “聪明”,也就是使其具备 响应性!
03:如何让你的程序变得更加“聪明”?
你为了让你的程序变得更加“聪明”,所以你开始想:“如果数据变化了,重新执行运算就好了”。
那么怎么去做呢?你进行了一个这样的初步设想:
- 创建一个函数
effect
,在其内部封装 *计算总价格的表达式 - 在第一次打印总价格之前,执行
effect
方法 - 在第二次打印总价格之前,执行
effect
方法
那么这样我们是不是就可以在第二次打印时,得到我们想要的 50 了呢?
所以据此,你得到了如下的代码:
<script>
// 定义一个商品对象,包含价格和数量
let product = {
price: 10,
quantity: 2
}
// 总价格
let total = 0;
// 计算总价格的匿名函数
+ let effect = () => {
+ total = product.price * product.quantity;
+ }
// 第一次打印
+ effect();
console.log(`总价格:${total}`); // 总价格:20
// 修改了商品的数量
product.quantity = 5;
// 第二次打印
+ effect();
console.log(`总价格:${total}`); // 总价格:50
</script>
在这样的代码中,我们成功的让第二次打印得到了我们期望的结果:数据变化了,运算也重新执行了
但是大家也可以发现,在我们当前的代码中存在一个明显的问题,那就是:**必须主动在数量发生变化之后,重新主动执行 effect ** 才可以得到我们想要的结果。那么这样未免太麻烦了。有什么好的办法吗?
肯定是有的,我们继续来往下看~~~
04:vue2 的响应性核心API:Object.defineProperty
vue2
以 Object.defineProperty 作为响应性的核心 API
,该 API
可以监听:指定对象的指定属性的 getter
和 setter
那么接下来我们就可以借助该 API
,让我们之前的程序进行 自动计算,该 API
接收三个参数:指定对象、指定属性、属性描述符对象
<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
// 监听 product 的 quantity 的 setter
Object.defineProperty(product, 'quantity', {
// 监听 product.quantity = xx 的行为,在触发该行为时重新执行 effect
set(newVal) {
// 注意:这里不可以是 product.quantity = newVal,因为这样会重复触发 set 行为
quantity = newVal
// 重新触发 effect
effect()
},
// 监听 product.quantity,在触发该行为时,以 quantity 变量的值作为 product.quantity 的属性值
get(val) {
return quantity
}
});
</script>
那么此时我们就通过 Object.defineProperty
方法成功监听了 quantity
属性的 getter
和 setter
行为,现在当 quantity
发生变化时,effect
函数将重新计算,以此得到最新的 total
05:Object.defineProperty 在设计层的缺陷
vue2
使用 Object.defineProperty
作为响应性的核心 API
,但是在 vue3
的时候却放弃了这种方式,转而使用 Proxy(后面会详细讲解,现在只需要知道即可)
实现,为什么会这样呢?
这是因为:Object.defineProperty 存在一个致命的缺陷
在 vue 官网中存在这样的一段描述 :
由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。
他说:由于 JavaScript 的限制,Vue 不能检测数组和对象的变化 这是什么意思呢?
我们来看下面的这个例子:
<template>
<div id="app">
<ul>
<li v-for="(val, key, index) in obj" :key="index">
{{ key }} - {{ val }}
</li>
</ul>
<button @click="addObjKey">为对象增加属性</button>
<hr />
<ul>
<li v-for="(item, index) in arr" :key="index">
{{ item }}
</li>
</ul>
<button @click="addArrItem">为数组添加元素</button>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
obj: {
name: '张三',
age: 30
},
arr: ['张三', '李四']
}
},
methods: {
addObjKey() {
this.obj.gender = '男'
console.log(this.obj) // 通过打印可以发现,obj 中存在 gender 属性,但是视图中并没有体现
},
addArrItem() {
this.arr[2] = '王五'
console.log(this.arr) // 通过打印可以发现,arr 中存在 王五,但是视图中并没有体现
}
}
}
</script>
在上面的例子中,我们呈现了 vue2
中响应性的限制:
- 当为 对象 新增一个没有在
data
中声明的属性时,新增的属性 不是响应性 的 - 当为 数组 通过下标的形式新增一个元素时,新增的元素 不是响应性 的
那么为什么会这样呢?
想要搞明白这个原因,那就需要明白官网所说的 由于 JavaScript 的限制 指的是什么意思。
我们知道
vue 2
是以Object.defineProperty
作为核心API
实现的响应性Object.defineProperty
只可以监听 指定对象的指定属性的 getter 和 setter- 被监听了
getter
和setter
的属性,就被叫做 该属性具备了响应性
那么这就意味着:我们 必须要知道指定对象中存在该属性,才可以为该属性指定响应性。
但是 由于 JavaScript 的限制,我们没有办法监听到 指定对象新增了一个属性,所以新增的属性就没有办法通过 Object.defineProperty
来监听 getter
和 setter
,所以 新增的属性将失去响应性
那么如果想要增加具备响应性的新属性,那么可以通过 Vue.set 方法实现
那么此时,我们已经知道了这些 vue2
中的 “缺陷”,那么 vue3
是如何解决这些缺陷的呢?我们继续来往下看~~~
06:vue3 的响应性核心API:proxy
因为 Object.defineProperty
存在的问题,所以 vue3
中修改了这个核心 API
,改为使用 Proxy 进行实现。
proxy
顾名思义就是 代理 的意思。我们来看如下代码:
<script>
// 定义一个商品对象,包含价格和数量
let product = {
price: 10,
quantity: 2
}
// new Proxy 接收两个参数(被代理对象,handler 对象)。
// 生成 proxy 代理对象实例,该实例拥有《被代理对象的所有属性》 ,并且可以被监听 getter 和 setter
// 此时:product 被称为《被代理对象》,proxyProduct 被称为《代理对象》
const proxyProduct = new Proxy(product, {
// 监听 proxyProduct 的 set 方法,在 proxyProduct.xx = xx 时,被触发
// 接收四个参数:被代理对象 tager,指定的属性名 key,新值 newVal,最初被调用的对象 receiver
// 返回值为一个 boolean 类型,true 表示属性设置成功
set(target, key, newVal, receiver) {
// 为 target 附新值
target[key] = newVal
// 触发 effect 重新计算
effect()
return true
},
// 监听 proxyProduct 的 get 方法,在 proxyProduct.xx 时,被触发
// 接收三个参数:被代理对象 tager,指定的属性名 key,最初被调用的对象 receiver
// 返回值为 proxyProduct.xx 的结果
get(target, key, receiver) {
return target[key]
}
})
// 总价格
let total = 0;
// 计算总价格的匿名函数
let effect = () => {
total = proxyProduct.price * proxyProduct.quantity;
};
// 第一次打印
effect();
console.log(`总价格:${total}`); // 总价格:20
</script>
在以上代码中,我们可以发现,Proxy
和 Object.defineProperty
存在一个非常大的区别,那就是:
proxy
:Proxy
将代理一个对象(被代理对象),得到一个新的对象(代理对象),同时拥有被代理对象中所有的属性。- 当想要修改对象的指定属性时,我们应该使用 代理对象 进行修改
- 代理对象 的任何一个属性都可以触发
handler
的getter
和setter
Object.defineProperty
:Object.defineProperty
为 指定对象的指定属性 设置 属性描述符- 当想要修改对象的指定属性时,可以使用原对象进行修改
- 通过属性描述符,只有 被监听 的指定属性,才可以触发
getter
和setter
所以当 vue3
通过 Proxy
实现响应性核心 API
之后,vue
将 不会 再存在新增属性时失去响应性的问题。
07:proxy的最佳拍档:Reflect — 拦截 js 对象操作
当我们了解了 Proxy 之后,那么接下来我们需要了解另外一个 Proxy 的 “伴生对象”:Reflect Reflect 属性,多数时候会与 proxy 配合进行使用在 MDN Proxy 的例子中,Reflect 也有对此出现。 那么 Reflect 的作用是什么呢?
查看 MDN
的文档介绍,我们可以发现 Reflect
提供了非常多的静态方法,并且很巧的是这些方法与 Proxy
中 Handler
的方法类似:
Reflect 静态方法
Reflect.get(target, propertyKey[, receiver\])
Reflect.has(target, propertyKey)
Reflect.set(target, propertyKey, value[, receiver\])
...
handler 对象的方法
...
我们现在已经知道了 handler
中 get
和 set
的作用,那么 Reflect
中 get
和 set
的作用是什么呢?
我们来看一下代码:
<script>
const obj = {
name: '张三'
}
console.log(obj.name) // 张三
console.log(Reflect.get(obj, 'name')) // 张三
</script>
由以上代码可以发现,两次打印的结果是相同的。这其实也就说明了 Reflect.get(obj, 'name')
本质上和 obj.name
的作用 相同
那么既然如此,我们为什么还需要 Reflect
呢?
根据官方文档可知,对于 Reflect.get 而言,它还存在第三个参数 receiver
,那么这个参数的作用是什么呢?
根据官网的介绍为:
如果
target
对象中指定了getter
,receiver
则为getter
调用时的this
值。
什么意思呢?我们来看以下代码:
<script>
const p1 = {
lastName: '张',
firstName: '三',
// 通过 get 标识符标记,可以让方法的调用像属性的调用一样
get fullName() {
return this.lastName + this.firstName
}
}
const p2 = {
lastName: '李',
firstName: '四',
// 通过 get 标识符标记,可以让方法的调用像属性的调用一样
get fullName() {
return this.lastName + this.firstName
}
}
console.log(p1.fullName) // 张三
console.log(Reflect.get(p1, 'fullName')) // 张三
// 第三个参数 receiver 在对象指定了 getter 时表示为 this
console.log(Reflect.get(p1, 'fullName', p2)) // 李四
</script>
在以上代码中,我们可以利用 p2
作为第三个参数 receiver
,以此来修改 fullName
的打印结果。即:此时触发的 fullName
不是 p1
的 而是 p2
的。
那么明确好了这个之后,我们再来看下面这个例子:
<script>
const p1 = {
lastName: '张',
firstName: '三',
// 通过 get 标识符标记,可以让方法的调用像属性的调用一样
get fullName() {
return this.lastName + this.firstName
}
}
const proxy = new Proxy(p1, {
// target:被代理对象
// receiver:代理对象
get(target, key, receiver) {
console.log('触发了 getter');
return target[key]
}
})
console.log(proxy.fullName);
</script>
在以上这个代码中,我问大家,此时我们触发了 prox.fullName
,在这个 fullName
中又触发了 this.lastName + this.firstName
那么问:getter 应该被触发几次?
此时 getter
应该被触发 3 次! ,但是 实际只触发了 1 次! 。为什么?
可能有同学已经想到了,因为在 this.lastName + this.firstName
这个代码中,我们的 this
是 p1
,而非 proxy
!所以 lastName
和 firstName
的触发,不会再次触发 getter
。
那么怎么办呢?我们如何能够让 getter
被触发三次?
想要实现这个想过,那么就需要使用到 Reflect.get
了。
我们已知,Reflect.get
的第三个参数 receiver
可以修改 this
指向,那么我们可不可以 利用 Reflect.get 把 fullName 中的 this 指向修改为 proxy,依次来达到触发三次的效果?
我们修改以上代码:
const proxy = new Proxy(p1, {
// target:被代理对象
// receiver:代理对象
get(target, key, receiver) {
console.log('触发了 getter');
+ // return target[key]
+ return Reflect.get(target, key, receiver)
}
})
修改代码之后,我们发现,此时 getter
得到了三次的触发!
总结
本小节的内容比较多,但是核心其实就是在描述一件事情,那就是 Reflect
的作用,我们为什么要使用它。
最后做一个总结:
当我们期望监听代理对象的 getter
和 setter
时,不应该使用 target[key]
,因为它在某些时刻(比如 fullName
)下是不可靠的。而 应该使用 Reflect
,借助它的 get
和 set
方法,使用 receiver(proxy 实例)
作为 this
,已达到期望的结果(触发三次 getter
)。
08:总结
在本章中,我们首先了解了 JS
的程序性,我们知道了默认情况下,JS
是非常死板的,所以如果想要让程序变得更加 “聪明” 那么需要额外做一些事情。
通常我们有两种方式可以监听 target
的 getter
和 setter
分别是:
Object.defineProperty
:这是vue2
的响应式核心API
,但是这个API
存在一些缺陷,他只能监听 指定对象 的 指定属性 的getter
和setter
。所以在 “某些情况下”,vue2
的对象或数组会失去响应性。Proxy
:这是vue3
的响应式核心API
。该API
表示代理某一个对象。代理对象将拥有被代理对象的所有属性和方法,并且可以通过操作代理对象来监听对应的getter
和setter
。
最后如果我们想要 “安全” 的使用 Proxy
,还需要配合 Reflect
一起才可以,因为一旦我们在 **被代理对象的内部,通过 this
触发 getter
和 setter
** 时,也需要被监听到。