技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTVUE

Vue 生命周期钩子函数实现原理

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/36-vuejs源码解剖 — Vue生命周期钩子函数实现原理
本文目录9 个章节
  1. 一、生命周期钩子到底做了什么?
  2. 二、Vue 2 的 beforeCreate:从 callHook 开始
  3. 三、Vue 2 的异常处理:invokeWithErrorHandling
  4. 四、Vue 3 的实现方式发生了变化
  5. 五、Vue 3 生命周期和 callWithAsyncErrorHandling
  6. 六、SSR 的差异
  7. 七、一句话总结
  8. 官方参考
  9. 原文出处

Vue 生命周期钩子函数实现原理

原文分析的是 Vue 2 的 callHookinvokeWithErrorHandling。这些内容仍适合学习 Vue 2 的源码,但不能直接当作 Vue 3 的实现。本文保留 Vue 2 源码主线,并补充 Vue 3.5 的生命周期注册与错误处理方式。

一、生命周期钩子到底做了什么?

无论是 Vue 2 还是 Vue 3,生命周期钩子都可以抽象为:

  1. 框架在组件实例上保存某类生命周期回调;
  2. 到达对应阶段时取出回调列表;
  3. 按顺序执行回调;
  4. 通过统一的错误处理函数处理同步异常和异步 Promise 异常;
  5. 在组件卸载、KeepAlive 激活/停用、SSR 等场景使用不同的调度时机。

Vue 2 会把同名的 mixinextends 和组件自身钩子合并为数组;Vue 3 则把 Composition API 注册的钩子放到组件内部对应的 hook 数组中。

二、Vue 2 的 beforeCreate:从 callHook 开始

Vue 2.7 的源码位置是:

在 Vue 2 中,调用生命周期钩子的入口可以简化为:

export function callHook(
  vm: Component,
  hook: string,
  ...args: any[]
) {
  // 暂时关闭依赖收集,避免钩子执行过程被错误地记录为当前 watcher 的依赖
  pushTarget()

  const handlers = vm.$options[hook]
  const info = `${hook} hook`

  if (handlers) {
    for (let i = 0; i < handlers.length; i++) {
      invokeWithErrorHandling(
        handlers[i],
        vm,
        args.length ? args : null,
        vm,
        info,
      )
    }
  }

  // 支持 vm.$on('hook:mounted', handler) 这类生命周期事件监听
  if (vm._hasHookEvent) {
    vm.$emit(`hook:${hook}`)
  }

  popTarget()
}

上面的代码是解释版,重点不在参数的逐字复制,而在执行顺序:

  • vm.$options 找到当前钩子;
  • 钩子通常已经在配置合并阶段变成数组;
  • 按数组顺序调用每个处理函数;
  • 执行前后通过 pushTarget() / popTarget() 暂停和恢复依赖收集;
  • 最后触发 hook:xxx 事件。

这里的 pushTarget() 不是为了“防止重复定义响应式数据”,而是为了避免执行生命周期函数时,当前正在运行的 watcher 错误收集到钩子内部读取的响应式依赖。它操作的是 Vue 2 响应式系统中的 Dep.target 栈。

beforeCreate 的调用位置

Vue 2 初始化实例时,大致顺序如下:

初始化 lifecycle / events / render
  -> beforeCreate
  -> 初始化 injections / props / methods / data / computed / watch
  -> created
  -> 判断是否有 el/template
  -> beforeMount
  -> mounted

因此在 Vue 2 的 beforeCreate 中,datapropscomputedwatch 还没有全部完成初始化。created 时响应式 data 和 methods 已经准备好,但 DOM 还没有挂载。

三、Vue 2 的异常处理:invokeWithErrorHandling

原文分析的函数可以整理为:

export function invokeWithErrorHandling(
  handler: Function,
  context: any,
  args: any[] | null,
  vm: any,
  info: string,
) {
  let res

  try {
    res = args
      ? handler.apply(context, args)
      : handler.call(context)

    if (
      res &&
      !res._isVue &&
      isPromise(res) &&
      !res._handled
    ) {
      res.catch(error => {
        handleError(error, vm, `${info} (Promise/async)`)
      })
      // Vue 2 内部标记,避免嵌套错误处理重复注册 catch
      res._handled = true
    }
  } catch (error) {
    handleError(error, vm, info)
  }

  return res
}

这里需要注意三点:

  1. 同步异常由 try/catch 交给 handleError
  2. 如果钩子返回 Promise,Vue 2 会额外监听 rejected Promise;
  3. 错误会继续经过组件的 errorCaptured 和应用级错误处理器。

这也是为什么下面这种异步生命周期代码不会因为没有手写 try/catch 就完全脱离 Vue 的错误处理:

export default {
  async created() {
    const data = await fetch('/api/user').then(response => response.json())
    this.user = data
  },
}

但这不代表可以忽略业务错误。请求取消、接口状态码和用户提示仍然应该按业务需要处理。

四、Vue 3 的实现方式发生了变化

Vue 3 不再使用 Vue 2 的 vm.$options[hook]pushTarget()vm.$emit('hook:xxx') 作为 Composition API 生命周期的主实现。相关源码位置是:

Composition API 的注册入口可以概括为:

export function injectHook(
  type: LifecycleHooks,
  hook: Function,
  target = currentInstance,
  prepend = false,
) {
  if (!target) {
    // 开发环境会提示:生命周期 hook 必须在 setup 中注册
    return
  }

  const hooks = target[type] || (target[type] = [])
  const wrappedHook = hook.__weh || (hook.__weh = (...args: unknown[]) => {
    pauseTracking()
    const reset = setCurrentInstance(target)

    const result = callWithAsyncErrorHandling(
      hook,
      target,
      type,
      args,
    )

    reset()
    resetTracking()
    return result
  })

  if (prepend) hooks.unshift(wrappedHook)
  else hooks.push(wrappedHook)

  return wrappedHook
}

这同样是保留结构的简化片段。Vue 3 的关键点是:

  • currentInstance 表示当前正在执行 setup() 的组件;
  • onMountedonUpdatedonUnmounted 等 API 本质上调用 injectHook
  • hook 会包装为带错误处理和当前实例上下文的函数;
  • pauseTracking() 防止执行 hook 时错误影响响应式依赖追踪;
  • Composition API 生命周期必须在 setup() 同步执行阶段注册,不能在 setTimeoutawait 之后才注册。

例如:

<script setup>
import { onMounted, onUnmounted } from 'vue'

function handleResize() {
  console.log(window.innerWidth)
}

onMounted(() => {
  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
})
</script>

如果生命周期钩子在异步回调之后才注册,当前组件实例可能已经不再是 currentInstance,Vue 无法把它可靠地关联到组件上。

五、Vue 3 生命周期和 callWithAsyncErrorHandling

Vue 3 的公共生命周期 API 可以理解为两部分:

const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
const onMounted = createHook(LifecycleHooks.MOUNTED)
const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
const onUpdated = createHook(LifecycleHooks.UPDATED)
const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT)
const onUnmounted = createHook(LifecycleHooks.UNMOUNTED)

组件渲染器在适当的阶段执行对应数组,并通过 callWithAsyncErrorHandling 处理回调:

onBeforeMount hooks
  -> 首次 render / patch
  -> queuePostRenderEffect(onMounted hooks)

onBeforeUpdate hooks
  -> render / patch
  -> queuePostRenderEffect(onUpdated hooks)

onBeforeUnmount hooks
  -> 卸载子树
  -> unmounted hooks

mountedupdatedunmounted 这类 hook 往往会作为 post-render effect 调度,具体执行时机还会受到组件更新队列、Suspense 和 Scheduler 的影响。不能把它们简单理解成普通函数调用后立刻同步执行。

Vue 3 没有 Vue 2 的 hook:mounted 主 API

Vue 2 可以使用:

vm.$on('hook:mounted', callback)

Vue 3 移除了组件实例上的 $on$off$once。跨组件通信应使用 props/emits、provide/inject 或明确的事件库;监听生命周期则使用 onMounted 等 Composition API,或者在 Options API 中直接声明 mounted()

六、SSR 的差异

服务器端没有真实 DOM,因此:

  • onMountedonUpdatedonUnmounted 不会在 SSR 期间执行;
  • 需要在服务端预取数据时,可使用 onServerPrefetch
  • DOM、windowdocument 操作应放到 onMounted 或客户端守卫中;
  • 组件 setup 中注册的普通生命周期仍然要遵循同步注册规则。
<script setup>
import { onMounted, onServerPrefetch, ref } from 'vue'

const user = ref(null)

async function loadUser() {
  user.value = await fetch('/api/user').then(response => response.json())
}

onServerPrefetch(loadUser)
onMounted(() => {
  // 只执行浏览器相关逻辑
  console.log(document.title)
})
</script>

七、一句话总结

  • Vue 2:生命周期回调保存在 vm.$options,通过 callHook 遍历执行,invokeWithErrorHandling 负责异常和 Promise 错误。
  • Vue 2 的 pushTarget() / popTarget() 是响应式依赖收集边界,不是响应式数据定义逻辑。
  • Vue 3:onMounted 等 API 在 setup 同步阶段通过 injectHook 注册,执行时使用当前实例和统一错误处理。
  • Vue 3 的 onMountedonUpdated 等钩子受到 Scheduler、post-render effect、Suspense 和 SSR 的影响。
  • Vue 2 的 $on('hook:xxx')vm.$optionscallHook 代码不能直接复制到 Vue 3。

官方参考

原文出处

作者:Fishing

原文链接:https://juejin.cn/post/7017001556243382308

来源:稀土掘金。著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS