Appearance
第 12 章 生命周期与调度
本章要点
- 组件生命周期的完整图谱:从 setup 到 unmounted,每个钩子的触发时机和内部实现
- 生命周期钩子的注册机制:injectHook 如何将钩子函数绑定到组件实例
- 调度器的核心设计:异步批量更新的队列模型和 flush 时机
- nextTick 的实现本质:为什么它能保证在 DOM 更新后执行
- 三种队列的优先级:pre 队列、queue 队列、post 队列的协作关系
- Suspense 对生命周期的影响:异步组件如何改变钩子触发顺序
- 调度器的错误处理与递归保护机制
在上一章中,我们深入了虚拟 DOM 和 Diff 算法——这是 Vue 渲染管线的"引擎"。但引擎不能随意启动,它需要一个精密的调度系统来协调"何时更新"、"以什么顺序更新"、"更新完之后做什么"。
同时,每个组件都有自己的"生命"——从诞生到消亡,在不同的阶段,开发者需要介入执行特定的逻辑。这就是生命周期系统的使命。
调度器和生命周期看似独立,实则紧密耦合。生命周期钩子的触发时机由调度器控制,而调度器的行为又受组件状态(是否挂载、是否激活)的制约。本章将一并剖析。
12.1 组件生命周期全景
生命周期的完整流程
Composition API 中的生命周期
在 Vue 3 的 Composition API 中,生命周期钩子通过 onXxx 函数注册:
typescript
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onActivated,
onDeactivated,
onErrorCaptured,
onRenderTracked,
onRenderTriggered
} from 'vue'
export default {
setup() {
onBeforeMount(() => {
console.log('DOM 即将创建')
})
onMounted(() => {
console.log('DOM 已创建,可以访问 this.$el')
})
onBeforeUpdate(() => {
console.log('DOM 即将更新')
})
onUpdated(() => {
console.log('DOM 已更新')
})
onBeforeUnmount(() => {
console.log('组件即将卸载')
})
onUnmounted(() => {
console.log('组件已卸载,所有副作用已清理')
})
}
}注意:setup 本身就是在 beforeCreate 和 created 之间执行的,所以 Composition API 中没有这两个钩子的对应函数——setup 就是它们的替代品。
12.2 钩子注册机制:injectHook
所有 onXxx 函数内部都调用同一个底层函数 injectHook:
typescript
// packages/runtime-core/src/apiLifecycle.ts
export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
export const onMounted = createHook(LifecycleHooks.MOUNTED)
export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
export const onUpdated = createHook(LifecycleHooks.UPDATED)
export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT)
export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED)
// createHook 只是一层柯里化
export const createHook = <T extends Function = () => any>(
lifecycle: LifecycleHooks
) => {
return (hook: T, target: ComponentInternalInstance | null = currentInstance) =>
injectHook(lifecycle, (...args: unknown[]) => hook(...args), target)
}injectHook 的核心逻辑:
typescript
// packages/runtime-core/src/apiLifecycle.ts
export function injectHook(
type: LifecycleHooks,
hook: Function & { __weh?: Function },
target: ComponentInternalInstance | null = currentInstance,
prepend: boolean = false
): Function | undefined {
if (target) {
// 获取或创建钩子数组
// 组件实例上用简写存储:bm=beforeMount, m=mounted 等
const hooks = target[type] || (target[type] = [])
// 包装钩子函数,确保调用时 currentInstance 正确
const wrappedHook =
hook.__weh ||
(hook.__weh = (...args: unknown[]) => {
if (target.isUnmounted) {
return
}
// 暂停追踪,防止钩子中的响应式访问被错误收集
pauseTracking()
// 设置当前实例,确保钩子内部能访问组件上下文
const reset = setCurrentInstance(target)
// 调用钩子,捕获错误
const res = callWithAsyncErrorHandling(hook, target, type, args)
reset()
resetTracking()
return res
})
if (prepend) {
hooks.unshift(wrappedHook)
} else {
hooks.push(wrappedHook)
}
return wrappedHook
}
}几个关键设计点:
currentInstance 绑定:钩子函数在注册时捕获当前组件实例,调用时恢复。这保证了即使钩子被延迟执行(如
mounted在异步 flush 中执行),也能正确访问组件上下文。暂停追踪:钩子函数中的响应式访问不应该被收集为依赖,否则会导致不可预期的重渲染。
错误处理:所有钩子调用都通过
callWithAsyncErrorHandling包装,支持onErrorCaptured的错误冒泡机制。卸载检查:如果组件已经卸载,钩子直接跳过,避免操作已清理的状态。
生命周期枚举
typescript
// packages/runtime-core/src/enums.ts
export enum LifecycleHooks {
BEFORE_CREATE = 'bc',
CREATED = 'c',
BEFORE_MOUNT = 'bm',
MOUNTED = 'm',
BEFORE_UPDATE = 'bu',
UPDATED = 'u',
BEFORE_UNMOUNT = 'bum',
UNMOUNTED = 'um',
DEACTIVATED = 'da',
ACTIVATED = 'a',
RENDER_TRIGGERED = 'rtg',
RENDER_TRACKED = 'rtc',
ERROR_CAPTURED = 'ec',
SERVER_PREFETCH = 'sp'
}注意这些简写——bm、m、bu、u——它们直接作为组件实例的属性名。这不是偷懒,而是刻意的优化:短属性名在 V8 的隐藏类中占用更少的内存。
12.3 钩子的触发时机
setupRenderEffect:生命周期的指挥中心
组件的生命周期钩子不是由一个统一的"生命周期管理器"触发的——它们散布在渲染流程的各个关键节点。最核心的触发点在 setupRenderEffect 中:
typescript
// packages/runtime-core/src/renderer.ts(简化)
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
namespace,
optimized
) => {
const componentUpdateFn = () => {
if (!instance.isMounted) {
// ======== 首次挂载 ========
const { bm, m, parent } = instance
// 触发 beforeMount
if (bm) {
invokeArrayFns(bm)
}
// 执行渲染函数
const subTree = (instance.subTree = renderComponentRoot(instance))
// 递归 patch(创建真实 DOM)
patch(null, subTree, container, anchor, instance, parentSuspense, namespace)
// 真实 DOM 已创建,保存引用
initialVNode.el = subTree.el
// 触发 mounted(通过调度器 post 队列延迟执行)
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
instance.isMounted = true
} else {
// ======== 更新 ========
let { next, bu, u, parent, vnode } = instance
if (next) {
next.el = vnode.el
updateComponentPreRender(instance, next, optimized)
} else {
next = vnode
}
// 触发 beforeUpdate
if (bu) {
invokeArrayFns(bu)
}
// 执行渲染函数
const nextTree = renderComponentRoot(instance)
const prevTree = instance.subTree
instance.subTree = nextTree
// Diff 并更新 DOM
patch(prevTree, nextTree, hostParentNode(prevTree.el!)!, getNextHostNode(prevTree), instance, parentSuspense, namespace)
next.el = nextTree.el
// 触发 updated(通过调度器 post 队列延迟执行)
if (u) {
queuePostRenderEffect(u, parentSuspense)
}
}
}
// 创建渲染 effect
const effect = (instance.effect = new ReactiveEffect(componentUpdateFn, NOOP, () => queueJob(instance.update)))
const update: SchedulerJob = (instance.update = () => {
if (effect.dirty) {
effect.run()
}
})
update.id = instance.uid
// 首次执行
update()
}注意 beforeMount 和 beforeUpdate 是同步调用的(invokeArrayFns 直接执行),而 mounted 和 updated 是通过 queuePostRenderEffect 延迟到 post 队列执行的。