一文吃透 Vue3 组件的 16 个生命周期

3,497 阅读30分钟

Vue3 中有两种注册生命周期的方法,一种是选项式的 API 风格,另一种的组合式的 API 风格。Vue3 的生命周期是完全兼容了 Vue2 的生命周期的。生命周期选项和组合式 API 中的生命周期钩子可以混合使用,但是不建议将两者混合使用,因为会增加用户的心智负担。

生命周期的实现原理其实就是先将用户注册的生命周期函数存起来,然后在合适的时机调用它。

各个生命周期的执行时机与应用场景

Vue3 组件生命周期选项有

  • beforeCreate
  • created
  • beforeMount
  • mounted
  • beforeUpdate
  • updated
  • beforeUnmount
  • unmounted
  • errorCaptured
  • renderTracked
  • renderTriggered
  • activated
  • deactivated
  • serverPrefetch

在组合式 API 中,beforeCreatecreated 生命周期由 setup 函数代替。即在 beforeCreatecreated 生命周期选项中编写的代码,都可以写在 setup 函数中。同一个生命周期,同时用选项式 API 和组合式 API 中注册,组合式 API 中的生命周期函数会先于选项式 API 中的执行。

Vue.createApp({
  mounted: () => {
    // 后输出
    console.error('mounted1')
  },
  setup () {
    Vue.onMounted(() => {
      // 先输出
      console.error('mounted2')
    })
  }
})

mounted2 会比 mounted1 先输出。

在组合式 API 中,生命周期钩子有

  • onBeforeMount()
  • onMounted()
  • onBeforeUpdate()
  • onUpdated()
  • onBeforeUnmount()
  • onUnmounted()
  • onErrorCaptured()
  • onRenderTracked()
  • onRenderTriggered()
  • onActivated()
  • onDeactivated()
  • onServerPrefetch()

选项式 API 与组合式 API 中生命周期的对应是关系是:

  • beforeCreatecreated 对应 setup 函数

  • beforeMountmounted 对应 onBeforeMount()onMounted()

  • beforeUpdateupdated 对应 onBeforeUpdate()onUpdated()

  • beforeUnmountunmounted 对应 onBeforeUnmount()onUnmounted()

  • errorCaptured 对应 onErrorCaptured()

  • renderTrackedrenderTriggered 对应 onRenderTracked()onRenderTriggered()

  • activateddeactivated 对应 onActivated()onDeactivated()

  • serverPrefetch 对应 onServerPrefetch()

其中 onRenderTracked()onRenderTriggered() 是 Vue3 新增的用于调试的生命周期钩子。

其实生命周期钩子函数就是在组件特定的时候运行的函数,用于给用户在组件的不同阶段添加自己的代码。

  • beforeCreate 会在组件实例初始化完成并且 props 被解析后立即调用。所以 beforeCreate 可以正确拿到 props 中的值,但是此时 data 选项还未被处理,因此拿不到 data 选项中的数据。

注意,组合式 API 中的 setup() 钩子会在所有选项式 API 钩子之前调用,beforeCreate() 也不例外。在组合式 API 中,setup() 钩子代替了 beforeCreate()created() 生命周期选项。

  • created 在组件实例处理完所有与状态相关的选项后调用。此时响应式数据、计算属性、方法和侦听器已被配置完成。因此,在 created 中,可以正确拿到 data 、计算属性的数据、调用组件定义的方法,但由于挂载阶段还未开始,$el 属性仍不可用。

在组合式 API 中,setup 钩子函数替代了 beforeCreate 和 created 选项。

  • onBeforeMount() 注册一个钩子,在组件被挂载之前被调用。当这个钩子被调用时,组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程。这个钩子在服务器端渲染期间不会被调用

  • onMounted() 注册一个回调函数,在组件挂载完成后执行。可以在这个生命周期中操作组件的 DOM 。这个钩子在服务器端渲染期间不会被调用

  • onBeforeUpdate() 注册一个钩子,在组件即将因为响应式状态变更而更新其 DOM 树之前调用。这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态。在这个钩子中更改状态也是安全的。这个钩子在服务器端渲染期间不会被调用

  • onUpdated() 注册一个回调函数,在组件因为响应式状态变更而更新其 DOM 树之后调用。父组件的更新钩子在其子组件的更新钩子之后调用。这个钩子会在组件的任意 DOM 更新后被调用,这些更新可能是由不同的状态变更导致的,因为多个状态变更可以在同一个渲染周期中批量执行(考虑到性能因素)。如果你需要在某个特定的状态更改后访问更新后的 DOM,可以使用 nextTick() 作为替代。这个钩子在服务器端渲染期间不会被调用

注意,不要在 updated 钩子中更改组件的状态,因为这会导致无限的更新循环。

  • onBeforeUnmount() 注册一个钩子,在组件实例被卸载之前调用。这个钩子在服务器端渲染期间不会被调用

  • onUnmounted() 注册一个回调函数,在组件实例被卸载之后调用。可以在这个钩子中手动清理一些副作用,比如:取消全局事件绑定,销毁定时器等。从而防止浏览器内存泄漏。这个钩子在服务器端渲染期间不会被调用

  • onErrorCaptured() 注册一个钩子,在捕获了后代组件传递的错误时调用。

有关 onErrorCaptured() 更多详细信息可见笔者写的另一篇文章:深入源码,剖析 Vue3 是如何做错误处理的

  • onRenderTracked() 注册一个调试钩子,当组件渲染过程中追踪到响应式依赖时调用。这个钩子仅在开发模式下可用,且在服务器端渲染期间不会被调用

  • onRenderTriggered() 注册一个调试钩子,当响应式依赖的变更触发了组件渲染时调用。这个钩子仅在开发模式下可用,且在服务器端渲染期间不会被调用

  • onActivated() ,仅针对 KeepAlive 包裹的组件。注册一个回调函数,当组件被插入到 DOM 中时调用。这个钩子在服务器端渲染期间不会被调用

  • onDeactivated() ,仅针对 KeepAlive 包裹的组件。注册一个回调函数,当组件从 DOM 中被移除时调用。这个钩子在服务器端渲染期间不会被调用

  • onServerPrefetch() 仅在服务端渲染期间使用,用于注册一个异步函数,在组件实例在服务器上被渲染之前调用。可用于在服务器上请求后台接口数据,它比在客户端上请求后台数据更快。

各个生命周期的实现源码分析

渲染器遇到组件,会执行组件初始化的流程,其中会执行 setupComponent 函数。在 setupComponent 函数中会执行初始化组件 props 、初始化组件插槽和判断组件是否为有状态组件,如果遇到的组件是有状态的组件,则会取得用户注册的 setup 函数,然后执行,然而此时,组件实例中还不存在 beforeCreate 、create 等组件选项(选项式 API 还未初始化),因此 setup 钩子会比之前的 beforeCreate 和 create 生命周期选项先执行。

同时,在 Vue.js 的官方文档中也特意说明了组合式 API 中的 setup() 钩子会在所有选项式 API 钩子之前调用。从源码中我们也可以得知,在 setup() 钩子执行的时候,选项式 API 还未初始化。

pic28.png

👆 链接为:cn.vuejs.org/api/options…

组件要能够执行用户注册的生命周期钩子函数,就先要将用户注册的生命钩子函数保存到组件的实例中。无论是生命周期选项,还是生命周期钩子函数,整体的实现逻辑都是先将用户注册的生命周期函数存储到组件实例中,然后在合适的时机取出对应的生命周期函数来执行

为了方便维护,以及提高代码可读性, Vue 内部给各个生命周期钩子定义了枚举值

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

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

beforeCreate、created 生命周期选项的实现

在组合式 API 中 beforeCreate、created 已被 setup 钩子代替,但是 Vue3 仍然支持使用 beforeCreate、 created 生命周期选项,这主要是 applyOptions 函数实现的,applyOptions 函数让 Vue3 仍然支持选项式 API 的写法。

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

export function applyOptions(instance: ComponentInternalInstance) {
  const options = resolveMergedOptions(instance)
  // 在初始化其他选项(option)前,先执行 beforeCreate 生命周期函数
  if (options.beforeCreate) {
    callHook(options.beforeCreate, instance, LifecycleHooks.BEFORE_CREATE)
  }

  const {
    created,
    // 其他生命周期选项
  } = options

  if (injectOptions) {
    // 初始化组件的 inject 选项
  }

  if (methods) {
    // 初始化组件的 method 选项
  }

  if (dataOptions) {
    // 初始化组件的 data 选项
  }

  if (computedOptions) {
    // 初始化组件的 computed 选项
  }

  if (watchOptions) {
    // 初始化组件的 watch 选项
  }

  if (provideOptions) {
    // 初始化组件的 provide 选项
  }

  if (created) {
    // 执行组件的 created 生命周期函数
    callHook(created, instance, LifecycleHooks.CREATED)
  }

  // 注册其他生命周期函数
  // ...

  if (__COMPAT__) {
    // 兼容 Vue2 版本的构建
    // 注册 beforeDestroy 、destroyed 生命周期选项
  }

  if (isArray(expose)) {
    // 初始化 expose 选项
  }

  if (render && instance.render === NOOP) {
    // 将 render 选项存储到组件实例中
  }

  if (inheritAttrs != null) {
    // 是否继承 Attributes 选项存储到组件实例中
  }

  if (components) {
    // 将 components 选项存储到组件实例中
  }

  if (directives) {
    // 将自定义指令选项存储到组件实例中
  }

  if (
    __COMPAT__ &&
    filters &&
    isCompatEnabled(DeprecationTypes.FILTERS, instance)    
  ) {
    // 兼容 Vue2 并且存在过滤器,而且用户配置过滤器兼容特性为 true    
  }
}

本文中的源码均摘自 Vue.js 3.2.45,为了方便理解,会省略与本文主题无关的代码

beforeCreate 会在初始化选项 API 前执行,created 选项则在处理完所有与状态相关的选项后调用。

beforeCreate 、created 在 Vue 内部是用数组来存储的,因为组件可以注册多个 beforeCreate 、created 生命周期选项,比如来自 mixins 中的生命周期选项。

beforeCreate 、created 生命周期选项统一由 callHook 函数执行。

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

function callHook(
  hook: Function,
  instance: ComponentInternalInstance,
  type: LifecycleHooks
) {
  callWithAsyncErrorHandling(
    isArray(hook)
      ? hook.map(h => h.bind(instance.proxy!))
      : hook.bind(instance.proxy!),
    instance,
    type
  )
}

callHook 函数其实只是封装了 callWithAsyncErrorHandling 函数,callWithAsyncErrorHandling 函数是异步异常处理函数,传入 callWithAsyncErrorHandling 函数的函数会被执行,如果调用传入的函数时发生错误,该错误会被捕获。

有关 callWithAsyncErrorHandling 函数的更多内容可查看笔者写的另一篇文章:深入源码,剖析 Vue3 是如何做错误处理的

总结一下 beforeCreate、created 生命周期选项的实现:在 applyOptions 函数中,会从组件实例中获取用户注册的 beforeCreate、created 函数,然后在组件实例初始化完成后(此时选项式 API 还未初始化)立即调用 beforeCreate 函数,然后在所有与状态相关的选项初始化完后,调用 created 函数。

onBeforeMount 生命周期钩子与 beforeMount 生命周期选项的实现

onBeforeMount 生命周期钩子和 beforeMount 生命周期选项的底层实现是一样的,它们只是 API 的形式不一样。

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

// 注册 beforeMount 生命周期钩子的函数
export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
import { onBeforeMount } from './apiLifecycle'

export function applyOptions(instance: ComponentInternalInstance) {
  const options = resolveMergedOptions(instance)

  const {
    beforeMount
  } = options

  function registerLifecycleHook(
    register: Function,
    hook?: Function | Function[]
  ) {
    if (isArray(hook)) {
      hook.forEach(_hook => register(_hook.bind(publicThis)))
    } else if (hook) {
      register((hook as Function).bind(publicThis))
    }
  }

  // 注册 beforeMount 生命周期选项(钩子)
  registerLifecycleHook(onBeforeMount, beforeMount)
}

可以看到 beforeMount 生命周期选项是基于 onBeforeMount 生命周期钩子实现的。

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

export const createHook =
  <T extends Function = () => any>(lifecycle: LifecycleHooks) =>
  (hook: T, target: ComponentInternalInstance | null = currentInstance) =>
    // post-create lifecycle registrations are noops during SSR (except for serverPrefetch)
    (!isInSSRComponentSetup || lifecycle === LifecycleHooks.SERVER_PREFETCH) &&
    injectHook(lifecycle, (...args: unknown[]) => hook(...args), target)

createHook 函数过滤了服务器渲染场景和 onServerPrefetch() 生命周期钩子,同时 onErrorCaptured 生命周期钩子函数是单独注册的(下文会讲 onErrorCaptured 生命周期钩子的实现),因此,在服务端渲染期间,除了 onServerPrefetch()onErrorCaptured() 生命周期钩子,其它生命周期钩子都不执行。

createHook 函数会返回一个函数,它内部通过 injectHook 函数注册钩子函数。这里使用了函数柯里化的技巧来实现参数复用。先传入代表生命周期类型的枚举值,在调用 createHook 返回的函数时,便不需要再传入代表生命周期类型的枚举值了,因为它在执行 createHook 函数时就已经实现了该参数的保留(即参数复用)。

然后将用户传入的生命周期钩子函数用一层函数包裹作为 injectHook 函数的参数,因为生命周期钩子函数不能马上执行,都有特定的执行时机,所以injectHook 函数会先将用户传入的生命周期钩子函数存到组件的实例中,等到特定的时机再执行。

// 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) {
    const hooks = target[type] || (target[type] = [])
    // cache the error handling wrapper for injected hooks so the same hook
    // can be properly deduped by the scheduler. "__weh" stands for "with error
    // handling".
    const wrappedHook =
      hook.__weh ||
      (hook.__weh = (...args: unknown[]) => {
        if (target.isUnmounted) {
          return
        }
        // disable tracking inside all lifecycle hooks
        // since they can potentially be called inside effects.
        pauseTracking()
        // Set currentInstance during hook invocation.
        // This assumes the hook does not synchronously trigger other hooks, which
        // can only be false when the user does something really funky.
        setCurrentInstance(target)
        const res = callWithAsyncErrorHandling(hook, target, type, args)
        unsetCurrentInstance()
        resetTracking()
        return res
      })
    if (prepend) {
      hooks.unshift(wrappedHook)
    } else {
      hooks.push(wrappedHook)
    }
    return wrappedHook
  } else if (__DEV__) {
    const apiName = toHandlerKey(ErrorTypeStrings[type].replace(/ hook$/, ''))
    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().` +
        (__FEATURE_SUSPENSE__
          ? ` If you are using async setup(), make sure to register lifecycle ` +
            `hooks before the first await statement.`
          : ``)
    )
  }
}

用户注册的生命周期钩子函数会以数组的形式存储到组件实例中,代表生命周期类型的枚举值会作为组件实例上的 key ,到特定的时机,会通过 key 取到对于的生命周期钩子函数来执行。

// 用户注册的生命周期钩子函数会以数组的形式存储到组件实例中
const hooks = target[type] || (target[type] = [])
// ...
if (prepend) {
  hooks.unshift(wrappedHook)
} else {
  hooks.push(wrappedHook)
}

用户注册的钩子函数会被包裹为一个函数(wrappedHook),然后缓存到 __weh 的内部属性中。__weh 代表了 "with error handling" ,即带有错误处理的意思。用户注册的钩子函数会传入 callWithAsyncErrorHandling 函数执行。callWithAsyncErrorHandling 函数是 Vue3 内部公共的异步异常处理函数,传入的函数在执行的时候如果报错了,该错误会被捕获。

const wrappedHook =
  hook.__weh ||
  (hook.__weh = (...args: unknown[]) => {
    if (target.isUnmounted) {
      // 如果组件卸载了,则不能执行该组件的生命周期函数
      return
    }
    // disable tracking inside all lifecycle hooks
    // since they can potentially be called inside effects.
    pauseTracking()
    // Set currentInstance during hook invocation.
    // This assumes the hook does not synchronously trigger other hooks, which
    // can only be false when the user does something really funky.
    setCurrentInstance(target)
    const res = callWithAsyncErrorHandling(hook, target, type, args)
    unsetCurrentInstance()
    resetTracking()
    return res
  })

在执行用户注册的生命周期钩子函数前,会先停止依赖收集(pauseTracking),因为钩子函数内部访问的响应式对象,在组件内部副作用函数中都已经执行过依赖收集,所以钩子函数执行的时候没有必要再次收集依赖,毕竟依赖收集的这个过程也有一定的性能消耗。

接下来是调用 setCurrentInstance 函数来设置当前组件实例,这个是为了确保组件生命周期钩子函数执行时,对应的组件实例是正确的。即,A 组件上的生命周期钩子函数执行的时候,当前的组件实例是 A 组件,B 组件上的生命周期钩子函数执行的时候,当前的组件实例是 B 组件。保证生命周期钩子函数执行时的组件,与该生命周期钩子函数注册时的组件是一致的。

在 Vue 内部,会一直维护一个全局变量 currentInstance ,存储了组件运行时当前组件的实例。在注册生命周期钩子函数时(即执行 injectHook 函数时),会通过 currentInstance 这个全局变量,拿到当前运行组件的实例,并存储到 target 变量中,然后在执行生命周期钩子函数时,为了保证此时的 currentInstance 和注册生命周期钩子函数时的一致,会再次调用 setCurrentInstance 函数,设置 target 为当前组件实例。

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

export let currentInstance: ComponentInternalInstance | null = null

export const setCurrentInstance = (instance: ComponentInternalInstance) => {
  currentInstance = instance
  instance.scope.on()
}

当生命周期钩子函数执行完毕后,会将当前运行组件实例设置为 null (unsetCurrentInstance),并恢复依赖收集(resetTracking)。

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

export const unsetCurrentInstance = () => {
  currentInstance && currentInstance.scope.off()
  currentInstance = null
}

当执行生命周期钩子函数时,不存在组件实例的话,这个是错误的情况,则要输出警告信息。

else if (__DEV__) {
  const apiName = toHandlerKey(ErrorTypeStrings[type].replace(/ hook$/, ''))

  // 输出警告信息
  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().` +
      (__FEATURE_SUSPENSE__
        ? ` If you are using async setup(), make sure to register lifecycle ` +
          `hooks before the first await statement.`
        : ``)
  )
}

beforeMount 生命周期选项是基于 onBeforeMount 生命周期钩子实现的,而 onBeforeMount 等各个生命周期钩子函数本质上是调用 injectHook 函数注册的,然后用户注册的生命周期函数会被包裹为到一个函数(wrappedHook)中执行,在该包裹函数(wrappedHook)内部执行生命周期钩子函数前,会先暂停依赖收集,因为生命周期钩子函数内部访问的响应式对象,在组件内部副作用函数中都已经执行过依赖收集,所以钩子函数执行的时候没有必要再次收集依赖,毕竟依赖收集的这个过程也有一定的性能消耗,然后调用设置组件实例的函数(setCurrentInstance),这是为了保证生命周期钩子函数执行时的组件实例与注册该生命周期钩子函数时的组件实例一致。

beforeMount 生命周期函数会在组件被挂载之前被调用

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

const setupRenderEffect: SetupRenderEffectFn = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
) => {
  const componentUpdateFn = () => {
    if (!instance.isMounted) {
      const { bm } = instance

      if (bm) {
        // 执行 beforeMount 钩子函数
        invokeArrayFns(bm)
      }

      if (el && hydrateNode) {
        // ...
      } else {
        // 生成组件渲染的子树(虚拟DOM)
        const subTree = (instance.subTree = renderComponentRoot(instance))

        // 完成组件挂载(将 subTree 挂载到 container 中)
        patch(
          null,
          subTree,
          container,
          anchor,
          instance,
          parentSuspense,
          isSVG
        )
      }
    }
  }
}

在执行挂载组件之前,会检测组件实例上是否有注册的 beforeMount 钩子函数(bm),如果有,则会遍历执行它。因为用户可以通过多次执行 onBeforeMount 函数注册多个 beforeMount 钩子函数,并且在 mixin 中也可以定义额外的 beforeMount 钩子函数,所以 Vue 内部会用数组来保存生命周期钩子函数。到特定的时机,则会通过遍历的方式来依次执行特定的生命周期钩子函数。

// packages/shared/src/index.ts

export const invokeArrayFns = (fns: Function[], arg?: any) => {
  for (let i = 0; i < fns.length; i++) {
    fns[i](arg)
  }
}

onMounted 生命周期钩子与 mounted 生命周期选项的实现

mounted 生命周期选项是基于组合式 API 中的 onMounted 生命周期钩子实现的。这一点也印证了 Vue 官网中的那句话:选项式 API 是在组合式 API 的基础上实现的

pic29.png

👆 链接为:cn.vuejs.org/guide/intro…

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

// 注册 mounted 生命周期钩子的函数
export const onMounted = createHook(LifecycleHooks.MOUNTED)
// packages/runtime-core/src/componentOptions.ts

import {
  onMounted
} from './apiLifecycle'

export function applyOptions(instance: ComponentInternalInstance) {
  const options = resolveMergedOptions(instance)

  const {
    mounted
  } = options

  function registerLifecycleHook(
    register: Function,
    hook?: Function | Function[]
  ) {
    if (isArray(hook)) {
      hook.forEach(_hook => register(_hook.bind(publicThis)))
    } else if (hook) {
      register((hook as Function).bind(publicThis))
    }
  }
  // 通过调用 onMounted 生命周期钩子注册 mounted 生命周期函数
  registerLifecycleHook(onMounted, mounted)
}

onMounted() 生命周期钩子和 onBeforeMount() 生命周期钩子的实现原理是一样的(都是调用 injectHook 函数实现),只是执行时机不同,因此关于 onMounted() 生命周期钩子函数是如何注册以及如何被包裹为一个函数执行的在这里就不再赘述了。

mounted 生命周期函数会在组件挂载完成后执行,可以在这个生命周期中操作组件的 DOM

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

const setupRenderEffect: SetupRenderEffectFn = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
) => {
  const componentUpdateFn = () => {
    if (!instance.isMounted) {
      const { m } = instance
      
      if (el && hydrateNode) {
        // ...
      } else {
        // 生成组件渲染的子树(虚拟DOM)
        const subTree = (instance.subTree = renderComponentRoot(instance))

        // 完成组件挂载(将 subTree 挂载到 container 中)
        patch(
          null,
          subTree,
          container,
          anchor,
          instance,
          parentSuspense,
          isSVG
        )
      }

      if (m) {
        // 将 mounted 生命周期函数推入 Post 任务队列
        queuePostRenderEffect(m, parentSuspense)
      }
    }
  }
}

在执行完组件的挂载后,会检查组件实例上是否注册有 mounted 生命周期钩子函数 (m),如果有的话,则会将 mounted 生命周期钩子函数数组推入 Post 任务队列中,然后在整个应用 render 完毕后,遍历 Post 任务队列,依次执行 mounted 生命周期钩子函数。

在 Vue 中,凡是需要等 DOM 更新后再执行的任务,都会加入到 Post 队列中管理,因此 mounted 生命周期也会放入到 Post 队列中。因为 mounted 可以操作 DOM,因此该生命周期函数肯定需要在 DOM 更新完毕后才能执行。

有关 Post 任务队列更多内容可查看笔者写的另一篇文章:深入源码,理解 Vue3 的调度系统与 nextTick

有些使用 Vue 的开发者也许会纠结,在组件初始化阶段,对于发送一些 Ajax 异步请求的逻辑,是应该放在 created 生命周期钩子函数中,还是应该放在 mounted 生命周期钩子函数中?其实不用纠结,ceated 生命周期钩子函数和 mounted 生命周期钩子函数中的执行顺序虽然有先后,但是性能的差异并不大,它们都能拿到组件数据。所以,对于不需要操作 DOM 的相关初始化逻辑,放到 created 中或 mounted 中都可以,但是,如果相关初始化逻辑依赖了 DOM 操作,那只能将相关初始化逻辑放到 mounted 生命周期钩子函数中执行了。

onBeforeUpdate() 生命周期钩子与 beforeUpdate 生命周期选项的实现

beforeUpdate 生命周期选项是基于组合式 API 中的 onBeforeUpdate() 生命周期钩子实现的。

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

// 注册 beforeUpdate 生命周期钩子的函数
export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
// packages/runtime-core/src/componentOptions.ts

import { onBeforeUpdate } from './apiLifecycle'

export function applyOptions(instance: ComponentInternalInstance) {
  const options = resolveMergedOptions(instance)

  const {
    beforeUpdate
  } = options

  function registerLifecycleHook(
    register: Function,
    hook?: Function | Function[]
  ) {
    if (isArray(hook)) {
      hook.forEach(_hook => register(_hook.bind(publicThis)))
    } else if (hook) {
      register((hook as Function).bind(publicThis))
    }
  }

  // 通过调用 onBeforeUpdate 生命周期钩子注册 beforeUpdate 生命周期函数
  registerLifecycleHook(onBeforeUpdate, beforeUpdate)
}

onBeforeUpdate() 生命周期钩子和其它生命周期钩子的实现原理是一样的(都是调用 injectHook 函数实现),只是执行时机不同,因此关于 onBeforeUpdate() 生命周期钩子函数是如何注册以及如何被包裹为一个函数执行的在这里就不再赘述了。

beforeUpdate 生命周期函数在组件即将因为一个响应式状态变更而更新其 DOM 树之前调用。这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态。在这个钩子中更改状态也是安全的。

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

const setupRenderEffect: SetupRenderEffectFn = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
) => {
  const componentUpdateFn = () => {
    if (!instance.isMounted) {
      // 挂载组件
    } else {
      // 更新组件

      // 获取组件实例上通过 onBeforeUpdate 注册的生命周期函数(即,bu)
      let { next, bu, vnode } = instance

      // next 表示新的组件虚拟 DOM 树
      if (next) {
        // 更新组件虚拟 DOM 树、更新 Props 与插槽数据等
        updateComponentPreRender(instance, next, optimized)
      } else {
        next = vnode
      }

      // 执行 beforeUpdate 钩子函数
      if (bu) {
        invokeArrayFns(bu)
      }
      // 渲染新的虚拟 DOM 树
      const nextTree = renderComponentRoot(instance)
      // 缓存旧的虚拟 DOM 树
      const prevTree = instance.subTree

      // 组件更新的核心逻辑,根据新旧虚拟 DOM 做 patch
      patch(prevTree, nextTree)
    }
  }
}

在执行 patch 函数更新组件之前,会检测组件实例上是否有注册的 beforeUpdate 生命周期钩子函数(bu),如果有,则会遍历 beforeUpdate 生命周期钩子函数的数组,依次执行它。和其它生命周期一样,用户也可以在 setup 函数中多次调用 onBeforeUpdate() 生命周期钩子注册多个 beforeUpdate 生命周期函数,同时 mixin 中也可以定义 beforeUpdate 生命周期函数,因此 beforeUpdate 生命周期函数也是用数组保存的。

// packages/shared/src/index.ts

export const invokeArrayFns = (fns: Function[], arg?: any) => {
  for (let i = 0; i < fns.length; i++) {
    fns[i](arg)
  }
}

onUpdated() 生命周期钩子与 updated 生命周期选项的实现

updated 生命周期选项是基于组合式 API 中的 onUpdated() 生命周期钩子实现的。

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

export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
// packages/runtime-core/src/componentOptions.ts

import { onUpdated } from './apiLifecycle'

export function applyOptions(instance: ComponentInternalInstance) {
  const options = resolveMergedOptions(instance)

  const {
    updated
  } = options

  function registerLifecycleHook(
    register: Function,
    hook?: Function | Function[]
  ) {
    if (isArray(hook)) {
      hook.forEach(_hook => register(_hook.bind(publicThis)))
    } else if (hook) {
      register((hook as Function).bind(publicThis))
    }
  }
  // 通过调用 onUpdated 生命周期钩子注册 updated 生命周期函数
  registerLifecycleHook(onUpdated, updated)
}

onUpdated() 生命周期钩子和其它生命周期钩子的实现原理是一样的(都是调用 injectHook 函数实现),只是执行时机不同,因此关于 onUpdated() 生命周期钩子函数是如何注册以及如何被包裹为一个函数执行的在这里就不再赘述了。

updated 生命周期函数在组件因为一个响应式状态变更而更新其 DOM 树之后调用

因为任何数据的变化导致组件的更新都会执行 updated 生命周期函数,所以,如果要监听数据的改变并执行某些逻辑,最好不要使用 updated 生命周期函数而是用计算属性或监听器代替。另外要注意,不要在 updated 钩子中更改组件的状态,因为这样会再次触发组件更新,导致无限递归更新 。

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

const setupRenderEffect: SetupRenderEffectFn = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
) => {
  const componentUpdateFn = () => {
    if (!instance.isMounted) {
      // 挂载组件
    } else {
      // 更新组件

      // 获取组件实例上通过 onUpdated 注册的生命周期函数(即,u)
      let { u } = instance

      // 渲染新的虚拟 DOM 树
      const nextTree = renderComponentRoot(instance)
      // 缓存旧的虚拟 DOM 树
      const prevTree = instance.subTree

      // 组件更新的核心逻辑,根据新旧虚拟 DOM 做 patch
      patch(prevTree, nextTree)

      // 将 updated 生命周期函数推入 Post 任务队列
      if (u) {
        queuePostRenderEffect(u, parentSuspense)
      }
    }
  }
}

在执行完组件更新之后(即执行完 patch 函数后),会检查组件实例上是否有注册的 updated 生命周期函数(即 u),如果有,则将 updated 生命周期函数推入 Post 任务队列中(调用 queuePostRenderEffect 函数将 updated 生命周期函数推入 Post 任务队列中),当组件更新完毕后,则会遍历 Post 任务队列,依次执行 updated 生命周期函数。

在 Vue 中,凡是需要等 DOM 更新后再执行的任务,都会加入到 Post 任务队列中管理,所以和 mounted 生命周期函数类似,updated 生命周期函数也会推入 Post 任务队列中管理。

onBeforeUnmount() 生命周期钩子与 beforeUnmount 生命周期选项的实现

beforeUnmount 生命周期选项是基于组合式 API 中的 onBeforeUnmount() 生命周期钩子实现的。

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

export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT)
// packages/runtime-core/src/componentOptions.ts

import { onBeforeUnmount } from './apiLifecycle'

export function applyOptions(instance: ComponentInternalInstance) {
  const options = resolveMergedOptions(instance)

  const {
    beforeUnmount
  } = options

  function registerLifecycleHook(
    register: Function,
    hook?: Function | Function[]
  ) {
    if (isArray(hook)) {
      hook.forEach(_hook => register(_hook.bind(publicThis)))
    } else if (hook) {
      register((hook as Function).bind(publicThis))
    }
  }

  // 通过调用 onBeforeUnmount 生命周期钩子注册 beforeUnmount 生命周期函数
  registerLifecycleHook(onBeforeUnmount, beforeUnmount)
}

onBeforeUnmount() 生命周期钩子和其它生命周期钩子的实现原理是一样的(都是调用 injectHook 函数实现),只是执行时机不同,因此关于 onBeforeUnmount() 生命周期钩子函数是如何注册以及如何被包裹为一个函数执行的在这里就不再赘述了。

beforeUnmount 生命周期函数会在一个组件实例被卸载之前调用。可以在这个生命周期中移除定时器、DOM 事件监听等,否则会造成内存泄漏。

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

const unmountComponent = (
  instance: ComponentInternalInstance,
  parentSuspense: SuspenseBoundary | null,
  doRemove?: boolean
) => {

  // 获取组件实例上通过 onBeforeUnmount() 注册的生命周期函数(即,bum)
  const { bum, subTree } = instance

  // 执行 beforeUnmount 生命周期函数
  if (bum) {
    invokeArrayFns(bum)
  }
  // 调用 unmount 卸载组件的虚拟 DOM 树,即组件的子树 subTree
  unmount(subTree, instance, parentSuspense, doRemove) 
}

unmount 主要就是遍历组件的虚拟 DOM(即组件的子树subTree),它会通过递归的方式来销毁子节点,遇到组件节点时执行 unmountComponent,遇到普通节点时则删除 DOM 元素。组件的销毁过程和渲染过程类似,都是递归的过程。

在组件卸载前,会检测组件实例上是有否有注册的 beforeUnmount 生命周期钩子函数 (bum),如果有,则会遍历 beforeUnmount 生命周期钩子函数的数组,依次执行它(通过 invokeArrayFns 函数执行)。

onUnmounted() 生命周期钩子与 unmounted 生命周期选项的实现

unmounted 生命周期选项是基于组合式 API 中的 onUnmounted() 生命周期钩子实现的。

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

export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED)
// packages/runtime-core/src/componentOptions.ts

import { onUnmounted } from './apiLifecycle'

export function applyOptions(instance: ComponentInternalInstance) {
  const options = resolveMergedOptions(instance)

  const {
    unmounted
  } = options

  function registerLifecycleHook(
    register: Function,
    hook?: Function | Function[]
  ) {
    if (isArray(hook)) {
      hook.forEach(_hook => register(_hook.bind(publicThis)))
    } else if (hook) {
      register((hook as Function).bind(publicThis))
    }
  }

  // 通过调用 onUnmounted 生命周期钩子注册 unmounted 生命周期函数
  registerLifecycleHook(onUnmounted, unmounted)
}

onUnmounted() 生命周期钩子和其它生命周期钩子的实现原理是一样的(都是调用 injectHook 函数实现),只是执行时机不同,因此关于 onUnmounted() 生命周期钩子函数是如何注册以及如何被包裹为一个函数执行的在这里就不再赘述了。

unmounted 生命周期函数会在一个组件实例被卸载之后调用。和 beforeUnmount 生命周期一样,也可以在 unmounted 生命周期函数中手动清理一些副作用,例如计时器、DOM 事件监听器等,避免浏览器内存泄漏。

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

const unmountComponent = (
  instance: ComponentInternalInstance,
  parentSuspense: SuspenseBoundary | null,
  doRemove?: boolean
) => {

  // 获取组件实例上通过 onUnmounted() 注册的生命周期函数(即,um)
  const { subTree, um } = instance

  // 调用 unmount 卸载组件的虚拟 DOM 树,即组件的子树 subTree
  unmount(subTree, instance, parentSuspense, doRemove)

  // 将 unmounted 生命周期函数推入 Post 任务队列
  if (um) {
    queuePostRenderEffect(um, parentSuspense)
  }
}

在组件卸载后,会检查组件实例上是否有注册的 unmounted 生命周期函数(即 um),如果有,则将 unmounted 生命周期函数推入 Post 任务队列中(调用 queuePostRenderEffect 函数将 unmounted 生命周期函数推入 Post 任务队列中),当组件卸载完毕后,则会遍历 Post 任务队列,依次执行 unmounted 生命周期函数。

在 Vue 中,凡是需要等 DOM 更新后再执行的任务,都会加入到 Post 任务队列中管理,所以和 mounted 生命周期函数类似,unmounted 生命周期函数也会推入 Post 任务队列中管理。

beforeDestroy 与 destroyed 生命周期函数的实现

beforeDestroy 与 destroyed 生命周期函数在 Vue3 中被启用了,分别由 beforeUnmount 和 unmounted 生命周期代替。

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

interface LegacyOptions<
  Props,
  D,
  C extends ComputedOptions,
  M extends MethodOptions,
  Mixin extends ComponentOptionsMixin,
  Extends extends ComponentOptionsMixin,
  I extends ComponentInjectOptions,
  II extends string
> {
  /** @deprecated use `beforeUnmount` instead */
  beforeDestroy?(): void
  /** @deprecated use `unmounted` instead */
  destroyed?(): void
}

在 Vue3 的兼容模式下的构建版本中(即 @vue/compat 包)是可以使用 beforeDestroy 和 destroyed 生命周期函数的。同时,beforeDestroy 和 destroyed 生命周期函数是基于 beforeUnmount 和 unmounted 生命周期函数实现的。

在 Vue3 内部,用户注册的 beforeDestroy 和 destroyed 生命周期选项会被当作是 beforeUnmount 和 unmounted。通过源码也可以知道,beforeDestroy 和 destroyed 生命周期选项分别是通过 onBeforeUnmount 和 onUnmounted 生命周期钩子注册的。

所以在 Vue3 兼容模式的构建版本中,beforeDestroy 和 destroyed 生命周期函数的执行时机与 beforeUnmount 和 unmounted 生命周期函数是一样的。它们只是名字不同

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

import {
  onBeforeUnmount,
  onUnmounted
} from './apiLifecycle'

export function applyOptions(instance: ComponentInternalInstance) {
  const options = resolveMergedOptions(instance)

  const {
    beforeDestroy,
    destroyed
  } = options


  // 兼容模式
  if (__COMPAT__) {    
    if (
      beforeDestroy &&
      softAssertCompatEnabled(DeprecationTypes.OPTIONS_BEFORE_DESTROY, instance)
    ) {
      // 通过调用 onBeforeUnmount 生命周期钩子注册 beforeDestroy 生命周期函数
      registerLifecycleHook(onBeforeUnmount, beforeDestroy)
    }
    if (
      destroyed &&
      softAssertCompatEnabled(DeprecationTypes.OPTIONS_DESTROYED, instance)
    ) {
      // 通过调用 onUnmounted 生命周期钩子注册 destroyed 生命周期函数
      registerLifecycleHook(onUnmounted, destroyed)
    }
  }
}

softAssertCompatEnabled 函数用于判断是否启用了相关的兼容配置。如上面的代码就是用于判断是否启用了 beforeDestroy 和 destroyed 的兼容配置,在 DEV 模式下,还会有兼容相关的警告信息。

onErrorCaptured() 生命周期钩子与 errorCaptured 生命周期选项的实现

errorCaptured 生命周期选项是基于组合式 API 中的 onErrorCaptured() 生命周期钩子实现的。

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

export function onErrorCaptured<TError = Error>(
  hook: ErrorCapturedHook<TError>,
  target: ComponentInternalInstance | null = currentInstance
) {
  injectHook(LifecycleHooks.ERROR_CAPTURED, hook, target)
}
// packages/runtime-core/src/componentOptions.ts

import { onErrorCaptured } from './apiLifecycle'

export function applyOptions(instance: ComponentInternalInstance) {
  const options = resolveMergedOptions(instance)

  const {
    errorCaptured
  } = options

  function registerLifecycleHook(
    register: Function,
    hook?: Function | Function[]
  ) {
    if (isArray(hook)) {
      hook.forEach(_hook => register(_hook.bind(publicThis)))
    } else if (hook) {
      register((hook as Function).bind(publicThis))
    }
  }

  // 通过调用 onErrorCaptured 生命周期钩子注册 errorCaptured 生命周期函数
  registerLifecycleHook(onErrorCaptured, errorCaptured)
}

onErrorCaptured() 生命周期钩子和其它生命周期钩子的实现原理是一样的(都是调用 injectHook 函数实现),只是执行时机不同,因此关于 onErrorCaptured() 生命周期钩子函数是如何注册以及如何被包裹为一个函数执行的在这里就不再赘述了。

errorCaptured 生命周期函数会在捕获了后代组件传递的错误时调用。errorCaptured 在平时工作中可能用的不多,但它的确是一个很实用的功能,比如我们可以在根组件注册一个 errorCaptured 钩子函数,去捕获所有子孙组件的错误,并且可以根据错误的类型和信息统计和上报错误。

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

export function handleError(
  err: unknown,
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  throwInDev = true
) {
  const contextVNode = instance ? instance.vnode : null
  if (instance) {
    // 1. 获取父组件实例
    let cur = instance.parent
    // 2. 获取错误类型
    const errorInfo = __DEV__ ? ErrorTypeStrings[type] : type
    // 3. 开启 while 循环,不断向上遍历,取得父组件实例
    while (cur) {
      // 4. 从父组件实例中获取 errorCaptured 生命周期钩子函数
      const errorCapturedHooks = cur.ec
      if (errorCapturedHooks) {
        // 5. 遍历 errorCaptured 生命周期钩子函数数组
        for (let i = 0; i < errorCapturedHooks.length; i++) {
          if (
            // 6. 执行 errorCaptured 生命周期钩子函数,
            // errorCaptured 返回 false 则退出 while 循环,
            // 错误会停止向上传递
            errorCapturedHooks[i](err, exposedInstance, errorInfo) === false
          ) {
            return
          }
        }
      }
      cur = cur.parent
    }
    
    const appErrorHandler = instance.appContext.config.errorHandler
    // 7. 如果用户注册了全局错误处理函数 errorHandler ,则将错误信息传给 errorHandler 函数
    if (appErrorHandler) {
      callWithErrorHandling(
        appErrorHandler,
        null,
        ErrorCodes.APP_ERROR_HANDLER,
        [err, exposedInstance, errorInfo]
      )
      return
    }
  }
  // 8. errorCaptured 生命周期函数和全局错误处理函数 errorHandler 都没有或者注册有 errorCaptured 生命周期函数,
  // 但 errorCaptured 生命周期函数没有返回 false
  // 则往控制台输出错误信息
  logError(err, type, contextVNode, throwInDev)
}

Vue3 内部执行代码发生了错误都会流入 handleError 函数,因为 errorCaptured 生命周期函数在捕获了后代组件传递的错误时调用。因此 errorCaptured 生命周期函数也会在 handleError 函数内被调用。

handleError 的实现其实很简单,它会从当前报错的组件的父组件实例开始,通过 while 循环,不断向上遍历,取得父组件实例,然后在取得的父组件实例中尝试查找是否注册了 errorCaptured 生命周期函数。如果有则遍历执行 errorCaptured 生命周期函数并且判断 errorCaptured 生命周期函数的返回值是否为 false,如果为 false 则退出 while 循环,错误会停止向上传递。

用户还可以注册全局的错误处理函数 errorHandler ,如果用户注册了全局的错误处理函数 errorHandler ,则不会往控制台输出错误信息,错误会统一由 errorHandler 函数处理。如果用户没有注册 errorHandler 函数且用户注册的 errorCaptured 生命周期函数没有返回 false ,则会往控制台输出错误信息。

如果想了解更多关于 Vue3 是如何做错误处理的源码,可见笔者写的另一篇文章:深入源码,剖析 Vue3 是如何做错误处理的
全局的错误处理函数 errorHandler 的内容可见:应用实例 API | Vue.js

onRenderTracked() 生命周期钩子与 renderTracked 生命周期选项的实现

renderTracked 生命周期选项是基于组合式 API 中的 onRenderTracked() 生命周期钩子实现的。

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

export const onRenderTracked = createHook<DebuggerHook>(
  LifecycleHooks.RENDER_TRACKED
)
export function applyOptions(instance: ComponentInternalInstance) {
  const options = resolveMergedOptions(instance)

  const {
    renderTracked
  } = options

  function registerLifecycleHook(
    register: Function,
    hook?: Function | Function[]
  ) {
    if (isArray(hook)) {
      hook.forEach(_hook => register(_hook.bind(publicThis)))
    } else if (hook) {
      register((hook as Function).bind(publicThis))
    }
  }
  // 通过调用 onRenderTracked 生命周期钩子注册 renderTracked 生命周期函数
  registerLifecycleHook(onRenderTracked, renderTracked)
}

onRenderTracked() 生命周期钩子和其它生命周期钩子的实现原理是一样的(都是调用 injectHook 函数实现),只是执行时机不同,因此关于 onRenderTracked() 生命周期钩子函数是如何注册以及如何被包裹为一个函数执行的在这里就不再赘述了。

renderTracked 生命周期是 Vue3 新增的生命周期,仅在开发模式下可用,目的是用于组件调试,该生命周期函数会在一个响应式依赖被组件的渲染作用追踪后调用

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

const setupRenderEffect: SetupRenderEffectFn = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
) => {
  const componentUpdateFn = () => {
    // 定义组件更新时需要执行的逻辑
  }

  // 创建用于渲染的响应式副作用对象
  const effect = (instance.effect = new ReactiveEffect(
    componentUpdateFn,
    () => queueJob(update),
    instance.scope // track it in component's effect scope
  ))

  if (__DEV__) {
    // DEV 模式下,如果注册了 renderTracked 生命周期函数,
    // 则封装执行 renderTracked 生命周期的函数存储到副作
    // 用对象的 onTrack 属性
    effect.onTrack = instance.rtc
      ? e => invokeArrayFns(instance.rtc!, e)
      : void 0
  }
}

用于执行 renderTracked 生命周期函数的函数会存储到响应式副作用对象的 onTrack 属性。onTrack 会在一个响应式数据执行完依赖收集后执行,也就是遍历执行用户注册的 renderTracked 函数。

一个响应式数据执行完依赖收集,也即一个响应式依赖被组件的渲染作用完成了追踪。我们可以使用 renderTracked 生命周期函数来追踪组件渲染的依赖来源。

// packages/reactivity/src/effect.ts

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {

  if (shouldTrack) {
    // 执行完依赖收集
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
    if (__DEV__ && activeEffect!.onTrack) {
      // 判断是否有 onTrack 函数,如果有则执行它,
      // 即遍历执行用户注册的 renderTracked 生命周期函数
      activeEffect!.onTrack({
        effect: activeEffect!,
        ...debuggerEventExtraInfo!
      })
    }
  }
}

dep 是存放响应式数据依赖的集合(Set 数据结构)

onRenderTriggered() 生命周期钩子与 renderTriggered 生命周期选项的实现

renderTriggered 生命周期选项是基于组合式 API 中的 onRenderTriggered() 生命周期钩子实现的。

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

export const onRenderTriggered = createHook<DebuggerHook>(
  LifecycleHooks.RENDER_TRIGGERED
)
// packages/runtime-core/src/componentOptions.ts

import { onRenderTriggered } from './apiLifecycle'

export function applyOptions(instance: ComponentInternalInstance) {
  const options = resolveMergedOptions(instance)

  const {
    renderTriggered
  } = options

  function registerLifecycleHook(
    register: Function,
    hook?: Function | Function[]
  ) {
    if (isArray(hook)) {
      hook.forEach(_hook => register(_hook.bind(publicThis)))
    } else if (hook) {
      register((hook as Function).bind(publicThis))
    }
  }
  // 通过调用 onRenderTriggered 生命周期钩子注册 renderTriggered 生命周期函数
  registerLifecycleHook(onRenderTriggered, renderTriggered)
}

onRenderTriggered() 生命周期钩子和其它生命周期钩子的实现原理是一样的(都是调用 injectHook 函数实现),只是执行时机不同,因此关于 onRenderTriggered() 生命周期钩子函数是如何注册以及如何被包裹为一个函数执行的在这里就不再赘述了。

renderTriggered 生命周期是 Vue3 新增的生命周期,仅在开发模式下可用,目的是用于组件调试,该生命周期函数会在一个响应式依赖被组件触发了重新渲染之后调用

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

const setupRenderEffect: SetupRenderEffectFn = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
) => {
  const componentUpdateFn = () => {
    // 定义组件更新时需要执行的逻辑
  }

  // 创建用于渲染的响应式副作用对象
  const effect = (instance.effect = new ReactiveEffect(
    componentUpdateFn,
    () => queueJob(update),
    instance.scope // track it in component's effect scope
  ))

  if (__DEV__) {
    // DEV 模拟下,如果注册了 renderTriggered 生命周期函数,
    // 则封装执行 renderTriggered 生命周期的函数存储到副作
    // 用对象的 onTrigger 属性
    effect.onTrigger = instance.rtg
      ? e => invokeArrayFns(instance.rtg!, e)
      : void 0
  }
}

用于执行 renderTriggered 生命周期函数的函数会存储到响应式副作用对象的 onTrigger 属性。onTrigger 会在响应式依赖的变更触发了组件渲染时调用,onTrigger 会遍历执行用户注册的 renderTriggered 生命周期函数。

// packages/reactivity/src/effect.ts

function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || effect.allowRecurse) {
    if (__DEV__ && effect.onTrigger) {
      // 判断是否有 onTrigger 函数,如果有则执行它,
      // 即遍历执行用户注册的 renderTriggered 生命周期函数
      effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
    }
    // 执行副作用渲染函数,更新组件
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
}

当响应式数据被修改后,会执行 triggerEffect 函数,在 DEV 模式下,会判断是否有定义 onTrigger 函数,如果有,则会执行 onTrigger 函数,即遍历执行用户注册的 renderTriggered 生命周期函数。然后会执行副作用渲染函数,更新组件。

可以通过 renderTriggered 生命周期函数确定哪个依赖正在触发更新。

onActivated() 生命周期钩子与 activated 生命周期选项的实现

onActivated() 生命周期钩子与 activated 生命周期选项与 KeepAlive 组件有关。

activated 生命周期选项是基于组合式 API 中的 onActivated() 生命周期钩子实现的。

// packages/runtime-core/src/components/KeepAlive.ts

export function onActivated(
  hook: Function,
  target?: ComponentInternalInstance | null
) {
  registerKeepAliveHook(hook, LifecycleHooks.ACTIVATED, target)
}
// packages/runtime-core/src/componentOptions.ts

import { onActivated } from './apiLifecycle'

export function applyOptions(instance: ComponentInternalInstance) {
  const options = resolveMergedOptions(instance)

  const {
    activated
  } = options

  function registerLifecycleHook(
    register: Function,
    hook?: Function | Function[]
  ) {
    if (isArray(hook)) {
      hook.forEach(_hook => register(_hook.bind(publicThis)))
    } else if (hook) {
      register((hook as Function).bind(publicThis))
    }
  }

  // 通过调用 onActivated 生命周期钩子注册 activated 生命周期函数
  registerLifecycleHook(onActivated, activated)
}

onActivated 生命周期钩子与其他生命周期钩子有点不太一样,虽然 onActivated 生命周期钩子也是通过 injectHook 函数来实现的,但是他比其他生命周期函数又多了一层封装,这层封装主要用于不断地向父组件查找,判断当前组件是否被移除页面的 DOM 树中。如果判断当前组件或者其任一父组件从页面的 DOM 树中移除了,则不会再执行 activated 生命周期函数了。

// packages/runtime-core/src/components/KeepAlive.ts

function registerKeepAliveHook(
  hook: Function & { __wdc?: Function },
  type: LifecycleHooks,
  target: ComponentInternalInstance | null = currentInstance
) {
  // cache the deactivate branch check wrapper for injected hooks so the same
  // hook can be properly deduped by the scheduler. "__wdc" stands for "with
  // deactivation check".
  const wrappedHook =
    hook.__wdc ||
    (hook.__wdc = () => {
      // 仅在组件未被从 DOM 中移除的状态才执行该 hook
      let current: ComponentInternalInstance | null = target
      while (current) {
        if (current.isDeactivated) {
          return
        }
        current = current.parent
      }
      return hook()
    })
  // 将该 hook 注册到当前组件实例(target)中
  injectHook(type, wrappedHook, target)
  // In addition to registering it on the target instance, we walk up the parent
  // chain and register it on all ancestor instances that are keep-alive roots.
  // This avoids the need to walk the entire component tree when invoking these
  // hooks, and more importantly, avoids the need to track child components in
  // arrays.
  if (target) {
    let current = target.parent
    while (current && current.parent) {
      if (isKeepAlive(current.parent.vnode)) {
        injectToKeepAliveRoot(wrappedHook, type, target, current)
      }
      current = current.parent
    }
  }
}

除了会将 activated 生命周期函数注册到目标组件实例上,还会向上遍历注册到最靠近 keepAlive 组件的子组件上(需要注意的是,在嵌套的 keepAlive 中,仅会将 activated 生命周期注册到最靠近子组件的 keepAlive 组件实例上),这样的好处是在执行 activated 生命周期函数的时候,可以避免遍历子组件数组,提高性能。

读者可能会有疑问,将同一个 activated 生命周期函数注册两次(一次是自身组件上,另一次是最靠近 keepAlive 组件的子组件上),不会导致同一个 activated 生命周期函数执行两次吗?不会的,因为 activated 生命周期函数根 mounted 生命周期函数一样,会放到 Post 任务队列中执行,而在执行 Post 任务队列前,会先使用 Set 数据结构去重,这样就避免了重复执行 activated 生命周期函数的问题。

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

export function flushPostFlushCbs(seen?: CountMap) {
  if (pendingPostFlushCbs.length) {
    // 对 Post 任务队列去重
    const deduped = [...new Set(pendingPostFlushCbs)]
    pendingPostFlushCbs.length = 0

    activePostFlushCbs = deduped

    for (
      postFlushIndex = 0;
      postFlushIndex < activePostFlushCbs.length;
      postFlushIndex++
    ) {
      // 遍历执行 Post 任务队列中的函数
      activePostFlushCbs[postFlushIndex]()
    }
    activePostFlushCbs = null
    postFlushIndex = 0
  }
}
<!-- 嵌套 KeepAlive 组件的情况 -->
<keep-alive>
  <keep-alive>
    <demo />
  </keep-alive>
</keep-alive>

最靠近 keepAlive 组件的子组件就是 keepAliveRoot

// packages/runtime-core/src/components/KeepAlive.ts

function injectToKeepAliveRoot(
  hook: Function & { __weh?: Function },
  type: LifecycleHooks,
  target: ComponentInternalInstance,
  keepAliveRoot: ComponentInternalInstance
) {
  // true 表示把 hook 放到 keepAliveRoot[type] 对应的生命周期函数数组的头部,
  // 即使用 unshift() 方法,具体可见前文 injectHook 函数的实现
  const injected = injectHook(type, hook, keepAliveRoot, true /* prepend */)
  // 卸载时移除对应注册的生命周期函数
  onUnmounted(() => {
    remove(keepAliveRoot[type]!, injected)
  }, target)
}

remove 函数的实现很简单,主要就是调用数组的 splice 方法移除元素

// packages/shared/src/index.ts

export const remove = <T>(arr: T[], el: T) => {
  const i = arr.indexOf(el)
  if (i > -1) {
    arr.splice(i, 1)
  }
}

activated 生命周期函数会在组件被插入到 DOM 中时调用。

// packages/runtime-core/src/components/KeepAlive.ts

const KeepAliveImpl: ComponentOptions = {
  name: `KeepAlive`,
  // 标识为 KeepAlive 组件
  __isKeepAlive: true,
  setup(props: KeepAliveProps, { slots }: SetupContext) {
    const instance = getCurrentInstance()!
    const sharedContext = instance.ctx as KeepAliveContext
    sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
      // 使用 move 函数将组件 DOM 挂载到 container 上
      move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
      queuePostRenderEffect(() => {
        instance.isDeactivated = false
        // 遍历执行 activated 生命周期函数
        if (instance.a) {
          invokeArrayFns(instance.a)
        }
        const vnodeHook = vnode.props && vnode.props.onVnodeMounted
        if (vnodeHook) {
          invokeVNodeHook(vnodeHook, instance.parent, vnode)
        }
      }, parentSuspense)
    }
  }
}
// packages/runtime-core/src/renderer.ts

const setupRenderEffect: SetupRenderEffectFn = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
) => {
  const componentUpdateFn = () => {
    if (!instance.isMounted) {

      // 判断组件是否应该保持 keepAlive 状态
      if (
        initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE ||
        (parent &&
          isAsyncWrapper(parent.vnode) &&
          parent.vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE)
      ) {
        // 检查组件实例是否有注册 activated 生命周期函数,
        // 如果有则将 activated 生命周期函数推入 Post 队列
        instance.a && queuePostRenderEffect(instance.a, parentSuspense)
      }
    }
  }
}

onDeactivated() 生命周期钩子与 deactivated 生命周期选项的实现

onDeactivated() 生命周期钩子与 deactivated 生命周期选项也与 KeepAlive 组件有关。

deactivated 生命周期选项是基于组合式 API 中的 onDeactivated() 生命周期钩子实现的。

// packages/runtime-core/src/components/KeepAlive.ts

export function onDeactivated(
  hook: Function,
  target?: ComponentInternalInstance | null
) {
  registerKeepAliveHook(hook, LifecycleHooks.DEACTIVATED, target)
}
// packages/runtime-core/src/componentOptions.ts

export function applyOptions(instance: ComponentInternalInstance) {
  const options = resolveMergedOptions(instance)

  const {
    deactivated
  } = options

  function registerLifecycleHook(
    register: Function,
    hook?: Function | Function[]
  ) {
    if (isArray(hook)) {
      hook.forEach(_hook => register(_hook.bind(publicThis)))
    } else if (hook) {
      register((hook as Function).bind(publicThis))
    }
  }

  // 通过调用 onDeactivated 生命周期钩子注册 deactivated 生命周期函数
  registerLifecycleHook(onDeactivated, deactivated)
}

onDeactivated() 生命周期钩子和 onActivated() 生命周期钩子的实现原理是一样的(都是调用 registerKeepAliveHook 函数实现,本质是调用 injectHook),只是执行时机不同,因此关于 onDeactivated() 生命周期钩子函数是如何注册以及如何做两次封装的在这里就不再赘述了。

deactivated 生命周期函数会在组件从 DOM 中被移除时调用。

// packages/runtime-core/src/components/KeepAlive.ts

const KeepAliveImpl: ComponentOptions = {
  name: `KeepAlive`,
  // 标识为 KeepAlive 组件
  __isKeepAlive: true,
  setup(props: KeepAliveProps, { slots }: SetupContext) {
    const instance = getCurrentInstance()!
    const sharedContext = instance.ctx as KeepAliveContext
    sharedContext.deactivate = (vnode: VNode) => {
      const instance = vnode.component!
      // 使用 move 函数将组件 DOM 移除
      move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
      queuePostRenderEffect(() => {
        // 遍历执行 deactivated 生命周期函数
        if (instance.da) {
          invokeArrayFns(instance.da)
        }
        const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
        if (vnodeHook) {
          invokeVNodeHook(vnodeHook, instance.parent, vnode)
        }
        instance.isDeactivated = true
      }, parentSuspense)
    }
  }
}

注意,这里只是把 DOM 移除了,并不会走组件的卸载流程。

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

const unmount: UnmountFn = (
  vnode,
  parentComponent,
  parentSuspense,
  doRemove = false,
  optimized = false
) => {
  const {
    shapeFlag
  } = vnode
  // 判断组件是否应该保持 keepAlive 状态
  if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
    // 执行完 deactivate 函数后直接返回,不走组件完整的卸载流程
    ;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
    return
  }
}

onServerPrefetch() 生命周期钩子与 serverPrefetch 生命周期选项的实现

serverPrefetch 生命周期选项是基于组合式 API 中的 onServerPrefetch() 生命周期钩子实现的。

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

export const onServerPrefetch = createHook(LifecycleHooks.SERVER_PREFETCH)
// packages/runtime-core/src/componentOptions.ts

export function applyOptions(instance: ComponentInternalInstance) {
  const options = resolveMergedOptions(instance)

  const {
    serverPrefetch
  } = options

  function registerLifecycleHook(
    register: Function,
    hook?: Function | Function[]
  ) {
    if (isArray(hook)) {
      hook.forEach(_hook => register(_hook.bind(publicThis)))
    } else if (hook) {
      register((hook as Function).bind(publicThis))
    }
  }

  // 通过调用 onServerPrefetch 生命周期钩子注册 serverPrefetch 生命周期函数
  registerLifecycleHook(onServerPrefetch, serverPrefetch)
}

onServerPrefetch() 生命周期钩子和其它生命周期钩子的实现原理是一样的(都是调用 injectHook 函数实现),只是执行时机不同,因此关于 onServerPrefetch() 生命周期钩子函数是如何注册以及如何被包裹为一个函数执行的在这里就不再赘述了。

serverPrefetch 生命周期函数会在组件实例在服务器上被渲染之前调用。仅在服务端渲染中执行。可用于在服务器上请求后台接口数据,它比在客户端上请求后台数据更快。

如果 serverPrefetch 生命周期函数返回了一个 Promise ,服务端渲染会在渲染该组件前等待该 Promise 完成。

// packages/server-renderer/src/render.ts

export function renderComponentVNode(
  vnode: VNode,
  parentComponent: ComponentInternalInstance | null = null,
  slotScopeId?: string
): SSRBuffer | Promise<SSRBuffer> {
  const instance = createComponentInstance(vnode, parentComponent, null)
  const res = setupComponent(instance, true /* isSSR */)
  const hasAsyncSetup = isPromise(res)
  // 1. 从组件实例对象中取出注册的 serverPrefetch 生命周期函数
  const prefetches = instance.sp
  // 2. 如果存在注册的 serverPrefetch 生命周期函数
  if (hasAsyncSetup || prefetches) {
    let p: Promise<unknown> = hasAsyncSetup
      ? (res as Promise<void>)
      : Promise.resolve()
    if (prefetches) {
      p = p
        .then(() => // 3. 遍历执行注册的 serverPrefetch 生命周期函数
          Promise.all(prefetches.map(prefetch => prefetch.call(instance.proxy)))
        )
        // Note: error display is already done by the wrapped lifecycle hook function.
        .catch(() => {})
    }
    // 4. 如果 serverPrefetch 生命周期函数返回 Promise ,
    // 服务端渲染会在渲染该组件前等待该 Promise 完成。   
    return p.then(() => renderComponentSubTree(instance, slotScopeId))
  } else {
    return renderComponentSubTree(instance, slotScopeId)
  }
}

总结

Vue3 当前的生命周期有 14 个,如果算上被废弃的生命周期 beforeDestroydestroyed ,则有 16 个。其中 renderTrackedrenderTriggered 是 Vue3 新增用于调试的生命周期。其实各个生命周期的实现原理都是相同的,只是执行时机不一样。他们的实现原理都是把用户注册的生命周期存储到组件实例中,然后等到合适的时机,再从组件实例中取出生命周期来执行。

在组合式 API 中,beforeCreatecreated 生命周期函数已经被 setup 函数取代了。

  • beforeCreate 生命周期函数会在组件实例初始化完成并且 props 被解析后立即调用。

  • created 生命周期函数在组件实例处理完所有与状态相关的选项后调用。

  • beforeMount 生命周期函数在组件被挂载之前调用

  • mounted 生命周期函数在组件被挂载之后调用

  • beforeUpdate 生命周期函数在组件即将因为响应式状态变更而更新其 DOM 树之前调用。

  • updated 生命周期函数在组件因为响应式状态变更而更新其 DOM 树之后调用

  • beforeUnmount 生命周期函数在一个组件实例被卸载之前调用

  • unmounted 生命周期函数在一个组件实例被卸载之后调用

beforeUnmountunmounted 用于替代被废弃的 beforeDestroydestroyed 生命周期函数

  • errorCaptured 生命周期函数在捕获了后代组件传递的错误时调用

  • renderTracked 生命周期函数在组件渲染过程中追踪到响应式依赖时调用

  • renderTriggered 生命周期函数在响应式依赖的变更触发了组件渲染时调用

renderTrackedrenderTriggered 生命周期函数仅在 DEV 模式下可用

  • activated 生命周期函数与 KeepAlive 组件有关,当组件被插入到 DOM 中时调用

  • deactivated 生命周期函数也与 KeepAlive 组件有关,当组件从 DOM 中被移除时调用

  • serverPrefetch 生命周期函数在组件实例在服务器上被渲染之前调用,仅在服务器渲染期间调用。