为什么重新赋值不能用reactive要使用ref

这个问题的核心原因在于 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 必须通过 .valuedata.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)。