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()。掌握好自动解包和异步更新机制,有助于避免一些常见的坑。
参考链接: