Vue 3.2 源码系列:02-面试必会的《响应式设计原则》
序
博客代码已上传至github
点击这里 即可访问
另提供:完整代码(ts+rollup)和视频教程
回顾
上一篇文章中咱们一起了解了 vue 框架设计理念,根据上一篇博客咱们知道了:
- 命令式
- 声明式
- 运行时
- 编译器
- 副作用
对应的概念,不熟悉的小伙伴们,可以点击 这里 进行查看。
正文开始
本篇博客要说明的问题
在咱们日常的工作,特别是 面试 中,面试官经常会问到 vue 响应式设计 相关的问题,在前端越来越卷的今天,如果你连 vue 响应式设计 都回答不出,那么这个工作基本上就与你无缘了。
所以说,本篇博客的目的就是为了给大家说清楚、捋顺溜 vue 响应式设计原则,以帮助看过本篇博客的小伙伴顺利通过 vue 响应式设计 相关的问题面试。
全篇文章大约 3000 字,我们会从:
- JS程序性 开始
- 讲到 vue 2 响应式核心
- 分析 vue2 响应性问题
- 引出 vue3 响应性核心
按照以上的顺序,进行讲解。
那么废话不多说,本篇文章内容正式开始~~
JS 的程序性
从我们学习 JS
的第一天就知道,JS
是图灵完备的编程语言。所以JS
将具备完善的程序性。
那么什么是 程序性 呢?下面咱们通过一段代码来进行明确:
// 定义一个商品对象,包含价格和数量
let product = {
price: 10,
quantity: 2
}
// 总价格
let total = product.price * product.quantity;
// 第一次打印
console.log(`总价格:${total}`);
// 修改了商品的数量
product.quantity = 5;
// 第二次打印
console.log(`总价格:${total}`);
在上面的代码中:
- 首先:咱们定义了一个
商品对象 product
,它包含price 价格
、quantity 数量
两个属性 - 然后:咱们定义了
total 总价格
,总价格的计算等于商品的单价 * 商品的数量
- 此时:打印
total
应该为 20 元 - 然后:我们尝试修改
quantity 商品数量
,把它的值修改为 5 - 最后:再对
total
进行打印,那么问大家:此时 total 的值是什么呢?
相信看到这里,大家不免会想:“你这是把我们当小白了吧,这谁不会啊!肯定还是 20 啊!”
恭喜大家!答对了! 第二次的打印结果依然为 20。但是!,大家有没有思考过一个问题,那就是:商品数量发生变化之后,total 真的应该还是 20 吗?
此时你有没有冒出来一个想法:如果商品数量发生变化之后,总价格 total 可以自动重新计算,那就太好了!
但是 js
本身具备 程序性,所谓程序性指的就是:一套固定的,不会发生变化的执行流程 。
在这样的一个程序性之下,是 不可能 重新发生计算的。
那么如果我们想要拿到这个 50(正确的值)
就必须要让你的程序变得更加的 “聪明”,也就是使其具备 响应性!
让你的程序变得更加“聪明”
你为了让你的程序变得更加 "聪明" , 所以你开始想:”如果数据变化了,程序可以重新执行运算就好了“。
那么怎么去做呢?你进行了一个这样的初步设想:
- 创建一个函数
effect
,在其内部封装 计算总价格的表达式 - 在第一次打印总价格之前,执行
effect
方法 - 在第二次打印总价格之前,执行
effect
方法
那么这样我们是不是就可以在第二次打印时,得到我们想要的 50
了呢?
所以据此,你得到了如下的代码:
// 定义一个商品对象,包含价格和数量
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
在上面的代码中:
- 首先:你封装了一个函数
effect
,这个函数可以计算总价格 - 然后:你分别在每次打印
total
前,执行effect
方法,得到最新的总价格
依靠这样的代码,你成功的在第二次打印时得到了我们期望的结果:数据变化了,运算也重新执行了。
太 Beautiful 了~~
但是很快你就发现了一个问题,那就是:必须主动的在数量发生变化之后,重新主动执行 effect
,这样才可以得到想要的结果。
那么这样未免太麻烦了。有什么好的办法吗?
vue 2 的响应性核心 API:Object.defineProperty
你通过主动调用
effect
方法的形式,成功得到了正确的结果。但是你也发现了这样做的一些问题。所以你开始想:如果不需要主动调用
effect
方法,那就太好了!
你冥思苦想之后,发现:想要达到你的需求,那么可以借助 Object.defineProperty 进行实现。
vue2
以 Object.defineProperty 作为响应性的核心 API
,该 API
可以监听:指定对象的指定属性的 getter
和 setter
那么接下来咱们就可以借助该 API
,让我们之前的程序进行 自动计算,该 API
接收三个参数:
- 指定对象
- 指定属性
- 属性描述符对象
得出代码如下:
// 定义一个商品对象,包含价格和数量
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
}
});
在以上代码中:
- 首先:把
product
进行了拆分,主要是把quantity
拆分了出来,这样做的目的是为了:setter 和 getter 行为的重复触发 - 然后:在第一次打印时,同样触发了
effect
方法,得到第一次打印的总价格 - 重点:利用
Object.defineProperty
方法,监听了product
中quantity
属性的getter
和setter
行为。 当触发set
方法时,会调用 effect() ,从而达到quantity
被赋值,重新计算的目的。
现在,你已经成功了利用 Object.defineProperty
方法,完成了基本的响应性。但是这种方法依然不是太好,为什么呢?
Object.defineProperty 在设计层的缺陷
Object.defineProperty 存在缺陷:它只能监听 指定对象的、指定属性的 getter 和 setter
在 vue 官网中存在这样的一段描述 :
由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。
由于 JavaScript 的限制,Vue 不能检测数组和对象的变化 这是什么意思呢?
咱们通过一段基本的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>
在上面的代码,咱们分别通过:
- 为对象增加属性
- 为数组增加属性
这么两个例子,来验证了 新增属性 下的响应性,经过验证可以发现:
- 当为 对象 新增一个没有在
data
中声明的属性时,新增的属性 不是响应性 的 - 当为 数组 通过下标的形式新增一个元素时,新增的元素 不是响应性 的
那么为什么会这样呢?
想要搞明白这个原因,那就需要明白官网所说的 由于 JavaScript 的限制 指的是什么意思。
我们知道:
vue 2
是以Object.defineProperty
作为核心API
实现的响应性Object.defineProperty
只可以监听 指定对象的指定属性的 getter 和 setter- 被监听了
getter
和setter
的属性,就被叫做 该属性具备了响应性
那么这就意味着:我们 必须要知道指定对象中存在该属性,才可以为该属性指定响应性。
但是 由于 JavaScript 的限制,我们没有办法监听到 指定对象新增了一个属性,所以新增的属性就没有办法通过 Object.defineProperty
来监听 getter
和 setter
,所以 新增的属性将失去响应性。(PS:那么如果想要增加具备响应性的新属性,那么可以通过 Vue.set 方法实现)
那么此时,你已经明确了 Object.defineProperty
所存在的问题,也知道通过 Object.defineProperty
实现响应性可能会存在缺陷,那么怎么办呢?
vue3的响应性核心 API:proxy
Object.defineProperty
的缺陷让你寝食难安,直到你发现了Proxy
~~~~
当你知道了 Object.defineProperty
的缺陷之后,你迫切的期望想要修正它。这时你注意到了一个新的API
: Proxy。
proxy
顾名思义就是 代理 的意思。我们来看如下代码:
// 定义一个商品对象,包含价格和数量
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
在以上代码中,我们可以发现 Proxy
和 Object.defineProperty
存在着非常大的区别,那就是:
-
proxy
:Proxy
将代理一个对象(被代理对象),得到一个新的对象(代理对象),同时拥有被代理对象中所有的属性。- 当想要修改对象的指定属性时,我们应该使用 代理对象 进行修改。
- 代理对象 的任何一个属性都可以触发
handler
的getter
和setter
。
-
Object.defineProperty
:Object.defineProperty
为 指定对象的指定属性 设置 属性描述符。- 当想要修改对象的指定属性时,可以使用原对象进行修改。
- 通过属性描述符,只有 被监听 的指定属性,才可以触发
getter
和setter
。
现在你成功的利用proxy
解决了Object.defineProperty
所带来的的缺陷,真是好极了~~
总结
在本篇博客成,咱们按照:
- JS程序性
- vue 2 响应式核心
- vue2 响应性问题
- vue3 响应性核心
的顺序对 vue 响应性
进行了分析,期望大家可以在看完本篇博客的内容之后,达到文章开头我们所提到的目的:顺利通过 vue 响应式设计 相关的问题面试。
谢谢大家阅读~~~
转载自:https://juejin.cn/post/7187285471230165051