这个问题的核心原因在于 JavaScript 的引用传递机制 和 Vue 的响应式代理(Proxy)原理。
简单来说:reactive 给你的是一个"代理对象",如果你把整个变量重新赋值,就等于把这个代理丢了;而 ref 给你的是一个"装数据的盒子",你重新赋值时修改的是盒子里的内容(.value),盒子本身没变。
1. 为什么 reactive 重新赋值会失效?
reactive 返回的是一个 Proxy 代理对象 。你的变量指向的是这个代理。当你进行 = 重新赋值时,变量指向了一个全新的普通对象,Vue 的代理丢失,自然无法追踪变化。
javascript
import { reactive } from 'vue'
// 1. 初始创建:state 指向一个 Proxy 代理
let state = reactive({ count: 1 })
// 2. 修改属性:没问题!触发了 Proxy 的 setter
state.count = 2 // ✅ 页面会更新
// 3. 重新赋值:state 不再指向 Proxy,而是指向了一个全新的普通对象 {}
state = { count: 3 } // ❌ 致命错误!响应式链接彻底断开了
// 此时再修改属性,Vue 完全感知不到
state.count = 4 // ❌ 页面不会更新(因为 state 已不是响应式对象)
2. 为什么 ref 重新赋值就可以?
ref 返回的是一个 RefImpl 实例对象(即带有 .value 属性的盒子) 。你重新赋值时,修改的是盒子里的 .value 属性,盒子本身(变量指向)始终是那个 Ref 实例,从未改变,因此 Vue 能够一直监听到 .value 的变化。
javascript
import { ref } from 'vue'
// 1. 初始创建:data 指向一个 Ref 盒子
const data = ref({ count: 1 })
// 2. 修改属性:通过 .value 修改,触发了 ref 的 setter
data.value.count = 2 // ✅ 页面会更新
// 3. 重新赋值整个对象:注意!我们修改的是 data.value(盒子里的内容)
data.value = { count: 3 } // ✅ 完美响应式!盒子还是那个盒子
// 此时修改属性,Vue 依然能感知到
data.value.count = 4 // ✅ 页面会更新
3. 特别注意:如果重新赋值整个 ref 变量会怎样?
如果你这样写:
javascript
let data = ref({ count: 1 })
data = ref({ count: 2 }) // 重新赋值整个变量(不是改 .value)
- 语法可行 :
data确实变成了一个新的 Ref 盒子,它本身依然是响应式的。 - 严重隐患 :如果模板或
watch监听了旧的data变量,它们绑定的还是旧盒子的引用,新盒子无法被追踪到。这在 Vue 3 的setup中极易引起难以排查的 Bug。 - Vue 官方最佳实践 :声明
ref时必须使用const,永远不要重新赋值变量本身,只修改.value。这是写 Vue 3 的铁律。
javascript
// ❌ 错误示范(极其罕见,但会踩坑)
let num = ref(0)
num = ref(10) // 虽然新值是响应式,但可能造成监听失效
// ✅ 绝对正确的写法
const num = ref(0)
num.value = 10 // 改值永远走 .value
4. 什么时候该用 reactive,什么时候用 ref?
| 对比维度 | reactive |
ref |
|---|---|---|
| 适用数据类型 | 对象(Object)和数组(Array) | 所有类型(包括基本类型和对象) |
| 重新赋值整个对象 | ❌ 绝对不行(会丢失响应式) | ✅ 可以 (通过 .value = newObj) |
| 访问方式 | 直接访问属性(state.count) |
必须通过 .value(data.value.count) |
| 解构/展开 | ❌ 解构后会丢失响应式 | ✅ 配合 toRefs 可解构 |
| 典型应用场景 | 表单数据 (form)、配置项 (config),数据结构和属性固定,不需要整体替换。 |
任何需要整体替换的数据 (如接口返回的新对象)、基本类型 (数字、字符串、布尔)、异步获取的数据。 |
5. 实战场景:为什么你在项目中更容易遇到这个问题?
最常见的情况是:从后端接口获取数据并赋值。
javascript
// ❌ 错误写法(reactive)
const userInfo = reactive({ name: '', age: 0 })
const fetchData = async () => {
const res = await api.getUser()
userInfo = res.data // 试图整体替换------失效!
}
// ✅ 正确写法(ref)
const userInfo = ref({ name: '', age: 0 })
const fetchData = async () => {
const res = await api.getUser()
userInfo.value = res.data // 整体替换------完美生效!
}
总结一句口诀
reactive:只能改属性 (obj.key = newVal),不能换对象 (obj = newObj)。ref:既能改属性 (ref.value.key = newVal),也能换对象 (ref.value = newObj)。
所以,当你的数据未来大概率需要整体替换 (比如接口返回的新对象)时,请毫不犹豫地使用 ref + .value。如果数据结构固定且不需要整体替换(如表单校验对象),用 reactive 会更省代码(不用到处写 .value)。