Skip to content

第 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 本身就是在 beforeCreatecreated 之间执行的,所以 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
  }
}

基于 VitePress 构建