响应式系统是 Vue 的“自动档变速箱”——用好了行云流水,用错了轻则顿挫,重则熄火。本章总结出一套实战检验过的使用规范,并罗列最常见的失效场景和修复方法,帮你把响应式用对、用稳。
一、响应式使用规范(黄金法则)
1. 基本类型用 ref,引用类型用 reactive,别混着用
ref包装基本类型(字符串、数字、布尔值)和需要整体替换的引用类型(如一个可能被整体赋值的数组/对象)。reactive包装对象、数组、Map、Set 等不需要整体替换的引用类型。
import { ref, reactive } from 'vue'
// ✅ 推荐
const name = ref('Alice') // 基本类型
const list = ref([]) // 可能被整体替换的数组
const user = reactive({ age: 30 }) // 固定结构的对象
// ❌ 避免
const nameObj = reactive({ value: 'Alice' }) // 多此一举
const count = reactive(1) // reactive 不接受基本类型
2. 不要解构 reactive 对象的属性,使用 toRefs 或 toRef
解构会丢失响应式,因为解构出来的是原始值的拷贝,不再是 Proxy。
const state = reactive({ count: 0, name: 'Vue' })
// ❌ 解构后丢失响应式
let { count, name } = state
count++ // 视图不更新
// ✅ 使用 toRefs 保留响应式
const { count, name } = toRefs(state)
count.value++ // 视图更新
3. 直接替换 reactive 对象整体会失效,改用 ref 或 Object.assign
reactive 返回的是一个固定的 Proxy 引用,你不能用另一个对象去覆盖它,否则原 Proxy 失去意义。
let state = reactive({ items: [] })
// ❌ 整体替换(state 的引用变了,原来的 Proxy 没变)
state = reactive({ items: [1,2,3] }) // 模板里绑定的还是旧的 state
// ✅ 方案一:使用 ref
const state = ref({ items: [] })
state.value = { items: [1,2,3] } // 有效
// ✅ 方案二:修改 reactive 内部的属性
const state = reactive({ items: [] })
state.items = [1,2,3] // 有效
4. 数组操作建议通过 ref 或 reactive 包裹,不要用索引直接赋值
Vue 3 的 Proxy 已经可以侦测数组索引赋值,但为了语义清晰和避免潜在问题,仍然推荐使用完整替换或数组方法。
const list = ref(['a', 'b'])
// ✅ 可行
list.value[0] = 'x' // Vue 3 支持
// ✅ 更推荐
list.value.splice(0, 1, 'x')
5. 使用 computed 代替模板中复杂表达式,且永远只读,不要手动修改 computed 值
const fullName = computed(() => firstName.value + ' ' + lastName.value)
// ❌ 不要这样做
fullName.value = 'new name'
6. watch 侦听 reactive 对象时,默认是深层的;侦听 ref 对象时要明确是否深层
const obj = reactive({ a: { b: 1 } })
watch(obj, (newVal) => { ... }) // 默认深层
const objRef = ref({ a: { b: 1 } })
watch(objRef, (newVal) => { ... }, { deep: true })
二、十大常见失效场景与修复方案
| 场景 | 问题代码 | 修复方案 | 原因 |
|------|----------|----------|------|
| 1. 丢失响应式解构 | let { x } = reactive({x:1}) | const { x } = toRefs(reactive({x:1})) | 解构取到原始值,丢失 Proxy 代理 |
| 2. 直接修改 ref 内对象的属性(.value 未深入) | const obj = ref({ a:1 }); obj.a = 2 | obj.value.a = 2 | ref 的响应式点在 .value,不点出 value 修改不到 Proxy |
| 3. 整体替换 reactive 变量 | let s = reactive({}); s = newObj | 用 ref 或用 Object.assign(s, newObj) | reactive 本身不能被整体替换,变量指向新地址,原代理被废弃 |
| 4. 给 reactive 对象添加新属性不响应 | const obj = reactive({}); obj.newProp = 1 | Vue 3 已支持(Proxy 优势),但切记不要用 Object.defineProperty 打补丁 | Vue 3 无此问题;Vue 2 需要用 Vue.set |
| 5. 数组索引赋值 / length 裁切 | arr[0] = 'x' ; arr.length = 0 | Vue 3 支持,但为通用性可改用 splice | Vue 2 不支持,Vue 3 Proxy 支持 |
| 6. 将 reactive 对象的属性传给函数时 | fn(obj.prop) 函数内修改该值 | 传 obj.prop 是原始值,要传整个 obj 或用 toRef | 传递时脱钩,失去响应式联系 |
| 7. 在 setup 外使用响应式 API(如模块顶层) | 模块顶层 const state = ref(0) 并在多个组件引用 | 每个组件调用时共享同一个 ref(除非这就是你想要的状态共享),否则需在组件内创建 | 响应式实例在模块范围内是单例 |
| 8. 在 computed 中产生副作用(如修改其他状态) | computed(() => { list.value.push(1); return x }) | 绝对禁止,改用 watch | computed 应该是纯函数 |
| 9. 忘记 .value 在模板外使用 ref | <script> 里直接 count = 5 | count.value = 5 | 模板会自动解包,但 JS 里必须手动解包 |
| 10. 在异步回调中访问已卸载组件的响应式数据 | setTimeout(() => { data.value = … }, 5000) | 在 onUnmounted 清除定时器,或用 effectScope | 组件销毁后修改其响应式数据可能造成内存泄漏或无效更新 |
三、排查与调试技巧
- Vue DevTools:打开组件树,选中组件查看其响应式数据,实时修改验证是否响应。
console.log陷阱:console.log(reactiveObj)会输出 Proxy,浏览器展开时会自动读取属性,看起来像有值,其实可能是空的。用toRaw(state)查看原始对象。watchEffect跟踪依赖:临时添加watchEffect(() => console.log(state.xxx)),如果控制台没输出,说明该属性不是响应式的。- TypeScript 帮助规避:开启
strict: true,用泛型约束ref<User>,避免错误赋值。
四、速记口诀
ref 管标量,reactive 管对象,
解构用 toRefs,替换用 ref,
不要直接给 reactive 换对象,
异步回调要清理,副作用别进 computed 里。
遵循这些规范,响应式就不再是“玄学”,而是一套可预测、可调试的可靠机制。