对 Vue 响应式的理解
约 983 字大约 3 分钟
2025-08-05
响应式对象,首先得是个对象
要想实现一个原始数据的响应式,最重要的就是实现对原始数据的监听、相关依赖的收集、组件和原始数据之间订阅关系的维护。
也就是说,响应式对象,首先得是个对象。只有对象才能被挂载上 Observer
、dep
、subs[]
这些和依赖、订阅有关的属性。
不管是 Vue 2 的 Object.defineProperty()
,还是 Vue 3 的 Proxy()
,都是通过 对象 来实现响应式,其实核心思想都是:
- 在
getter
操作里完成依赖的收集,组件和数据之间订阅关系的绑定。 - 在
setter
操作里对新值进行重新包装、依赖更新、订阅通知。
而基本类型,无疑需要进行特殊处理,才能实现响应式:
- Vue 2 中,在处理
data
对象的时候,基本类型作为data
对象的属性,顺带实现了响应式的封装。 - Vue 3 中,基本类型被一个对象包裹,这也就解释了为什么通过
.value
来访问实际值。
ref 是对 reactive 的包装
在 Vue 3 中, 我们在使用 ref
去定义一个对象的时候,通过查看返回结果就可以看出:ref
是对 reactive
的包装。
const person = ref({ name: "lisi", age: 12 });
console.log(person.value);
很显然,使用 ref
去包装一个特别复杂、层级特别深的对象,每次引用都需要通过 .value
来访问,并不是一个很好的做法。我个人更推荐直接使用 reactive
去包装一个复杂对象。
const person = ref({
name: "lisi",
age: 12,
skills: [
{ id: 1, name: "Java" },
{ id: 2, name: "Vscode" },
],
});
console.log(person.value.skills[0].name);
当然细心的话,你会发现官网有一句 "由于这些限制,我们建议使用 ref()
作为声明响应式状态的主要 API"。在实际开发中,我们可以通过值引用、浅拷贝的方式简化引用代码。
let skill = person.value.skills[0];
console.log(skill.name);
你喜欢哪个就用哪个
具体项目中,使用 ref
还是 refactive
还是得看个人习惯和项目组的风格。虽然 ref 是多了一层壳,但是实际开发中,这点代价对代码性能影响不大。所以说,你喜欢哪个,就用哪个。
属性值的替换
reactive
不支持整个替换
在 Vue 2,得益于对 data
的处理,当我们替换某个对象的值时,可以直接整个替换,不会破坏其原有的响应式结构。
function changePerson1() {
// this 本身就是个响应式对象。
this.person = { name: "lisi", id: "2" };
}
但是在 vue3 中,使用 reactive
包装的对象是不能直接使用该方式去修改的。
<template>
<div class="person">
<div>{{ person.name }}</div>
<button @click="changeName1">方式 1</button>
<button @click="changeName2">方式 2</button>
<button @click="changeName3">方式 3</button>
</div>
</template>
<script setup lang="ts">
import { defineOptions, ref, reactive, toRefs } from "vue";
let person = reactive({ name: "xiaohong", id: "1" });
// 方式 1
function changeName1() {
person = { name: "zhangsan", id: "2" };
}
// 方式 2
function changeName2() {
let person1 = person;
person = reactive({ name: "lisi", id: "2" });
console.log(person1 === person); // false
}
// 方式 3
function changeName3() {
person1 = Object.assign(person1, { name: "zhangsan", id: "2" });
}
</script>
在上述代码中:
- 方式 1 会让 person 失去响应式,得到的是一个新对象。
- 方式 2 的做法虽然也可以得到一个响应式对象,但是新旧两份数据不相等,也就意味着组件引用该数据的地方,会失去该数据的订阅,导致无法刷新。
- 方式 3 利用了
Object.assign()
的浅拷贝特性,恰好保持了原有的响应式结构、依赖、订阅关系。
ref.value
支持替换整个对象
由于 ref
会将原始数据包裹一层对象,当我们整个替换时,恰好能保持原有的响应式引用。
let person = ref({
name: "xiaohong",
id: "1",
});
person.value = { name: "zhangsan", id: "2" };
当然你不可能傻到使用 person = { name: "zhangsan", id: "2" }
去修改 🙄
更新日志
e7112
-1于