先记住这个答案
onUpdated 在组件完成一次响应式更新后执行;若回调无条件修改了该组件渲染依赖的状态,就会安排下一次更新,新的 updated 又继续写入,形成反馈环。应把纯派生值改成 computed,把特定源变化后的副作用改成显式 watch,节点测量则只在结果确实变化时写入,并使用 ResizeObserver 等真实变化信号。改变 flush 或加固定延迟只会改变循环速度,不能移除反馈关系。
- updated 写渲染依赖会重新安排更新
- 纯派生状态用 computed 避免双份同步
- 节点测量写回必须有稳定比较条件
无条件写入让每轮都有新工作
例如 updated 中执行 count++,渲染又显示 count,那么每轮都产生不同值,无法达到稳定状态。即使写回另一个字段,只要该字段也参与本组件渲染,仍然可能重新进入钩子。
有些循环跨越多个组件:父 updated 改共享状态,子 watch 再改父状态。排查时列出每个回调读取与写入的源,沿着触发链寻找回到起点的路径,不能只盯最后抛错的位置。
把派生关系改成单向计算
下例让 subtotal 和 tax 通过 computed 得出 total,渲染读取 total 即可。源变化会重新计算展示值,却不需要在 updated 之后再把结果写入另一份响应式状态。
若总额需要提交服务端,应在明确的保存动作或 watch 特定源时做副作用,并处理失败与竞态。computed 只承担本地派生,不应在 getter 内发请求或修改它所读取的源。
import { computed, h, ref } from 'vue'
const subtotal = ref(100)
const taxRate = ref(0.1)
const total = computed(() => subtotal.value * (1 + taxRate.value))
const TotalProbe = {
setup() {
return () => h('span', total.value.toFixed(2))
}
}组件首次显示 110.00,subtotal 改为二百并等待更新后显示 220.00。整个过程没有 onUpdated 状态写回,也不维护可能与源失去同步的第二份 total ref。
节点测量必须判断是否真的改变
布局测量有时确实需要把宽度写入状态。应使用 ResizeObserver 或明确更新后的测量点,并在新宽度与已保存值不同且差异有意义时才写;还要防止写入样式反过来改变宽度造成振荡。
简单布尔 guard 如果每轮结束都重置,可能只是把循环拆成多轮。真正的守卫要代表稳定业务条件,例如新测量值等于当前值后不再写,且用边界测试验证缩放和舍入不会在两个值间来回跳。
容易答错的地方
- 用 nextTick 包住写回就认为安全
- 延后赋值仍会创建下一轮组件更新,链条只是跨微任务继续。修复需要改变数据依赖或让写回收敛,不能用时序技巧隐藏同一个反馈环。
- 在钩子开头加永远会重置的锁
- 锁若在本轮结束时释放,下一次 updated 仍会再次写入。应证明状态最终稳定,或把动作移到由具体源触发的一次流程中,而不是依赖脆弱的执行中标志。
面试官还会怎么问?
onBeforeUpdate 中修改状态就一定安全吗?
官方允许在该钩子中修改组件状态,但无界反馈和难以理解的多轮更新仍可能存在。允许调用不等于适合维护,应优先让数据流单向并限定修改条件。
为什么 computed 能避免两份状态不一致?
结果每次由当前源计算,不需要在另一个生命周期回调中复制。少了一条写回边,就不会出现源已更新而缓存 ref 尚未同步或同步又触发下一轮的问题。
错误边界能捕获递归更新并恢复吗?
错误捕获可以提供降级和报告,但不应作为状态循环的控制流程。即使捕获到告警或异常,组件数据关系仍然错误,应从触发链移除无界写回。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。