响应式系统的核心是 Vue 对数据的代理。一旦你绕过了这层代理,响应式就会失效。下面两个场景是实际开发中最常见的坑,理解它们能避免大量“为什么界面不更新”的困扰。
直接替换 reactive 对象的整体引用
问题代码:
import { reactive } from 'vue'
const state = reactive({ count: 0, name: 'Alice' })
// ❌ 错误:直接覆盖整个对象
state = { count: 1, name: 'Bob' } // 这句会报错或失效,state 不再是响应式对象
为什么失效?reactive() 返回的是一个 Proxy 代理对象,你把这个代理赋值给了变量 state。当你用 state = ... 覆盖整个对象时,你实际上是丢掉了这个代理,让 state 重新指向了一个普通对象。这个新对象没有经过 Proxy 包裹,自然失去响应式。而且,由于你修改的是变量 state 自身的引用,原代理对象还在但已无法访问,视图不会更新。
正确做法:
- 修改对象的属性,而不是替换整个对象
const state = reactive({ count: 0, name: 'Alice' })
// ✅ 正确:修改属性
state.count = 1
state.name = 'Bob'
- 如果确实需要整体替换数据,使用
ref
ref 可以包裹任意类型(包括对象),通过 .value 替换时,Vue 会重新创建响应式连接。
import { ref } from 'vue'
const state = ref({ count: 0, name: 'Alice' })
state.value = { count: 1, name: 'Bob' } // ✅ 正确,响应式保持
- 使用
Object.assign合并新值(保持同一代理引用)
const state = reactive({ count: 0, name: 'Alice' })
Object.assign(state, { count: 1, name: 'Bob' }) // ✅ 属性更新,响应式保留
解构丢失响应式
问题代码:
import { reactive } from 'vue'
const state = reactive({ count: 0, name: 'Alice' })
// ❌ 错误:解构后得到普通值,失去响应式连接
let { count, name } = state
count++ // 视图不更新
name = 'Bob' // 视图不更新
为什么失效?
当你从 reactive 对象中解构属性时,你取得的是属性的当前值。对于基本类型(如 count),它就是一个纯数字,没有和原来的 Proxy 对象建立任何联系。之后你修改这个局部变量,Vue 完全感知不到。
正确做法:
- 使用
toRefs将解构后的值转为 Ref,保持响应式连接
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, name: 'Alice' })
const { count, name } = toRefs(state)
count.value++ // ✅ 视图更新
name.value = 'Bob' // ✅ 视图更新
解构出的 count 和 name 变成了 Ref,它们与原始 state 对象共享响应式,修改 .value 会同步更新源对象。
- 不解构,直接使用
state.count和state.name
这是最稳妥的方式,在模板中尤其自然:
<template>
<p>{{ state.count }} - {{ state.name }}</p>
</template>
在 <script setup> 中,也可以直接使用 state.count,不需要解构。
- 对于
ref包裹的对象,用.value访问属性,不直接解构
const state = ref({ count: 0, name: 'Alice' })
// ✅ 通过 state.value 访问属性
state.value.count++
// 若用解构同样需要 toRefs
const { count, name } = toRefs(state.value)
一句话总结:响应式依赖代理引用,不要切断这层联系。修改对象用属性赋值,整体替换用 ref,解构时用 toRefs 保持连接。