先记住这个答案
onUpdated 会在所属组件因响应式状态变化完成自己的 DOM 树更新后执行。若只有子组件内部状态改变,父组件没有重新渲染或更新自己的树,父级 onUpdated 不会仅因为后代更新而触发;子级自己的钩子会执行。如果同一轮父子都更新,子级 updated 通常先于父级。一个 updated 可能对应多项批量状态变化,若只关心某个字段造成的节点变化,应 watch 该字段并使用 post,或在修改后 await nextTick。
- 钩子归属具体组件实例
- 纯子级更新不会广播给未更新父级
- 指定状态后的节点操作优先用明确等待
更新钩子不是 DOM MutationObserver
onUpdated 由 Vue 组件更新流程调用,它并不监听任意后代节点变化。第三方库直接修改节点、浏览器输入状态变化或纯子组件内部更新,都不能据此推断父组件一定进入 updated。
排查父钩子没有执行时,先确认父渲染函数是否实际读取了发生变化的响应式值。值只在事件函数里使用、没有影响父渲染输出,就可能根本不需要更新父节点树。
分别改变子状态与父状态
下例的 Parent 渲染父标签和 Child。改变 childCount 只让 Child 更新;改变 parentLabel 才让父级更新。两个计数能把组件边界从最终页面节点变化中分离出来。
测试应每次修改后等待刷新再读计数,避免把两项变化放进同一批而无法判断来源。示例也不在 onUpdated 内继续写状态,保证观察行为不会反过来制造更新。
import { h, onUpdated, ref } from 'vue'
const childCount = ref(0)
const parentLabel = ref('parent')
const updates = { parent: 0, child: 0 }
const Child = {
setup() {
onUpdated(() => { updates.child++ })
return () => h('span', String(childCount.value))
}
}
const Parent = {
setup() {
onUpdated(() => { updates.parent++ })
return () => h('section', [h('b', parentLabel.value), h(Child)])
}
}挂载后 childCount 加一并等待,计数为父零、子一;再改变 parentLabel 并等待,父计数增加,子级若输入和插槽未变则无需因此执行自己的 updated。
一个钩子不对应一个具体字段
同一同步任务中改变多个父状态,更新可以批量合并,onUpdated 只表明这次组件节点更新已完成,不告诉你究竟哪个字段是业务原因。需要归因时使用显式 watch 或在更新入口记录操作。
如果钩子里每次都查询和重建昂贵第三方实例,会对所有组件更新付费。缩小到真正影响实例的源,并在 post 回调里对比必要字段,通常比用宽泛的 onUpdated 更容易控制。
容易答错的地方
- 把子组件任何变化都算作父更新
- 子组件拥有独立渲染效果。只有父级自身依赖变化或子级交互进一步改变了父状态,父级才会进入相应更新;看见页面后代变化不足以证明父钩子应触发。
- 用 onUpdated 判断某字段改变
- 这个钩子可能由组件使用的不同状态触发,也可能合并多项变化。要观察明确字段,应使用 watch,并根据是否依赖新节点选择 post 或 nextTick。
面试官还会怎么问?
父组件传给子组件的 prop 改变会怎样?
父级先因自身状态变化更新并给子级新 prop,子级随后更新。完成钩子通常先子后父,因此父 updated 时同步子级的相应更新已经完成。
直接修改 DOM 会触发 onUpdated 吗?
不会因为任意原生 DOM 修改就自动触发 Vue 组件钩子。若后续 Vue 渲染覆盖同一节点,外部修改还可能丢失;第三方 DOM 管理需要清楚隔离与同步入口。
如何知道某次指定修改对应的 DOM 已完成?
在执行那次状态修改的函数中 await nextTick,能把因果关系写得更清楚。若多个入口都修改同一源,可 watch 该源并设置 flush post 集中处理。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。