Vue3 笔记:响应式基础
最近翻 Vue3 文档,重新过了一遍响应式的基础内容。之前用的时候总有些细节记不清,比如 ref 和 reactive 到底该怎么选、为什么有时候改了数据页面不更,这次特意整理了笔记(过程中也借助了大模型辅助梳理逻辑、补充细节😉),主要是给自己后续查着方便,内容以复习要点为主,可能有理解不到位的地方,先记下来慢慢修正。
官方文档:响应式基础 | Vue.js (vuejs.org)
一. 声明响应式状态
1. 组合式 API:ref()
定义:ref() 接收参数,并将其包裹在一个带有 .value 属性的 ref 对象中返回:
1 |
|
- 模板使用:无需 .value,自动解包;可直接在事件监听器中修改(如@click=”count++”)。
- 优势:支持所有数据类型(原始值、对象等),可传递给函数并保持响应性。
- 简化语法:在
<script setup>
中,顶层声明的ref和方法可直接在模板使用,无需手动暴露。
注意:在 JavaScript 中需要 .value 来访问和修改 ref 的值。
2. 组合式 API:reactive()
定义:reactive()接收对象 / 数组等,返回其响应式代理(Proxy),直接通过属性访问 / 修改。
1 |
|
特性:代理与原始对象不等价(reactive(raw) !== raw),同一原始对象多次调用reactive()返回同一代理。
二. 核心特性
1. 深层响应式
默认行为:ref()和reactive()均默认实现深层响应性,嵌套对象 / 数组的修改会被追踪。
1 |
|
浅层响应性:可通过shallowRef(仅.value访问被追踪)或shallowReactive(仅顶层属性响应式)关闭深层响应性,优化性能。
2. DOM 更新时机
异步更新:Vue 会在“next tick”更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。
等待更新:需在 DOM 更新后执行代码时,使用nextTick():
1 |
|
三. reactive() 的局限性
reactive() API 有一些局限性:
有限的值类型:它只能用于对象类型 (对象、数组和如 Map、Set 这样的集合类型)。它不能持有如 string、number 或 boolean 这样的原始类型。
不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失。
对解构操作不友好:解构属性为本地变量或传递给函数时,响应性连接断开。
Vue官方建议:优先使用ref()作为响应式状态声明的主要 API。
四. ref 解包细节
作为 reactive 对象的属性:自动解包,行为类似普通属性。
1 |
|
数组 / 集合中:不会自动解包,需显式使用.value。
1 |
|
模板中:仅顶级属性自动解包,嵌套属性需解构为顶级属性才能解包。
1 |
|
模板中{{ id + 1 }}
生效(解包),而{{ object.id + 1 }}
不生效(未解包)。
如果 ref 是文本插值的最终计算值 (即 {{ }}
标签),会自动解包。该特性仅仅是文本插值的一个便利特性,等价于 {{ object.id.value }}
。
五、关键问题
为什么 ref 需要 .value 访问?
因为在标准的 JavaScript 中,检测普通变量的访问和修改是行不通的。但可以通过 getter 和 setter 方法来拦截对象属性的 get 和 set 操作。
而 ref 的设计思路是:用一个对象包裹原始值,这个对象只暴露一个 .value
属性。通过为 .value
定义 getter 和 setter,Vue 就能在你访问 xxx.value
时追踪依赖(收集谁在用这个值),在你修改 xxx.value = ...
时触发更新(通知用到这个值的地方重新渲染)。
简单说,.value
是 Vue 为了让原始类型也能具备响应式,而 “绕的一小步”—— 通过对象属性的拦截能力,间接实现对原始值的追踪。
ref()和reactive()的核心区别是什么?
① 支持类型:ref()支持所有数据类型(原始值、对象等),reactive()仅支持对象、数组等非原始类型;
② 访问方式:ref()需通过.value访问 / 修改值,reactive()直接通过属性访问;
③ 局限性:reactive()存在无法替换对象、解构丢失响应性等问题,ref()无这些限制;
④ 解包规则:ref()在模板中自动解包,作为响应式对象属性时也自动解包,而reactive()无类似解包逻辑。
为什么修改响应式状态后 DOM 没有立即更新?如何确保在 DOM 更新后执行代码?
Vue 会将所有状态修改缓冲到 “next tick” 更新周期中,确保每个组件只更新一次,提升性能。若需在 DOM 更新后执行代码,可使用nextTick()全局 API,它返回一个 Promise,在 DOM 更新完成后 resolve。示例:
1 |
|
在模板中使用ref时,为什么有时需要显式.value,有时不需要?
模板中ref的解包规则为:
① 顶级属性自动解包(无需.value),如const count = ref(0)
在模板中{{ count }}
生效;
② 嵌套属性(如object.id
,其中id是ref)不会自动解包,需解构为顶级属性(const { id } = object
)才能解包;
③ 若ref是文本插值的最终值(如{{ object.id }}
),会自动解包(等价于{{ object.id.value }}
)。因此,非顶级嵌套ref在模板中参与计算时需先解构,否则需显式处理。
浅层响应性的核心适用场景是什么?
核心适用场景:数据 “大而不变” 或 “仅需整体替换”
当数据满足以下特征时,用浅层响应性可以显著提升性能:
处理 “大型不可变数据”(如后端返回的海量列表)
场景:从后端获取的大型列表(如 1000 条以上数据),且业务中只需要展示、不需要修改其中的嵌套属性(仅可能整体替换列表)。
问题:如果用普通ref或reactive,Vue 会递归地将所有嵌套属性转为响应式(创建大量 Proxy),导致初始化时性能开销大。
解决方案:用shallowRef,只追踪.value的整体替换,不处理内部属性:
1 |
|
管理 “纯展示性的复杂对象”(如配置项、图表数据)
场景:页面中的配置对象(如表单布局配置、图表的 option),结构复杂但运行中不会修改嵌套属性,只会整体替换。
问题:深层响应性会对嵌套的每个对象 / 数组创建 Proxy,而这些 Proxy 完全用不上,属于浪费。
解决方案:用shallowReactive(对象)或shallowRef(整体替换):
1 |
|
手动控制更新时机(避免频繁触发)
场景:需要批量修改数据,且希望 “修改完所有内容后再统一更新 DOM”,而不是每改一个属性就更新一次。
问题:普通响应式会在每次修改时触发更新,批量操作时可能导致多次无用渲染。
解决方案:用shallowRef配合triggerRef(手动触发更新):
1 |
|
不适用场景:警惕 “过度优化”
浅层响应性的 “性能优化” 是有代价的 —— 丢失了深层追踪能力,因此以下场景绝对不能用:
- 数据需要修改嵌套属性(如用户信息对象{ user: { name: ‘xxx’ } },需要修改name);
- 数据结构简单(如仅包含 1-2 层的小对象),此时深层响应性的性能开销可忽略,没必要用浅层;
- 新手对响应式原理不熟悉,容易因 “修改不触发更新” 导致 bug。
浅层响应性是 “按需关闭深层追踪” 的优化手段,核心适用场景是:
数据结构复杂但仅需整体替换,或嵌套属性完全不需要修改。
它的设计不是为了 “替代” 普通响应式,而是在特定场景下(如处理大型数据)减少不必要的性能消耗,属于 “进阶优化技巧”,需结合具体业务判断是否使用。