编程 Vue3 响应式失效复盘:reactive 解构、数组整体替换与 watch/computed 的坑

2026-09-23 21:02:57

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:缓存,依赖不变直接返回旧值。大数据量渲染下差距很大。

陷阱:

  1. getter 里写异步代码:永远不会响应,返回的是 Promise。
  2. getter 里修改其他响应式数据:computed 应该是纯计算,不要产生副作用。
  3. 可写 computed 滥用:简单的双向绑定可以用,复杂逻辑不合适。
  4. v-for 中使用 computed 可能带来性能问题。
  5. computed 的 getter 执行时机:不要在 computed 里访问 DOM,把 DOM 操作放到 onMountedwatch 里。
  6. 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
  • flushpre(默认,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 之后)。

对照小结

特性refreactivecomputedwatch
包装形式RefImplProxyComputedRef副作用函数
支持类型全部对象/数组派生值任意响应式
是否缓存
可异步
解构风险不会不会不会
适用场景独立状态、DOM复杂对象派生数据变化执行逻辑

大部分响应式 bug 来自:解构、整体赋值覆盖、不了解 Proxy 的限制、不理解依赖追踪。

复制全文 生成海报 Vue 前端 响应式 排障

推荐文章

程序员茄子在线接单