Vue3 响应式失效复盘:reactive 解构、数组整体替换与 watch/computed 的坑
业务里遇到的响应式 bug,多数不是 Vue 失效,而是撞上了 Proxy 的边界。下面按 ref/reactive、computed、watch 三类机制分别梳理。
reactive 与 ref 的边界
ref 是基础类型的响应式外壳,内部会自动把对象转成 reactive;reactive 是 Proxy,只能代理对象,不能直接赋值替换。
坑 1:从 reactive 上解构会丢失响应式。
const state = reactive({ count: 0 })
const { count } = state // count 只是一个普通数字,后续 state.count++ 不会更新这个变量
需要保留响应式时用 toRefs(state) 取属性,或直接用 state.count。
坑 3:reactive 无法正确监听「替换整个数组」。对代理对象重新赋值(state.arr = newArr)会让引用指向新的原始对象,绕过 Proxy 的 set trap(在把 reactive 变量本身重新赋值时),必须用 ref 承载这类整体替换的数据。
普通 JS 变量、Date.now()、Math.random() 这类非响应式来源不会触发更新。
computed:惰性求值 + 缓存,不是「自动计算」
computed 内部维护依赖、dirty 标记和缓存的 value。只有依赖变化时才把 dirty = true,下次访问 .value 才重新计算;依赖不变直接返回旧值。
- method:每次渲染都执行。
- computed:缓存,依赖不变直接返回旧值。大数据量渲染下差距很大。
陷阱:
- getter 里写异步代码:永远不会响应,返回的是 Promise。
- getter 里修改其他响应式数据:computed 应该是纯计算,不要产生副作用。
- 可写 computed 滥用:简单的双向绑定可以用,复杂逻辑不合适。
- 在
v-for中使用 computed 可能带来性能问题。 - computed 的 getter 执行时机:不要在 computed 里访问 DOM,把 DOM 操作放到
onMounted或watch里。 - computed setter 的无限循环风险:在 setter 里修改依赖自身的数据可能互相触发。
循环依赖(A 依赖 B,B 依赖 A)不会抛错,Vue 3 通过 parent 链检测并提前 return,结果是 NaN,开发模式有警告。
Vue 3.4 引入多级脏检查机制(_dirtyLevel):依赖变化时不再立即重算,先标记为「可能脏」(ComputedValueMaybeDirty),等真正访问 .value 时再验证直接依赖是否真的变了,值没变就不触发下游更新。官方给的测试数据是 10 层嵌套 computed 场景下比 3.3 减少约 35% 的不必要重渲染。SSR 模式下 computed 不缓存(self.effect.active = this._cacheable = !isSSR)。
watch:三种监听源与常见坑
三种源:1) ref;2) reactive 对象;3) getter 函数(最推荐、最稳定)。
核心配置:
deep: true:只对对象生效,递归遍历所有 key 建立监听,性能开销大。Vue 3.5+ 还可以传数字表示最大遍历深度。immediate: true:初始化立即执行一次,第一次调用时旧值是undefined。flush:pre(默认,DOM 更新前执行)/post(DOM 更新后执行)/sync(同步触发,谨慎使用)。
坑 1:监听 reactive 时 oldValue === newValue。因为是引用类型,Proxy 不克隆对象。想要拿到快照用 getter:
watch(
() => ({ ...state }),
(newVal, oldVal) => {}
)
坑 2:监听数组时,list.push(1) 能监听到,但整体替换数组不行,必须用 ref。
坑 3:在 watch 回调里操作 DOM 报错,多半是没加 flush: 'post'。
watch 与 watchEffect 的区别:watch 显式指定源、能拿到新值旧值、默认懒执行;watchEffect 自动收集依赖、立即执行一次,但无法获取旧值,依赖过多时难以追踪,容易造成不必要更新。两者都有清理回调:onWatcherCleanup()(只能在同步执行期间调用,不能在 await 之后)。
对照小结
| 特性 | ref | reactive | computed | watch |
|---|---|---|---|---|
| 包装形式 | RefImpl | Proxy | ComputedRef | 副作用函数 |
| 支持类型 | 全部 | 对象/数组 | 派生值 | 任意响应式 |
| 是否缓存 | 否 | 否 | 是 | 否 |
| 可异步 | 否 | 否 | 否 | 是 |
| 解构风险 | 不会 | 会 | 不会 | 不会 |
| 适用场景 | 独立状态、DOM | 复杂对象 | 派生数据 | 变化执行逻辑 |
大部分响应式 bug 来自:解构、整体赋值覆盖、不了解 Proxy 的限制、不理解依赖追踪。