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
  }
}

几个关键设计点:

  1. currentInstance 绑定:钩子函数在注册时捕获当前组件实例,调用时恢复。这保证了即使钩子被延迟执行(如 mounted 在异步 flush 中执行),也能正确访问组件上下文。

  2. 暂停追踪:钩子函数中的响应式访问不应该被收集为依赖,否则会导致不可预期的重渲染。

  3. 错误处理:所有钩子调用都通过 callWithAsyncErrorHandling 包装,支持 onErrorCaptured 的错误冒泡机制。

  4. 卸载检查:如果组件已经卸载,钩子直接跳过,避免操作已清理的状态。

生命周期枚举

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'
}

注意这些简写——bmmbuu——它们直接作为组件实例的属性名。这不是偷懒,而是刻意的优化:短属性名在 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()
}

注意 beforeMountbeforeUpdate同步调用的(invokeArrayFns 直接执行),而 mountedupdated 是通过 queuePostRenderEffect 延迟到 post 队列执行的。

基于 VitePress 构建