watch 回调中修改被监听数据是 vue 死循环最常见源头,表现为页面卡顿、cpu 升高、组件反复重渲染;高危写法包括直接改监听源、改 computed 依赖项、父子组件隐式双向同步。

Watch 回调里改被监听的数据,是 Vue 死循环最常见、也最容易被忽视的源头。它不报错,但页面卡顿、CPU 狂飙、组件反复重渲染——问题就藏在那一行看似无害的赋值里。
一眼识别:哪些写法高危
以下模式只要出现在 watch 回调中,基本就是死循环预备役:
- 直接修改监听源本身:比如 watch(() => obj, () => { obj.key = 'new' }) 或 watch('obj', () => { this.obj.key = 'new' }),尤其配合 deep: true 时,改完立刻触发新一轮监听
- 修改 computed 的依赖项:watch(() => this.fullName, () => { this.firstName = 'A' }),而 fullName 又依赖 firstName —— getter 一求值,又进 watch
- 父子组件间隐式双向同步:子组件 watch props 变化后立刻 $emit,父组件收到后立即更新同一 prop,形成“你变我、我变你”的闭环
快速定位:三步缩小嫌疑范围
别靠猜,用可验证的动作确认问题根源:
- 在 watch 回调第一行加 console.trace(),看调用栈是否出现重复嵌套(如 A → B → A)
- 把疑似被误改的字段临时转为 shallowRef 或用 markRaw 包裹,再测试。若循环消失,说明该字段确实在回调中被意外修改
- 打开 Chrome Performance 面板,录制一次操作,重点看 “Vue” 标记是否密集堆叠,调用栈是否反复出现 queueWatcher → flushSchedulerQueue → updateComponent
稳妥修复:四类守卫策略
核心原则是:让修改行为有明确前提,不随数据变化自动发生。
-
加值对比守卫:只在新旧值真正不同且有意义时才更新,例如
if (newVal !== oldVal && newVal != null) this.target = newVal -
加来源标识守卫:区分是用户操作触发还是响应式更新触发,比如用一个标志位
isUserInput,仅在 input/change 事件中设为 true,watch 中据此决定是否执行 -
改用一次性同步:把 watch 改为 once: true,或手动在首次触发后调用
stop(),适用于初始化同步场景 - 拆离副作用逻辑:把“监听 → 修改”链断开,改成“监听 → 触发事件 → 父组件决定是否更新”,把数据所有权和修改权收归一处
替代方案:什么时候不该用 watch
不是所有需求都适合塞进 watch。以下情况建议换思路:
- 需要根据某值派生另一个值 → 用 computed,它专为派生设计,天然防副作用
- 变化后要发请求、改 DOM、启定时器 → 用 watchEffect 并手动控制依赖范围,避免无意收集到被修改的字段
- 监听的是大型对象但只关心几个字段 → 不要用 deep: true,改用多个精准监听:
watch(() => obj.a, ...)和watch(() => obj.b, ...)










