Skip to content

实现生命周期 Hooks

本节对标 Vue 3 源码 @vue/runtime-core 中的 apiLifecycle.ts + component.ts + renderer.ts

生命周期概览

Vue 3 提供了一组 Composition API 风格的生命周期 Hooks,必须在 setup() 中同步调用:

setup()

  ├── onBeforeMount    ── 组件挂载前
  ├── onMounted        ── 组件挂载后(DOM 已就绪)

  ├── onBeforeUpdate   ── 响应式数据变更,DOM 更新前
  ├── onUpdated        ── DOM 更新后

  ├── onBeforeUnmount  ── 组件卸载前
  └── onUnmounted      ── 组件卸载后

对标源码位置:packages/runtime-core/src/apiLifecycle.ts

currentInstance 机制

生命周期 Hooks 的核心依赖是 currentInstance——一个模块级变量,标识当前正在执行 setup() 的组件实例。

ts
// packages/runtime-core/src/component.ts

// 模块级变量,存储当前正在执行 setup() 的组件实例
// 在 setup() 执行期间被设置,执行完毕后被清除为 null
export let currentInstance: ComponentInternalInstance | null = null

// 设置当前活跃的组件实例,在 setup() 调用前后分别设置和清除
export function setCurrentInstance(instance: ComponentInternalInstance | null) {
  currentInstance = instance
}

// 获取当前活跃的组件实例,供生命周期 Hooks、inject() 等 API 内部使用
export function getCurrentInstance(): ComponentInternalInstance | null {
  return currentInstance
}

setupComponent 中,执行 setup() 前后会设置和清除 currentInstance

ts
function setupStatefulComponent(instance: ComponentInternalInstance) {
  const Component = instance.type // 获取组件的选项对象(即用户定义的组件配置)
  const { setup } = Component // 解构出 setup 函数

  if (setup) {
    // 在执行 setup() 之前,将当前组件实例设置为活跃实例
    // 这样 setup() 内部调用的 onMounted 等 API 才能获取到正确的实例
    setCurrentInstance(instance)
    // 调用 setup(),传入只读的 props 和上下文对象(emit、slots、attrs、expose)
    const setupResult = setup(shallowReadonly(instance.props), {
      emit: instance.emit,
      slots: instance.slots,
      attrs: instance.attrs,
      expose: instance.expose,
    })
    // setup() 执行完毕后立即清除 currentInstance
    // 防止在异步回调中误用生命周期注册 API
    setCurrentInstance(null)

    // 处理 setup() 的返回值(可能是渲染函数或状态对象)
    handleSetupResult(instance, setupResult)
  }
}

关键设计setCurrentInstance(null)setup() 执行完毕后立即调用。这意味着如果在异步回调(如 setTimeoutawait 之后)中调用 onMountedcurrentInstance 已经是 null,注册会失败。这是有意为之的设计约束。

组件实例上的生命周期数组

每个组件实例上预定义了生命周期 Hook 数组:

ts
// packages/runtime-core/src/component.ts

// 组件内部实例的接口定义
export interface ComponentInternalInstance {
  // ...
  isMounted: boolean    // 标记组件是否已挂载
  isUnmounted: boolean  // 标记组件是否已卸载

  // lifecycle hooks —— 使用缩写命名以减少内存占用
  bc: Function[] | null   // beforeCreate  (Options API,不在此讨论)
  c: Function[] | null    // created
  bm: Function[] | null   // beforeMount —— 挂载前的回调数组
  m: Function[] | null    // mounted —— 挂载后的回调数组
  bu: Function[] | null   // beforeUpdate —— 更新前的回调数组
  u: Function[] | null    // updated —— 更新后的回调数组
  bum: Function[] | null  // beforeUnmount —— 卸载前的回调数组
  um: Function[] | null   // unmounted —— 卸载后的回调数组
}

// 工厂函数:创建组件实例对象
function createComponentInstance(vnode, parent): ComponentInternalInstance {
  const instance: ComponentInternalInstance = {
    vnode,                // 组件对应的虚拟节点
    type: vnode.type,     // 组件的选项对象(包含 setup、render 等)
    parent,               // 父组件实例,用于 provide/inject 链式查找
    isMounted: false,     // 初始状态:未挂载
    isUnmounted: false,   // 初始状态:未卸载
    // lifecycle hooks - 初始化为 null,注册时按需创建数组
    // 使用 null 而非空数组可以减少内存开销,因为大部分组件不会注册所有生命周期
    bc: null,
    c: null,
    bm: null,
    m: null,
    bu: null,
    u: null,
    bum: null,
    um: null,
    // ...
  }
  return instance
}

使用 null 而不是空数组,是为了减少内存开销——大部分组件不会注册所有生命周期。

createHook 工厂函数

所有生命周期 Hooks 的注册逻辑几乎相同,Vue 3 使用 createHook 工厂函数来消除重复代码:

ts
// packages/runtime-core/src/apiLifecycle.ts
import { currentInstance, setCurrentInstance } from './component'

// 使用 const enum 定义生命周期类型常量
// const enum 在编译时会被内联替换为实际值,零运行时开销
export const enum LifecycleHooks {
  BEFORE_MOUNT = 'bm',    // 对应组件实例上的 bm 数组
  MOUNTED = 'm',           // 对应组件实例上的 m 数组
  BEFORE_UPDATE = 'bu',    // 对应组件实例上的 bu 数组
  UPDATED = 'u',           // 对应组件实例上的 u 数组
  BEFORE_UNMOUNT = 'bum',  // 对应组件实例上的 bum 数组
  UNMOUNTED = 'um',        // 对应组件实例上的 um 数组
}

// 核心注入函数:将生命周期回调注册到目标组件实例上
function injectHook(
  type: LifecycleHooks,       // 生命周期类型(决定注入到哪个数组)
  hook: Function,              // 用户传入的回调函数
  target: ComponentInternalInstance | null = currentInstance, // 默认注入到当前活跃实例
) {
  if (target) {
    // 延迟创建数组:如果实例上该类型的数组不存在,则创建一个空数组
    // 这是按需创建的策略,避免为未使用的生命周期分配内存
    const hooks = target[type] || (target[type] = [])
    // 对用户传入的 hook 进行包装,在执行时恢复 currentInstance 上下文
    const wrappedHook = (...args: unknown[]) => {
      // 在调用 hook 时恢复 currentInstance
      // 这样 hook 内部调用的 inject() 等 API 也能正确获取实例
      setCurrentInstance(target)
      const res = hook(...args) // 执行用户的回调函数
      setCurrentInstance(null)  // 执行完毕后清除,避免污染全局状态
      return res
    }
    hooks.push(wrappedHook) // 将包装后的 hook 推入对应的生命周期数组
    return wrappedHook       // 返回包装后的 hook,便于后续移除
  } else if (__DEV__) {
    // 开发环境下,如果没有活跃的组件实例,输出警告信息
    // 这意味着用户在 setup() 之外调用了生命周期 API
    const apiName = `on${type.charAt(0).toUpperCase()}${type.slice(1)}`
    console.warn(
      `${apiName} is called when there is no active component instance to be associated with. ` +
      `Lifecycle injection APIs can only be used during execution of setup().`,
    )
  }
}

// 工厂函数:利用闭包捕获生命周期类型,返回对应的注册函数
// 这种模式避免了为每个生命周期 Hook 重复编写注册逻辑
const createHook = (lifecycle: LifecycleHooks) => {
  return (hook: Function, target: ComponentInternalInstance | null = currentInstance) =>
    injectHook(lifecycle, hook, target)
}

// 导出所有生命周期 Hooks,每个都是通过 createHook 工厂函数创建的
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)

设计分析

  1. 工厂模式createHook 通过闭包捕获 lifecycle 类型,避免每个 Hook 重复编写注册逻辑
  2. 延迟创建数组target[type] || (target[type] = []) 按需创建,节省内存
  3. wrappedHook 恢复 instance:确保生命周期回调内部可以正常使用 getCurrentInstance()inject() 等 API
  4. 开发环境警告:在 setup() 外部调用时给出明确提示

invokeArrayFns 工具函数

生命周期 Hook 数组的批量调用使用统一的工具函数:

ts
// packages/shared/src/index.ts

// 批量调用函数数组的工具函数
// 使用 for 循环而非 forEach,避免闭包和函数调用开销,性能更优
export function invokeArrayFns(fns: Function[], arg?: any) {
  for (let i = 0; i < fns.length; i++) {
    fns[i](arg) // 按注册顺序依次执行每个函数,支持传入一个可选参数
  }
}

这个函数简洁高效:

  • 使用 for 循环而非 forEach,避免闭包和函数调用开销
  • 支持传入一个可选参数
  • 数组中的函数按注册顺序依次执行

调用时机:componentUpdateFn

生命周期 Hooks 的调用发生在渲染器的 componentUpdateFn 中,这是组件的核心渲染/更新函数:

ts
// packages/runtime-core/src/renderer.ts

// 设置组件的渲染副作用:创建响应式 effect 并绑定更新函数
function setupRenderEffect(
  instance: ComponentInternalInstance,
  initialVNode: VNode,
  container: any,
  anchor: any,
) {
  // 组件的核心渲染/更新函数,在首次挂载和后续更新时都会被调用
  const componentUpdateFn = () => {
    if (!instance.isMounted) {
      // ============ MOUNT 流程(首次渲染)============
      const { bm, m } = instance // 解构出 beforeMount 和 mounted 的 hook 数组

      // beforeMount hook —— 在 DOM 创建之前同步调用
      if (bm) {
        invokeArrayFns(bm)
      }

      // 执行组件的 render 函数,生成子树 VNode(虚拟 DOM 树)
      const subTree = (instance.subTree = renderComponentRoot(instance))

      // 递归 patch 子树,将虚拟 DOM 转化为真实 DOM 并插入容器
      patch(null, subTree, container, anchor, instance)

      // 将子树的根 DOM 元素赋给组件的 VNode,便于后续访问
      initialVNode.el = subTree.el

      // mounted hook —— 此时 DOM 已挂载到页面
      if (m) {
        // Vue 源码中 mounted 是通过 queuePostRenderEffect 异步调用的
        // 确保所有子组件也已完成挂载后再执行
        // 简化实现中可以同步调用
        queuePostRenderEffect(m)
      }

      // 标记组件已挂载,后续更新将走 else 分支
      instance.isMounted = true
    } else {
      // ============ UPDATE 流程(响应式数据变更触发)============
      const { bu, u } = instance // 解构出 beforeUpdate 和 updated 的 hook 数组

      // beforeUpdate hook —— 在 DOM 更新之前同步调用
      if (bu) {
        invokeArrayFns(bu)
      }

      // 重新执行 render 函数,生成新的子树 VNode
      const nextTree = renderComponentRoot(instance)
      const prevTree = instance.subTree // 保存旧的子树用于 diff 对比
      instance.subTree = nextTree       // 更新实例上的子树引用

      // 对比新旧子树,执行最小化的 DOM 更新(diff + patch)
      patch(prevTree, nextTree, container, anchor, instance)

      // updated hook —— 此时 DOM 已完成更新
      if (u) {
        queuePostRenderEffect(u) // 放入后置队列,确保子组件也更新完毕后再执行
      }
    }
  }

  // 创建响应式副作用:当组件依赖的响应式数据变化时,触发调度器
  // 调度器不会直接执行 componentUpdateFn,而是通过 queueJob 将更新任务加入队列
  // 这样可以在同一 tick 中合并多次数据变更,只执行一次更新
  const effect = new ReactiveEffect(componentUpdateFn, () =>
    queueJob(update),
  )

  // 将 effect.run 绑定为组件的 update 方法,便于手动触发更新
  const update = (instance.update = () => effect.run())
  update() // 首次调用,触发 MOUNT 流程
}

调用顺序详解

挂载阶段

1. onBeforeMount  ← 同步调用,DOM 尚未创建
2. render()       ← 生成 VNode 子树
3. patch()        ← 递归挂载,创建真实 DOM
4. onMounted      ← 异步调用(queuePostRenderEffect),DOM 已就绪

更新阶段

1. onBeforeUpdate ← 同步调用,DOM 尚未更新
2. render()       ← 生成新 VNode 子树
3. patch()        ← 对比更新 DOM
4. onUpdated      ← 异步调用(queuePostRenderEffect),DOM 已更新

注意onMountedonUpdated 在 Vue 3 源码中是通过 queuePostRenderEffect 放入 post flush 队列异步执行的,确保子组件也已完成挂载/更新。

调用时机:unmountComponent

卸载生命周期在 unmountComponent 中调用:

ts
// packages/runtime-core/src/renderer.ts

// 卸载组件:依次触发卸载生命周期并递归卸载子树
function unmountComponent(instance: ComponentInternalInstance) {
  const { bum, um, subTree } = instance // 解构出 beforeUnmount、unmounted hook 数组和子树

  // beforeUnmount hook —— 同步调用,此时组件仍然完整可用
  // 可以在这里执行清理操作,如取消定时器、移除事件监听器等
  if (bum) {
    invokeArrayFns(bum)
  }

  // 递归卸载子树,移除所有真实 DOM 节点
  unmount(subTree)

  // unmounted hook —— 放入后置队列异步执行
  // 此时组件的 DOM 已被移除,所有子组件也已卸载
  if (um) {
    queuePostRenderEffect(um)
  }

  // 标记组件已卸载,防止重复卸载
  instance.isUnmounted = true
}

卸载阶段

1. onBeforeUnmount ← 同步调用,组件仍然完整可用
2. unmount()       ← 递归卸载子树,移除 DOM
3. onUnmounted     ← 异步调用,组件已完全卸载

父子组件生命周期执行顺序

Parent setup()

  ├── Parent onBeforeMount
  │     │
  │     └── Child setup()
  │           ├── Child onBeforeMount
  │           ├── Child render + patch
  │           └── Child onMounted

  └── Parent onMounted

更新时:
  Parent onBeforeUpdate
    └── Child onBeforeUpdate
        └── Child onUpdated
  Parent onUpdated

卸载时:
  Parent onBeforeUnmount
    └── Child onBeforeUnmount
        └── Child onUnmounted
  Parent onUnmounted

规律:mount/update/unmount 都是"父先开始,子先完成"——即父组件的 before* 先执行,但子组件的完成回调先执行。

对比 React

维度Vue 3 Lifecycle HooksReact useEffect
调用约束必须在 setup() 中同步调用必须在函数组件顶层调用
挂载时机onMounted —— DOM 挂载后同步/微任务useEffect —— 浏览器绘制后异步执行
更新时机onBeforeUpdate / onUpdated 精确区分更新前后useEffect 无法区分"更新前"
卸载时机onBeforeUnmount / onUnmounted 两个阶段useEffect 返回的 cleanup 函数
粒度每个生命周期独立注册useEffect 统一处理挂载、更新、卸载
依赖追踪自动响应式追踪,无需手动指定依赖必须手动声明 deps 数组
执行时序onMounted 在微任务中执行useEffect 在宏任务(paint 后)执行
同步 DOM 访问onMounted 可安全访问 DOM需要 useLayoutEffect 才能在 paint 前访问

关键差异:执行时序

Vue 3:
  render → DOM 更新 → 微任务队列 → onMounted/onUpdated
  (在同一帧内,浏览器绘制前执行)

React:
  render → DOM 更新 → 浏览器绘制 → useEffect
  (在下一帧,浏览器绘制后异步执行)

React useLayoutEffect:
  render → DOM 更新 → useLayoutEffect → 浏览器绘制
  (类似 Vue 3 的 onMounted 时序)

简化实现

ts
// apiLifecycle.ts
import { currentInstance } from './component'

// 生命周期类型枚举,值对应组件实例上的属性名缩写
export const enum LifecycleHooks {
  BEFORE_MOUNT = 'bm',
  MOUNTED = 'm',
  BEFORE_UPDATE = 'bu',
  UPDATED = 'u',
  BEFORE_UNMOUNT = 'bum',
  UNMOUNTED = 'um',
}

// 简化版的 injectHook:省略了 wrappedHook 包装逻辑
// 直接将用户的回调推入目标实例对应的生命周期数组
function injectHook(type: LifecycleHooks, hook: Function, target = currentInstance) {
  if (target) {
    // 按需创建数组并推入 hook
    const hooks = target[type] || (target[type] = [])
    hooks.push(hook)
  }
}

// 工厂函数:通过闭包绑定生命周期类型,返回注册函数
const createHook = (lifecycle: LifecycleHooks) => {
  return (hook: Function) => injectHook(lifecycle, hook)
}

// 导出六个核心生命周期 Hooks
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)
ts
// shared/index.ts

// 工具函数:遍历函数数组并逐个调用
// 用于批量执行生命周期 hook 回调
export function invokeArrayFns(fns: Function[], arg?: any) {
  for (let i = 0; i < fns.length; i++) {
    fns[i](arg) // 将可选参数传递给每个回调函数
  }
}

测试用例

ts
import { describe, it, expect, vi } from 'vitest'
import {
  h,
  render,
  ref,
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  nextTick,
} from '../src'

describe('lifecycle hooks', () => {
  // 测试:onBeforeMount 和 onMounted 在组件挂载时被正确调用
  it('onBeforeMount and onMounted', () => {
    const beforeMount = vi.fn() // 创建 mock 函数用于追踪调用
    const mounted = vi.fn()

    const Comp = {
      setup() {
        // 在 setup() 中注册生命周期 Hooks
        onBeforeMount(beforeMount)
        onMounted(mounted)
        return () => h('div', null, 'hello') // 返回渲染函数
      },
    }

    const root = document.createElement('div') // 创建挂载容器
    render(h(Comp), root) // 渲染组件到容器

    // 断言:两个 hook 各被调用了一次
    expect(beforeMount).toHaveBeenCalledTimes(1)
    expect(mounted).toHaveBeenCalledTimes(1)
    // 断言:DOM 已正确渲染
    expect(root.innerHTML).toBe('<div>hello</div>')
  })

  // 测试:onBeforeUpdate 和 onUpdated 在响应式数据变更后被调用
  it('onBeforeUpdate and onUpdated', async () => {
    const beforeUpdate = vi.fn()
    const updated = vi.fn()
    const count = ref(0) // 创建响应式数据

    const Comp = {
      setup() {
        onBeforeUpdate(beforeUpdate)
        onUpdated(updated)
        // 渲染函数中依赖了 count.value,当 count 变化时会触发更新
        return () => h('div', null, String(count.value))
      },
    }

    const root = document.createElement('div')
    render(h(Comp), root)
    // 断言:首次渲染后 DOM 显示 0
    expect(root.innerHTML).toBe('<div>0</div>')
    // 断言:首次挂载时不会触发 update 相关的 hook
    expect(beforeUpdate).not.toHaveBeenCalled()
    expect(updated).not.toHaveBeenCalled()

    // 修改响应式数据,触发组件更新
    count.value++
    // 等待异步调度完成(Vue 的更新是批量异步执行的)
    await nextTick()

    // 断言:DOM 已更新为新值
    expect(root.innerHTML).toBe('<div>1</div>')
    // 断言:更新相关的 hook 各被调用了一次
    expect(beforeUpdate).toHaveBeenCalledTimes(1)
    expect(updated).toHaveBeenCalledTimes(1)
  })

  // 测试:onBeforeUnmount 和 onUnmounted 在组件卸载时被调用
  it('onBeforeUnmount and onUnmounted', () => {
    const beforeUnmount = vi.fn()
    const unmounted = vi.fn()

    const Comp = {
      setup() {
        onBeforeUnmount(beforeUnmount)
        onUnmounted(unmounted)
        return () => h('div', null, 'hello')
      },
    }

    const root = document.createElement('div')
    render(h(Comp), root)
    // 断言:挂载时卸载相关的 hook 不应被调用
    expect(beforeUnmount).not.toHaveBeenCalled()
    expect(unmounted).not.toHaveBeenCalled()

    // 渲染 null 触发组件卸载
    render(null, root)
    // 断言:卸载后两个 hook 各被调用了一次
    expect(beforeUnmount).toHaveBeenCalledTimes(1)
    expect(unmounted).toHaveBeenCalledTimes(1)
  })

  // 测试:父子组件生命周期的执行顺序——"父先开始,子先完成"
  it('lifecycle call order for parent and child', async () => {
    const calls: string[] = [] // 用于记录生命周期的调用顺序

    const Child = {
      setup() {
        // 子组件注册的生命周期 hook 会将标识字符串推入 calls 数组
        onBeforeMount(() => calls.push('child beforeMount'))
        onMounted(() => calls.push('child mounted'))
        onBeforeUnmount(() => calls.push('child beforeUnmount'))
        onUnmounted(() => calls.push('child unmounted'))
        return () => h('span')
      },
    }

    const Parent = {
      setup() {
        // 父组件同样注册生命周期 hook
        onBeforeMount(() => calls.push('parent beforeMount'))
        onMounted(() => calls.push('parent mounted'))
        onBeforeUnmount(() => calls.push('parent beforeUnmount'))
        onUnmounted(() => calls.push('parent unmounted'))
        // 父组件渲染子组件
        return () => h('div', null, [h(Child)])
      },
    }

    const root = document.createElement('div')
    render(h(Parent), root)

    // 断言挂载顺序:父 beforeMount → 子 beforeMount → 子 mounted → 父 mounted
    // 体现了"父先开始,子先完成"的嵌套执行模型
    expect(calls).toEqual([
      'parent beforeMount',
      'child beforeMount',
      'child mounted',
      'parent mounted',
    ])

    calls.length = 0 // 清空调用记录
    render(null, root) // 卸载整个组件树

    // 断言卸载顺序:父 beforeUnmount → 子 beforeUnmount → 子 unmounted → 父 unmounted
    expect(calls).toEqual([
      'parent beforeUnmount',
      'child beforeUnmount',
      'child unmounted',
      'parent unmounted',
    ])
  })

  // 测试:同一生命周期可以注册多个 hook,按注册顺序执行
  it('can register multiple hooks of the same type', () => {
    const calls: string[] = []

    const Comp = {
      setup() {
        // 连续注册三个 onMounted hook
        onMounted(() => calls.push('mounted 1'))
        onMounted(() => calls.push('mounted 2'))
        onMounted(() => calls.push('mounted 3'))
        return () => h('div')
      },
    }

    const root = document.createElement('div')
    render(h(Comp), root)

    // 断言:三个 hook 按注册顺序依次执行
    expect(calls).toEqual(['mounted 1', 'mounted 2', 'mounted 3'])
  })

  // 测试:在 setup() 外部调用生命周期 API 应触发警告
  it('should warn if called outside setup()', () => {
    // 拦截 console.warn 调用
    const spy = vi.spyOn(console, 'warn').mockImplementation(() => {})

    // 在 setup() 外部直接调用 onMounted,此时 currentInstance 为 null
    onMounted(() => {})

    // 断言:应该输出警告信息
    expect(spy).toHaveBeenCalled()
    spy.mockRestore() // 恢复 console.warn 原始行为
  })
})

本节小结

  1. currentInstance —— 模块级变量,在 setup() 执行期间指向当前组件实例,是生命周期注册的核心依赖
  2. createHook 工厂 —— 通过闭包捕获生命周期类型,统一注册逻辑,所有 Hook 调用 injectHook 将回调推入实例对应数组
  3. invokeArrayFns —— 简洁的批量调用工具函数,for 循环遍历执行
  4. 调用时机 —— beforeMount/beforeUpdate 同步调用在 render 之前,mounted/updated 通过 post flush 队列在 DOM 操作完成后调用
  5. 父子顺序 —— "父先开始,子先完成"的嵌套执行模型

下一节实现 nextTick 与异步调度机制。

用心学习,用代码说话 💻