Vue3 响应式系统深度解析:ref 与 reactive
Vue3 响应式系统深度解析:ref 与 reactive
概述
Vue3 的响应式系统是整个框架的核心基石。理解 ref 和 reactive 的底层原理与适用场景,是写出高效、无 bug 的 Vue3 代码的前提。本文深入解析两种响应式创建方式的区别、实现机制、常见陷阱以及企业级项目中的最佳实践。
为什么响应式系统如此重要
Vue3 宣称"反直觉的变更检测"已成为历史。在 Vue2 中,由于 JavaScript 限制,某些操作(如数组索引赋值 arr[0] = newValue、添加新属性 obj.newKey = value)不会触发视图更新。Vue3 通过 ES6 Proxy 彻底解决了这个问题,但前提是你必须正确使用响应式 API。
基础语法
import { ref, reactive, toRefs, isRef, unref, shallowRef, triggerRef } from 'vue'
| API | 入参类型 | 返回值 | 特点 |
|-----|---------|--------|------|
| ref | 任意基本类型或对象 | Ref<T> 对象 | 基本类型需 .value,模板自动解包 |
| reactive | 对象/数组( Plain Object) | Proxy<T> 深度代理 | 直接操作属性,浅层解构丢失响应式 |
| toRefs | reactive 对象 | 每个属性转为 ref | 把 reactive 对象转 ref 集合,避免解构丢失 |
| isRef | 任意值 | boolean | 判断某值是否为 ref 对象 |
| unref | ref 或普通值 | 原始值 | 自动解包,ref 返回 .value,否则原样返回 |
完整代码示例
<script setup>
import { ref, reactive, toRefs, isRef, unref, shallowRef, triggerRef, computed, watch } from 'vue'
// ========== ref 的使用 ==========
// ref 包装基本类型
const count = ref(0)
const message = ref('Hello Vue3')
const isLoading = ref(false)
// 访问/修改 ref 的值需要 .value(模板中无需 .value)
function increment() {
count.value++
}
// ref 也可以包装对象,Vue 会自动深层递归响应式处理
const user = ref({ name: '林小鸣', age: 28, scores: [90, 85, 92] })
function updateUser() {
user.value.age = 29 // 直接修改属性,响应式
user.value.scores.push(88) // 数组操作也是响应式的
user.value.newField = '新增字段' // 新增属性同样响应式(Vue3 的 Proxy 优势)
}
// ========== reactive 的使用 ==========
// reactive 接收普通对象,返回深度代理
const state = reactive({
title: 'Vue3 学习笔记',
count: 0,
metadata: {
author: '林小鸣',
tags: ['Vue3', 'JavaScript', '前端'],
stats: { views: 1024, likes: 128 }
}
})
// 直接访问/赋值属性
function addCount() {
state.count++
}
// ========== ref vs reactive 的关键区别 ==========
// 1. reactive 对象解构会丢失响应式
const { title, count: dCount } = state // dCount 和 title 是普通值,非响应式
// 正确做法:用 toRefs 转换
const { title: t, count: c } = toRefs(state) // t 和 c 是 ref,保持响应式
// 使用时:t.value 访问,若在模板中可直接 {{ t }}
function updateFromRef() {
// toRefs 后的 ref 与原始 reactive 数据完全同步
t.value = '新标题' // 修改 ref,原始 reactive state.title 同步更新
c.value++ // 修改 ref,原始 state.count 同步增加
}
// 2. ref 的 isRef 和 unref 工具函数
console.log('isRef(count):', isRef(count)) // true
console.log('isRef(state.count):', isRef(state.count)) // false(普通数字)
// unref:自动去掉 ref 的 .value 外壳,获取原始值
function logValue(val) {
const raw = unref(val) // val 是 ref 则返回 .value,否则原样返回
console.log('原始值:', raw)
}
// 3. 浅层响应式 shallowRef(性能优化场景)
// shallowRef 只追踪 .value 本身的替换,不会深度追踪对象内部变化
const shallow = shallowRef({ deep: '嵌套值' })
shallow.value.deep = '修改' // 不会触发更新!只改变属性
shallow.value = { deep: '整体替换' } // 这个才会触发更新
function triggerUpdate() {
// 需要手动通知 shallowRef 更新
triggerRef(shallow)
}
// 4. 监听 reactive/ref 变化
watch(count, (newVal) => {
console.log('count 变化了:', newVal)
})
watch(state.metadata, (newMeta) => {
// 监听嵌套对象,默认是深度监听
console.log('metadata 变化:', newMeta)
}, { deep: true })
</script>
<template>
<div>
<h2>ref value: {{ count }} / {{ message }}</h2>
<button @click="increment">+1</button>
<hr />
<h3>reactive 对象:{{ state.title }}</h3>
<p>计数器: {{ state.count }}</p>
<button @click="state.count++">reactive +1</button>
<p>标签: <span v-for="tag in state.metadata.tags" :key="tag">{{ tag }} </span></p>
<hr />
<p>isRef(count) = {{ isRef(count) }}</p>
<p>unref(count) = {{ unref(count) }}</p>
</div>
</template>
运行效果说明
页面初始化显示计数器初始值。点击 "+1" 按钮,count.value 递增,页面数字同步更新。在 reactive 部分,点击按钮递增 state.count,视图同步响应。toRefs 转换后的变量在 JavaScript 中访问需要 .value,模板中直接使用变量名即可。shallowRef 在修改深层属性时不会触发更新,除非整体替换 .value。
常见问题
Q1:为什么基本类型必须用 <code>ref</code> 而不是 <code>reactive</code>?
JavaScript 基本类型(number、string、boolean)不是对象,无法被 Proxy 代理。reactive 底层依赖 Proxy,只能接收对象类型。基本类型要实现响应式,只能通过 ref——它在内部创建一个包含 value 属性的响应式对象,通过 .value 的读写拦截实现响应式。
Q2:reactive 对象解构后为什么丢失响应式?如何安全解构?
解构 const { name } = reactive({...}) 时,name 是从 Proxy 上读取的普通值,没有经过响应式追踪。解决方案:① 使用 toRefs 将 reactive 对象每个属性转为 ref;② 直接在模板中使用 state.name 而不解构;③ 在 <script setup> 中保持 reactive 对象完整,避免解构。
Q3:shallowRef 和 ref 性能差异是什么?如何选择?
shallowRef 只对 .value 的整体替换做响应式追踪,不追踪对象内部的深层变化。当数据体量很大(如一个包含上万条数据的列表)且不需要深度监听时,用 shallowRef + 手动 triggerRef() 可显著减少响应式开销。常规开发中直接用 ref 即可,不要过早优化。
延伸阅读
- <a href="#">Vue3 组合式 API 完全指南</a> —— 组合式 API 的完整介绍与项目结构最佳实践
- <a href="#">Vue3 计算属性和监听器完全指南</a> —— computed、watch、watchEffect 的进阶用法
- <a href="#">Vue3 依赖注入:provide/inject 详解</a> —— 跨层级传递响应式数据的标准方案