6 min read

Reactivity-fundamentals

Vue 3 Reactivity Fundamentals
Reactivity-fundamentals

1. 为什么需要响应式(Reactivity)?

Vue 通过依赖追踪(dependency tracking)机制来实现响应式。当组件使用到某个响应式数据时,就会追踪到对该数据的访问。一旦该数据更新,会触发对应组件的重新渲染。

传统 JavaScript 里无法直接监听纯变量(primitive)修改,因此 Vue 提供了 ref() / reactive() 等 API,让 Vue 可以“拦截”数据的读取和写入,从而自动触发视图更新。

2. ref() API

2.1 基本用法

​ • 声明一个响应式变量

import { ref } from 'vue'



const count = ref(0)



count 是一个含有 .value 属性的对象,当我们想要获取/修改 count 的值时,要通过 count.value 来访问或赋值。



​	•	**在模板中使用**:


```javascript
<template>
  <div>{{ count }}</div>
</template>

模板中可以直接使用 count,无需写 .value。Vue 会在模板中进行“自动解包(unwrapping)”。

​ • 事件中修改值

<template>
 <button @click="count++">
  {{ count }}
 </button>
</template>

在模板中自增会自动触发视图更新。

2.2 在 <script setup> 中简化用法

<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
 count.value++
}
</script>
<template>
 <button @click="increment">
  {{ count }}
 </button>
</template>

​ 1. <script setup> 会自动将脚本中声明的变量暴露给同一组件的模板。

​ 2. 我们无需显式地在 setup() 函数中 return {}。

2.3 为什么要使用 .value?

ref() 返回的是一个带有 getter/setter 的对象,Vue 可以借助这些 getter/setter 来追踪数据的访问和更新并触发对应的渲染逻辑。

3. reactive() API

3.1 基本用法

​ • 将一个对象转化为响应式

import { reactive } from 'vue';
const state = reactive({ count: 0 });

它返回一个 代理对象(Proxy),对该对象属性的访问、修改会被 Vue 拦截,从而实现响应式。

​ • 在模板中使用

<template>
 <button @click="state.count++">
  {{ state.count }}
 </button>
</template>

3.2 注意点

​ 1. 只能处理对象类型:对字符串、数字、布尔值等基础类型无效。

​ 2. 无法直接替换整个对象:示例如下,这样会丢失之前的响应式连接:

let state = reactive({ count: 0 });

state = reactive({ count: 1 }); // 这样写会导致前一个对象的响应式失效

​ 3. 解构会丢失响应式:如果写 let { count } = state,那么 count 就只是一个普通的值,不再与 state.count 同步。

3.3 reactive() 与 ref() 的关系

​ • 当 ref() 接收的参数是一个对象时,Vue 内部会使用 reactive() 将这个对象转换为响应式。

​ • 当我们给一个响应式对象添加一个 ref() 作为属性时,Vue 会自动解包这个 ref(),在这个属性上直接读写值,无需再通过 .value。

4. 深层响应式(Deep Reactivity)

​ • 深度追踪:无论是 ref() 还是 reactive(),默认都会对内部嵌套对象或数组进行深度监听。

​ • 性能优化:如果不需要深度监听,可以使用 shallowRef() 或 shallowReactive() 来节省性能开销。

5. DOM 更新时机

Vue 不会在每一次状态改变后立刻更新 DOM,而是将多次变更进行合并,在下一个“更新循环(next tick)”时进行统一更新。

如果需要在状态更新后做某些操作(例如获取最新的 DOM 元素尺寸),可以使用:

import { nextTick } from 'vue';
async function increment() {
	count.value++;
	await nextTick();
	// 此处 DOM 已更新
}

6. 在数组或原生集合中的 Caveat

​ 1. 数组或原生集合(Map/Set 等)内部的 ref 不会被自动解包:

const books = reactive([ref('Vue 3 Guide')]);
console.log(books[0].value); // 需要手动 .value

​ 2. 对象属性会自动解包,但数组或集合元素不会。

7. 在模板中的自动解包(Unwrapping)注意事项

​ • 顶层属性(如直接 {{ count }})可以自动解包 ref。

​ • 嵌套属性(如 object.id)不会自动解包,可能会展示 [object Object]。必须将其解构到顶层或在模板里使用 .value。

示例:

const object = { id: ref(1) }
// 这样写会渲染成 [object Object]1
<template>
 {{ object.id + 1 }}
</template>

// 可解构后:
const { id } = object
<template>
 {{ id + 1 }} // 2
</template>

8. 常见面试问题

​ 1. ref() reactive() 的区别是什么?

​ • ref() 适用于单个值(含基础类型或对象),访问时需 .value;

​ • reactive() 适用于对象类型;它返回一个代理对象,深度监听所有属性。

​ 2. 为什么在 JS 里要用 .value**,但模板中可以省略** .value**?**

​ • 因为 Vue 需要拦截 getter/setter;在模板中,Vue 已帮我们自动解包了 ref。

​ 3. 深度监听带来的性能影响如何解决?

​ • 可以使用 shallowRef() 或 shallowReactive(),或在特定业务场景中按需处理。

​ 4. 把响应式对象重新赋值会怎样?

​ • 原响应式对象的“连接”会丢失,最佳实践是始终使用同一个对象引用。

​ 5. 解构会发生什么?

​ • 解构会让数据失去响应式能力,必须通过 toRefs() 等方式才能保留响应式。

9. 总结

​ • ref():可处理任意类型,主要针对基础类型数据;返回带 .value 的对象,内部使用 reactive() 实现对象的深度监听。

​ • reactive():可处理对象(包含对象、数组、Map、Set 等),返回 Proxy;深度监听其属性变动。

​ • 自动解包:在模板中访问顶层 ref 时自动解包,不需要 .value;但在深层嵌套或数组/Map 中需要手动 .value。

​ • 异步更新:Vue 将多次状态改变合并到下一个 “tick” 中进行 DOM 更新,可使用 nextTick() 等待 DOM 更新完成后再进行操作。

这些响应式机制为 Vue 提供了强大的数据驱动视图的能力,也是 Vue 设计的核心理念之一。在实际开发中,灵活运用 ref() 与 reactive() 可以让你的组件状态管理更加简洁高效。

建议:初学时以 ref() 为主,通过给对象赋值来存储复杂数据;当需要一次性处理多个深层属性时,再使用 reactive()。掌握好自动解包和异步更新机制,有助于避免一些常见的坑。

参考链接:

​ • Vue 官方文档 - Reactivity Fundamentals

​ • Vue 官方文档 - Reactivity in Depth