vue 3 源码设计方案:从源码设计角度看 reactivity 响应性变更
让更多的人以更简单的方式学习 vue 3 源码
《vue 3 源码设计方案》系列文章,关于本系列的详细描述,可以点击 这里 ,进行查看。
我们将尝试公开一个 vue 3
的源码学习库 vue-next-mini
,点击这里直达。该库可以帮助你快速的学习和掌握 vue 3
源代码的内容,不妨看看~~~~
本节博客,主要讲解 reactivity
响应性,以 vue 2
的响应性核心 API
Object.defineProperty 作为入口,分析其使用方案,以及缺陷。从而引出 vue 3
的响应性核心 API
Proxy
那么下面我们开始~~~
响应式数据
在 vue
的项目开发中,我们一直在接触 响应性 数据,我们知道 vue
根据使用方式的不同,生成响应性数据有以下几种方式:
1. options API
export default {
data () {
return {
msg: '你好,世界'
}
}
}
2. composition API
<script setup>
import { reactive, ref } from 'vue'
const msg = reactive({
msg: '你好,世界'
})
const msgRef = ref('你好,世界')
</script>
那么大家有没有思考过一个问题,为什么这些数据被叫做响应性数据?什么是非响应性数据呢?
根据我们之前的学习经验可知,所为响应性数据指的就是:数据的改变会引起视图的变化,此类数据就被称作响应性数据。同理,数据的改变不会引起视图变化的数据,就被叫做非响应性数据。
属性标志和属性描述符
我们知道,对象可以存储属性。
到目前为止,属性对我们来说只是一个简单的“键值”对。但对象属性实际上是更灵活且更强大的东西。
大家应该还记得,我们之前提到过一个 API
Object.defineProperty() ,它的第三个参数为 descriptor
表示 <要定义或修改的属性描述符>。
对象属性的描述符有很多,我们可以通过 Object.getOwnPropertyDescriptor() 来获取它。
当我们用“常用的方式”创建一个属性时,它们都为 true
。但我们也可以随时更改它们。
查看下面的例子:
<script>
const user = {
name: '张三'
}
const descriptor = Object.getOwnPropertyDescriptor(user, 'name')
console.log(JSON.stringify(descriptor)); // {"value":"张三","writable":true,"enumerable":true,"configurable":true}
</script>
以上属性描述符所代表的意思分别为:
value
— 当前属性的具体值writable
— 如果为true
,则值可以被修改,否则它是只可读的。enumerable
— 如果为true
,则会被在循环中列出,否则不会被列出。configurable
— 如果为true
,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。
我们可以利用 Object.defineProperty()
修改该 属性 的属性描述符:
Object.defineProperty(user, 'name', {
// name 属性不可被修改,变为只读的
writable: false,
// name 属性不可被枚举
enumerable: true,
// 不可再通过 Object.defineProperty 修改属性描述符,也不可以删除该属性
configurable: false
})
到目前为止,我们知道可以通过 Object.defineProperty
修改一个对象的属性描述符,每个属性描述符都对应不同的作用。
那么对于属性的描述符而言,除了以上列举出来的 4 个之外,还有另外两个非常关键的属性描述符 get
和 set
。
那么这两个属性描述符的作用是什么呢?我们继续来看。
getter
和 setter
的概念
到目前为止,我们了解了什么是响应性数据,什么是属性描述符。
并且我们知道在属性描述符中有两个非常重要的描述符:get
、set
。它们是两个函数,也被叫做 访问器属性(accessor property) 。它们本质上是用于获取和设置值的函数。
这两个访问器属性是把非响应性数据变为响应性数据的关键。
那么具体怎么做呢?我们继续往下看。
对于任何 对象的属性 而言,都存在两个行为:
getter
行为setter
行为
getter 行为
比如我们可以通过 obj.name
的形式,来访问 name
属性。那么此时 obj.name
的触发,本质上就是一个 getter
行为
setter 行为
比如我们可以通过 obj.name = '李四'
的形式,来为 name
属性赋值。那么此时 obj.name = xxx
的触发,本质上就是一个 setter
行为
我们可以通过访问器属性来监听对象属性的这两个行为:
<script>
const cart = {
price: 10,
num: 5
}
Object.defineProperty(cart, 'num', {
set() {
console.log('num 属性,触发 setter 行为');
},
get() {
console.log('num 属性,触发 getter 行为');
}
})
</script>
此时在浏览器执行 cart.num
或者 cart.num = xxx
的时候,会成功触发对应的 setter
和 getter
。
那么接下来我们期望把 cart.num
变为一个响应性数据,根据我们之前所学,我们知道:数据变化,会引起视图发生变化的数据, 就是响应性数据。
那么我们应该如何利用这样的特性来实现响应性的功能呢?
大家可以思考下这个问题,然后,我们继续来说。
vue 2 中响应性核心 API - Object.defineProperty() 的作用
vue2
的响应性核心 API
,就是通过 Object.defineProperty()
来进行实现的。那么它具体是怎么做的呢?
我们来看如下这段代码:
<body>
<div id="app"></div>
</body>
<script>
// 购物车
const cart = {
price: 10,
num: 5
}
// 总价格
let total = 0
// 计算总价格的方法
const effect = () => {
total = cart.price * cart.num
// 把总价格渲染到页面中
document.querySelector('#app').innerText = `总价格:${total}`
}
effect()
</script>
在上面这段代码中,我们构建了一个购物车对象,对象内部包含 price 价格
和 num 数量
这两个属性。同时我们声明了一个 total
属性,表示总价格。
利用 effect
函数,来计算总价格。并把总价格渲染到 #app
的 div
元素上。
那么如果我们期望 cart.num
变为响应性数据,那么大家可以想象一下,视图应该具备什么样的特性?
是不是只要:当 cart.num
的值发生变化时,div
中显示的总价格可以同步跟随变化,那么 cart.num
就可以被称作为响应性数据了。
如果想要做到这一点,那么我们就必须要满足两个条件:
- 监听到
cart.num
的值发生变化。 - 检测到变化后,需要做什么事情。
那么我们首先来看第一个需求:
第一个需求非常简单,想要监听 cart.num
的值发生变化,只需要通过我们之前学到的访问器属性来 监听 setter
行为 即可。
然后是第二个需求:
而第二个需求就比较难以理解了。我们希望数据变化之后,做些什么事情呢?
有些同学肯定会想,当然是触发 effect
函数了!如果你能够想到这里,那么就证明你已经搞明白了响应性的核心方案。
但是 问题在于,我们为什么要触发 effect
呢?触发 effect
的依据是什么?
仔细观察 effect
函数,可知:在 effect
函数中,我们执行了 cart.num
,即:触发了 getter
行为。
即:因为该函数内,触发了 getter
,所以该函数内将存在依赖数据产生变化的视图,所以我们应该在 setter
行为之后,触发 effect
函数(即:包含 getter
行为的函数)。
那么明确好了以上两点需求之后,接下来进行对应的实现就非常简单了:
<body>
<div id="app"></div>
</body>
<script>
// 购物车
let num = 5
const cart = {
price: 10,
num
}
...
Object.defineProperty(cart, 'num', {
set(newValue) {
console.log('num 属性,触发 setter 行为');
// 注意这里不可以直接使用 cart.num = xxx 进行赋值,因为这样会再次触发 setter ,导致死循环
num = newValue
effect()
},
get() {
console.log('num 属性,触发 getter 行为');
return num
}
})
</script>
此时我们可以在控制台中任意修改 cart.num
的值,可以发现视图跟随发生变化。
在上述代码中,我们把 cart.num
变成了一个响应性的数据,变为响应性数据的方式其实非常简单,主要分为两步:
- 利用
Object.defineProperty
监听对象属性的getter
和setter
行为 - 在触发
setter
行为时,执行触发了getter
行为的函数
Object.defineProperty() 的缺陷
截止到目前位置,我们学习了利用 Object.defineProperty()
实现响应性的功能。同时我们也知道 vue 2
的响应性核心 API
就是依赖于 Object.defineProperty()
进行实现的。
但是我们也知道一点,那就是 vue 在某些情况下会丢失响应性 :
由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。
在这段话的背后,其实就是暴露出来了 Object.defineProperty() 的缺陷
,那么这具体指的是什么意思呢?本小节就为大家解惑。
我们可以利用 vue-cli 创建一个 vue 2
的项目,在 APP.vue
中写入如下代码:
<template>
<div id="app">
<ul>
<li v-for="(val, key, index) in obj" :key="index">
{{ key }} --- {{ val }}
</li>
</ul>
<button @click="addObjKey">为对象增加属性</button>
<div>---------------</div>
<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)
},
addArrItem() {
// 视图不会发生变化,数据会发生变化
this.arr[2] = '王五'
console.log(this.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
是如何解决这些缺陷的呢?
我们继续来往下看~~~~
转载自:https://juejin.cn/post/7142426616952520718