本篇关键词:生命周期钩子、onMounted、onUpdated、注册机制、调用时机、钩子队列、组件实例、SSR 生命周期差异
工程价值 :这是 Vue3 组件系统的"时间轴"。为什么
onMounted能拿到 DOM?为什么父子组件的mounted执行顺序相反?为什么onUpdated在nextTick之后?这些问题的答案,都藏在生命周期钩子的注册、存储与调度机制中。读懂这一篇,你就能精确控制组件的执行时序。
一、开篇:生命周期不是"事件",而是"回调队列"
很多同学对生命周期的理解停留在:
"Vue 在合适的时间调用这些钩子。"
❌ 这是片面的。
在 Vue3 中,生命周期的本质是:
组件实例上的一组"有序回调函数队列"。
-
注册:在
setup阶段收集 -
存储:挂载在组件实例上
-
调度:在特定渲染阶段批量执行
👉 生命周期 = 注册 + 存储 + 调度
二、生命周期钩子的完整清单
Vue3 暴露的 Composition API 钩子:
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onActivated,
onDeactivated,
onErrorCaptured,
onRenderTracked,
onRenderTriggered,
onServerPrefetch
} from 'vue'
三、生命周期钩子的注册机制(核心)
1️⃣ 钩子函数的本质
onMounted(() => {
console.log('mounted')
})
👉 它只是一个普通的函数调用,不是语法糖。
2️⃣ 钩子注册的核心函数
源码位置:packages/runtime-core/src/apiLifecycle.ts
export function injectHook(
type: LifecycleHooks,
hook: Function,
target: ComponentInternalInstance
) {
if (target) {
const hooks = target[type] || (target[type] = [])
hooks.push(hook)
}
}
✅ 关键点:
-
每个生命周期对应一个数组
-
同一个钩子可以注册多次
-
按注册顺序执行
3️⃣ onMounted 的实现
export function onMounted(hook: Function) {
injectHook(
LifecycleHooks.MOUNTED,
hook,
getCurrentInstance()
)
}
👉 onMounted 只是把回调函数塞进当前组件实例的 mounted 数组里。
四、组件实例上的生命周期存储结构
1️⃣ 组件实例中的钩子字段
const instance: ComponentInternalInstance = {
mounted: [], // onMounted
updated: [], // onUpdated
unmounted: [], // onUnmounted
beforeMount: [], // onBeforeMount
beforeUpdate: [], // onBeforeUpdate
// ...
}
2️⃣ 钩子数组的执行顺序
mounted.forEach(hook => hook())
✅ 先注册,先执行。
五、生命周期的调用时机(重点)
1️⃣ onBeforeMount:组件挂载前
调用位置:
function mountComponent(instance) {
callHook(instance, LifecycleHooks.BEFORE_MOUNT)
instance.isMounted = true
}
执行时机:
-
组件已初始化
-
DOM 尚未创建
-
render()尚未执行
👉 此时不能访问 DOM。
2️⃣ onMounted:组件挂载完成
调用位置:
function mountedComponent(instance) {
instance.isMounted = true
callHook(instance, LifecycleHooks.MOUNTED)
}
执行时机:
-
DOM 已创建
-
子组件已挂载
-
可安全访问 DOM
✅ 这是访问 DOM 的正确时机。
3️⃣ onBeforeUpdate:组件更新前
调用位置:
function updateComponent(instance) {
callHook(instance, LifecycleHooks.BEFORE_UPDATE)
patch()
}
执行时机:
-
响应式数据已变化
-
DOM 尚未更新
-
可获取更新前的 DOM 状态
4️⃣ onUpdated:组件更新完成
调用位置:
function updatedComponent(instance) {
queuePostFlushCb(() => {
callHook(instance, LifecycleHooks.UPDATED)
})
}
✅ 关键点:
-
在
nextTick之后执行 -
DOM 已更新
-
适合操作更新后的 DOM
5️⃣ onBeforeUnmount:组件卸载前
调用位置:
function unmountComponent(instance) {
callHook(instance, LifecycleHooks.BEFORE_UNMOUNT)
unmount()
}
执行时机:
-
DOM 仍存在
-
可清理副作用
-
取消定时器、事件监听
6️⃣ onUnmounted:组件卸载完成
调用位置:
function unmountedComponent(instance) {
callHook(instance, LifecycleHooks.UNMOUNTED)
instance.isUnmounted = true
}
执行时机:
-
DOM 已移除
-
组件实例已销毁
-
不能再访问 DOM
六、父子组件生命周期执行顺序(VIP 重点)
1️⃣ 挂载阶段
父 setup
父 onBeforeMount
├─ 子 setup
├─ 子 onBeforeMount
├─ 子 onMounted
父 onMounted
✅ 规律:
-
setup:父 → 子 -
onBeforeMount:父 → 子 -
onMounted:子 → 父
👉 原因:子组件先挂载完成,父组件才能确定整体挂载完成。
2️⃣ 更新阶段
父 onBeforeUpdate
├─ 子 onBeforeUpdate
├─ 子 onUpdated
父 onUpdated
✅ 规律:
-
onBeforeUpdate:父 → 子 -
onUpdated:子 → 父
3️⃣ 卸载阶段
父 onBeforeUnmount
├─ 子 onBeforeUnmount
├─ 子 onUnmounted
父 onUnmounted
七、为什么 onMounted 能访问 DOM?
1️⃣ 渲染流程回顾
setupRenderEffect()
↓
componentUpdateFn()
↓
renderComponentRoot() → 生成 VNode
↓
patch() → 创建 / 更新 DOM
↓
mountedComponent()
↓
callHook(MOUNTED)
👉 DOM 在 mounted 钩子调用前已经创建完成。
2️⃣ 与 Vue2 的差异
| 框架 | mounted 时机 |
|---|---|
| Vue2 | DOM 已挂载 |
| Vue3 | DOM 已挂载(一致) |
✅ Vue3 保持了行为一致性。
八、onUpdated 与 nextTick 的关系
1️⃣ onUpdated 的执行时机
queuePostFlushCb(() => {
callHook(instance, LifecycleHooks.UPDATED)
})
👉 onUpdated 本身就是 nextTick 回调。
2️⃣ 为什么需要 onUpdated?
onUpdated(() => {
// DOM 已更新
el.getBoundingClientRect()
})
✅ 适合需要依赖更新后 DOM 的场景。
九、KeepAlive 下的生命周期
1️⃣ activated / deactivated
onActivated(() => {
// 组件被激活
})
onDeactivated(() => {
// 组件被缓存
})
执行时机:
-
mounted→activated -
unmounted→deactivated
2️⃣ 与 mounted 的区别
| 钩子 | 触发次数 |
|---|---|
| mounted | 一次 |
| activated | 多次 |
十、SSR 中的生命周期差异
1️⃣ 服务端执行的生命周期
setup
onBeforeMount
onServerPrefetch
❌ 不执行:
-
onMounted -
onUpdated -
onUnmounted
2️⃣ onServerPrefetch 的作用
onServerPrefetch(async () => {
await fetchData()
})
✅ 用于服务端数据预取。
十一、调试实战:观察生命周期调用栈
1️⃣ 打这些断点
packages/runtime-core/src/apiLifecycle.ts
├── injectHook()
├── callHook()
packages/runtime-core/src/renderer.ts
├── mountComponent()
├── updateComponent()
└── unmountComponent()
2️⃣ 测试代码
<script setup>
onBeforeMount(() => console.log('beforeMount'))
onMounted(() => console.log('mounted'))
onBeforeUpdate(() => console.log('beforeUpdate'))
onUpdated(() => console.log('updated'))
</script>
观察:
-
钩子注册时机
-
钩子调用顺序
-
父子组件调用差异
十二、工程启示
✅ 在 onMounted 中访问 DOM
onMounted(() => {
el.value.focus()
})
✅ 在 onBeforeUnmount 中清理副作用
onBeforeUnmount(() => {
clearInterval(timer)
removeEventListener('resize', handler)
})
✅ 避免在 onUpdated 中修改响应式数据
// ❌ 可能导致无限更新
onUpdated(() => {
count.value++
})
✅ KeepAlive 组件使用 activated / deactivated
onActivated(() => {
startTimer()
})
onDeactivated(() => {
stopTimer()
})
十三、面试追问
-
setup 和 onMounted 哪个先执行?
setup先执行,它是初始化阶段;onMounted是挂载完成阶段。 -
为什么 onMounted 能访问 DOM?
因为 DOM 在
mounted钩子调用前已经创建完成。 -
父子组件生命周期执行顺序为什么是反的?
因为子组件先挂载完成,父组件才能确定整体挂载完成。
-
onUpdated 和 watch 的区别?
onUpdated关注 DOM 更新;watch关注数据变化。
十四、本篇总结
✅ 生命周期钩子是组件实例上的回调队列
✅ 注册阶段:setup 中收集钩子
✅ 存储阶段:挂载到组件实例数组
✅ 调度阶段:在特定渲染节点批量执行
✅ 挂载顺序:setup → onBeforeMount → onMounted
✅ 父子顺序:setup 父→子,onMounted 子→父
✅ SSR 差异:服务端只执行部分钩子
✅ KeepAlive:activated / deactivated 替代 mounted / unmounted
一句话升华:
Vue3 的生命周期系统,是一套**"基于组件实例的、可预测的、可扩展的回调调度机制"**。理解它,你就掌握了组件行为的"时间轴"。