Appearance
生命周期:钩子的注册时机与执行机制
生命周期钩子是 Vue 组件在特定阶段(如挂载、更新、卸载)执行的回调函数。开发者通过这些钩子,可以在组件的不同时期注入自定义逻辑。
本节将分析两个核心问题:
- 注册:
onMounted(...)这样的函数,是如何将回调函数注册到组件实例上的? - 执行:Vue 的渲染器(Renderer)是在什么时机、以何种顺序去执行这些回调的?
核心:实例上的回调数组
一个组件实例(ComponentInternalInstance)是一个内部对象,它包含了组件的所有信息。所有生命周期钩子,最终都会被存储在这个实例对象上的特定数组中。
Vue 3 使用简短的内部标识符来管理这些钩子:
typescript
// core/packages/runtime-core/src/enums.ts
export const enum LifecycleHooks {
BEFORE_MOUNT = 'bm', // 挂载前
MOUNTED = 'm', // 挂载后
BEFORE_UPDATE = 'bu', // 更新前
UPDATED = 'u', // 更新后
BEFORE_UNMOUNT = 'bum', // 卸载前
UNMOUNTED = 'um', // 卸载后
ACTIVATED = 'a', // (KeepAlive) 激活
DEACTIVATED = 'da', // (KeepAlive) 失活
// ... 其他
}当你调用 onMounted(fn) 时,目的就是将 fn 添加到 instance.m 这个数组里。
注册:setup 期间的 injectHook
在组合式 API 中,我们使用 onMounted, onUpdated 等函数注册钩子。这些函数由 createHook 高阶函数生成:
typescript
// core/packages/runtime-core/src/apiLifecycle.ts
// createHook 是一个工厂函数,用于创建 onMounted, onUpdated...
export const createHook =
<T extends Function = () => any>(lifecycle: LifecycleHooks) =>
(hook: T, target: ComponentInternalInstance | null = currentInstance) =>
injectHook(lifecycle, (...args: unknown[]) => hook(...args), target)
// "on" 系列 API
export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
export const onMounted = createHook(LifecycleHooks.MOUNTED)
export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
// ...createHook 将注册工作委托给了 injectHook。
injectHook:注册的核心
injectHook 的逻辑很明确:找到当前组件实例,然后把钩子函数添加到它对应的回调数组中。
typescript
// core/packages/runtime-core/src/apiLifecycle.ts
export function injectHook(
type: LifecycleHooks, // e.g., 'm' (MOUNTED)
hook: Function,
target: ComponentInternalInstance | null = currentInstance, // 默认是“当前实例”
prepend: boolean = false
): Function | undefined {
if (target) {
// 1. 找到或创建该类型的钩子数组
const hooks = target[type] || (target[type] = [])
// 2. 包装钩子(用于错误处理和 unmounted 检查)
const wrappedHook = (...) => { /* ... */ }
// 3. 【核心】将包装后的钩子添加到数组
if (prepend) {
hooks.unshift(wrappedHook)
} else {
hooks.push(wrappedHook)
}
}
}getCurrentInstance:获取当前实例
injectHook 依赖一个关键的 currentInstance 变量。这个变量是在组件 setup() 函数执行之前,由 setCurrentInstance 设置的;在 setup() 执行完毕后,再被恢复为 null。
typescript
// core/packages/runtime-core/src/component.ts
let currentInstance: ComponentInternalInstance | null = null
export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
currentInstance
export const setCurrentInstance = (instance: ComponentInternalInstance) => {
const prev = currentInstance
currentInstance = instance
return () => { // 返回一个“重置”函数
currentInstance = prev
}
}这就是为什么 onMounted 只能在 setup() 同步执行期间被调用的原因。一旦 setup 执行完毕,currentInstance 就会被重置为 null,injectHook 也就无法找到目标实例了。
5.1.3 执行:渲染器的工作时机
回调函数被注册到 instance.bm, instance.m 等数组后,由**渲染器(Renderer)**在渲染流程的特定时机负责调用。
组件的“挂载”和“更新”都由一个核心的 effect(componentUpdateFn)来驱动。
挂载阶段:beforeMount (同步) 与 mounted (异步)
当 componentUpdateFn 首次执行时,会执行“挂载”逻辑:
typescript
// core/packages/runtime-core/src/renderer.ts - setupRenderEffect
const componentUpdateFn = () => {
if (!instance.isMounted) { // 首次挂载
const { bm, m } = instance // 取出钩子数组
// 1. 【beforeMount】
// 在 patch() 之前同步执行
if (bm) {
invokeArrayFns(bm) // 遍历并执行 'bm' 数组
}
// 2. 【渲染】
// render() 被调用,生成 subTree (VNode 树)
const subTree = (instance.subTree = renderComponentRoot(instance))
// patch() 被调用,递归地创建真实 DOM
patch(
null,
subTree,
container,
// ...
)
instance.isMounted = true
// 3. 【mounted】
// 在 patch() 之后,异步地推入“后置刷新队列”
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
} else {
// ... 更新阶段 ...
}
}执行时机分析:
beforeMount(bm) 是同步执行的。它发生在render()和patch()之前,此时 DOM 节点尚未创建。mounted(m) 是异步执行的。它被queuePostRenderEffect函数推入一个队列中。这个队列会在 Vue 的整个更新(包括所有子组件的patch)全部完成之后才被清空。这保证了当mounted钩子执行时,所有子孙 DOM 均已挂载完毕。
更新阶段:beforeUpdate (同步) 与 updated (异步)
当 componentUpdateFn 第二次(及以后)执行时,会执行“更新”逻辑:
typescript
// core/packages/runtime-core/src/renderer.ts - (componentUpdateFn 的 else 分支)
else {
let { next, bu, u } = instance // 'bu' = beforeUpdate, 'u' = updated
// 1. 【beforeUpdate】
// 在 patch() 之前同步执行
if (bu) {
invokeArrayFns(bu)
}
// 2. 【渲染与 Diff】
// 重新 render(),获取新的 VNode 树
const nextTree = renderComponentRoot(instance)
const prevTree = instance.subTree
instance.subTree = nextTree
// patch() 被调用,执行 Diff 算法,更新 DOM
patch(
prevTree,
nextTree,
// ...
)
// 3. 【updated】
// 在 patch() 之后,异步地推入“后置刷新队列”
if (u) {
queuePostRenderEffect(u, parentSuspense)
}
}这个逻辑与挂载阶段一致:beforeUpdate 在 Diff 之前同步执行,updated 在 DOM 更新完毕后异步执行。
卸载阶段:beforeUnmount (同步) 与 unmounted (异步)
当组件被卸载时,渲染器会调用 unmountComponent:
typescript
// core/packages/runtime-core/src/renderer.ts
const unmountComponent = (
instance: ComponentInternalInstance,
// ...
) => {
const { bum, scope, subTree, um } = instance // 'bum' = beforeUnmount, 'um' = unmounted
// 1. 【beforeUnmount】
// 同步执行
if (bum) {
invokeArrayFns(bum)
}
// 2. 【清理】
// 停止组件的所有响应式 effect (computed, watch...)
scope.stop()
// 递归卸载子树 (移除 DOM)
unmount(subTree, instance, parentSuspense)
// 3. 【unmounted】
// 异步推入“后置刷新队列”
if (um) {
queuePostRenderEffect(um, parentSuspense)
}
// ...
instance.isUnmounted = true
}执行时机分析:
beforeUnmount(bum) 是同步的。它在所有子组件被卸载、effect被停止之前执行。这是清理定时器、解绑全局事件的最佳时机,因为此时 DOM 和实例都还存在。unmounted(um) 是异步的,它在所有清理工作(scope.stop,unmount(subTree))都完成后才执行。
5.1.4 父子组件生命周期顺序
父子组件的执行顺序是 patch 函数递归执行的必然结果,而不是一个单独的规定。
挂载阶段: Parent(bm) -> Child(bm) -> Child(m) -> Parent(m)
- Parent(bm): 父组件
patch,调用Parent(bm)(同步)。 - Parent(patch): 父组件
patch遍历到子组件... - Child(bm): 子组件
patch开始,调用Child(bm)(同步)。 - Child(patch): 子组件
patch子树,创建 DOM。 - Child(m): 子组件
patch结束,Child(m)被推入queuePostRenderEffect队列。 (队列:[ Child(m) ]) - Parent(patch):父组件
patch继续,直到结束。 - Parent(m): 父组件
patch结束,Parent(m)被推入queuePostRenderEffect队列。 (队列:[ Child(m), Parent(m) ]) - Flush: 同步
patch全部完成。Vue 开始清空post队列,Child(m)先执行,Parent(m)后执行。
卸载阶段的 bum / um 顺序(Parent(bum) -> Child(bum) -> Child(um) -> Parent(um))与此同理,都是 unmount 函数递归调用的自然结果。
特殊生命周期
onActivated/onDeactivated: 这两个钩子专为<KeepAlive>组件设计。injectHook在注册它们时会使用一个特殊的registerKeepAliveHook包装器。当<KeepAlive>切换组件时,它会跳过unmount流程,转而手动调用deactivate和activate方法,这两个方法会分别触发da和a数组中的钩子。onErrorCaptured: 此钩子被注册到ec数组。当子孙组件中发生未捕获的错误时,错误会沿着组件链向上传播,handleError函数会查找父组件的ec钩子并执行它们,直到错误被“捕获”(钩子返回true)。
总结
Vue 3 的生命周期系统是一个清晰的“注册/执行”机制:
- 注册 (
setup阶段):onMounted(fn)等 API 通过injectHook,将fn添加到当前组件实例(currentInstance)的特定回调数组中(如instance.m)。 - 执行 (
render阶段):渲染器(renderer)在执行patch(挂载/更新)或unmount(卸载)的特定时机,遍历并执行这些数组。 - 时机(同步 vs. 异步):
- "Before" 钩子(
bm,bu,bum)总是在 DOM 操作之前,同步执行。 - "After" 钩子(
m,u,um)总是在 DOM 操作之后,通过queuePostRenderEffect异步执行,以确保 DOM 已完全稳定。
- "Before" 钩子(
- 顺序:父子组件的执行顺序是
patch递归调用和异步队列执行顺序的自然结果。
