先记住这个答案
watch 的 getter 先通过执行收集响应式读取,相关来源变化后才有重新求值的依据。getter 只读普通变量时,即使代码写着返回新对象,普通赋值也不会自动通知它。另一方面,getter 真正重新执行后,每次新建对象又会产生不同引用,可能让字段结果实际上没变的情况也触发回调。应分别检查触发来源与返回值身份,必要时直接返回真正关心的原始派生值。
- 是否重跑 getter,先看有没有被追踪的响应式变化
- 新对象身份会影响返回结果的变化判断
- 组件渲染不是 watch getter 的通用轮询机制
普通变量改变不会自动调用 getter
例如 getter 返回包含一个模块普通变量的对象,改变那个变量本身并没有经过 ref 或 reactive 的通知入口。新对象只在函数被执行时创建,而 watch 不是每帧轮询这个函数。因此“返回的一定是新对象”不能证明普通变量赋值会产生回调。
可以用明确的响应式来源保存业务输入,或为外部系统建立订阅,在外部变化时更新相应 ref。组件因其它 state 重新渲染,也不应该被当成这份普通变量变化的可靠通知。数据流需要可解释的更新入口,不能依赖别处顺便刷新。
包装对象可能把相同业务结果变成不同引用
假设监听页码所在的分组,页码从零变成一,Math.floor(page / 10) 仍然是零。如果 getter 直接返回这个数字,结果相同可以避免无意义回调;若每次返回一个包含 group 的新对象,重新求值会得到新引用,即使 group 字段仍然为零。
这并不是要求所有 getter 都只能返回原始值。请求参数确实是结构化数据时,可以明确各字段来源、比较规则或稳定的派生接口。关键是知道新对象身份表达了什么变化,不要把无条件包装当成免费的性能中立操作,再在回调里到处增加补丁去重。
用诊断记录分开验证两个环节
排查没有回调时,先确认 getter 实际读取了哪个 ref 或代理属性,以及修改是否经过同一入口。排查过多回调时,再检查返回对象、数组和函数是否每次新建,以及这些结果是否真正代表业务输入变化。开发调试钩子可以帮助观察依赖,但应注意它们的开发模式限制。
最后用明确的无关变化、相关但结果相同、相关且结果改变三类测试验证行为。默认调度会合并同一轮同步修改,不能用一次回调就倒推发生过一次赋值。副作用还需要自己的取消与幂等规则,稳定 getter 结果只解决触发条件的一部分。
容易答错的地方
- 返回新对象就一定每次组件渲染触发 watch
- watch 依据它的响应式来源工作,组件渲染不是自动轮询所有 getter。没有来源通知时,返回表达式写得再复杂也不建立可靠的变化关系。
- 给 getter 加 deep 就能监听任意普通外部变量
- deep 影响对象的深入观察范围,不会把普通变量绑定自动变成 ref。需要先建立有效来源或外部订阅,再讨论深度与返回结果。
面试官还会怎么问?
返回新数组也有类似问题吗?
有,新数组同样有新引用。不过来源数组语法和一个 getter 返回数组是不同 API 形式,后者返回值如何变化仍需结合其中元素类型与身份判断。
把 getter 改成 computed 再 watch 就一定更少吗?
不一定,computed 如果每次重新求值都返回新对象,结果身份仍可能变化。应先明确纯派生值和稳定性需求,不能只更换包装 API。
需要对多个原始字段分别观察时怎么办?
可以使用 watch 的来源数组,让每个位置直接表达一个 ref 或字段 getter。回调得到对应的新旧数组,通常比无条件构造一个大对象更容易解释。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。