《Vue3 从入门到大神50篇》Vue3 源码详解(二十):生命周期钩子源码解析 —— onMounted / onUpdated 如何实现?

本篇关键词:生命周期钩子、onMounted、onUpdated、注册机制、调用时机、钩子队列、组件实例、SSR 生命周期差异

工程价值 :这是 Vue3 组件系统的"时间轴"。为什么 onMounted 能拿到 DOM?为什么父子组件的 mounted 执行顺序相反?为什么 onUpdatednextTick 之后?这些问题的答案,都藏在生命周期钩子的注册、存储与调度机制中。读懂这一篇,你就能精确控制组件的执行时序。


一、开篇:生命周期不是"事件",而是"回调队列"

很多同学对生命周期的理解停留在:

"Vue 在合适的时间调用这些钩子。"

这是片面的。

在 Vue3 中,生命周期的本质是:

组件实例上的一组"有序回调函数队列"。

  • 注册:在 setup 阶段收集

  • 存储:挂载在组件实例上

  • 调度:在特定渲染阶段批量执行

👉 生命周期 = 注册 + 存储 + 调度


二、生命周期钩子的完整清单

Vue3 暴露的 Composition API 钩子:

复制代码
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onActivated,
  onDeactivated,
  onErrorCaptured,
  onRenderTracked,
  onRenderTriggered,
  onServerPrefetch
} from 'vue'

三、生命周期钩子的注册机制(核心)

1️⃣ 钩子函数的本质

复制代码
onMounted(() => {
  console.log('mounted')
})

👉 它只是一个普通的函数调用,不是语法糖。


2️⃣ 钩子注册的核心函数

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

复制代码
export function injectHook(
  type: LifecycleHooks,
  hook: Function,
  target: ComponentInternalInstance
) {
  if (target) {
    const hooks = target[type] || (target[type] = [])
    hooks.push(hook)
  }
}

关键点

  • 每个生命周期对应一个数组

  • 同一个钩子可以注册多次

  • 按注册顺序执行


3️⃣ onMounted 的实现

复制代码
export function onMounted(hook: Function) {
  injectHook(
    LifecycleHooks.MOUNTED,
    hook,
    getCurrentInstance()
  )
}

👉 onMounted 只是把回调函数塞进当前组件实例的 mounted 数组里。


四、组件实例上的生命周期存储结构

1️⃣ 组件实例中的钩子字段

复制代码
const instance: ComponentInternalInstance = {
  mounted: [],      // onMounted
  updated: [],      // onUpdated
  unmounted: [],    // onUnmounted
  beforeMount: [],  // onBeforeMount
  beforeUpdate: [], // onBeforeUpdate
  // ...
}

2️⃣ 钩子数组的执行顺序

复制代码
mounted.forEach(hook => hook())

先注册,先执行。


五、生命周期的调用时机(重点)

1️⃣ onBeforeMount:组件挂载前

调用位置:

复制代码
function mountComponent(instance) {
  callHook(instance, LifecycleHooks.BEFORE_MOUNT)
  instance.isMounted = true
}

执行时机:

  • 组件已初始化

  • DOM 尚未创建

  • render() 尚未执行

👉 此时不能访问 DOM。


2️⃣ onMounted:组件挂载完成

调用位置:

复制代码
function mountedComponent(instance) {
  instance.isMounted = true
  callHook(instance, LifecycleHooks.MOUNTED)
}

执行时机:

  • DOM 已创建

  • 子组件已挂载

  • 可安全访问 DOM

这是访问 DOM 的正确时机。


3️⃣ onBeforeUpdate:组件更新前

调用位置:

复制代码
function updateComponent(instance) {
  callHook(instance, LifecycleHooks.BEFORE_UPDATE)
  patch()
}

执行时机:

  • 响应式数据已变化

  • DOM 尚未更新

  • 可获取更新前的 DOM 状态


4️⃣ onUpdated:组件更新完成

调用位置:

复制代码
function updatedComponent(instance) {
  queuePostFlushCb(() => {
    callHook(instance, LifecycleHooks.UPDATED)
  })
}

关键点

  • nextTick 之后执行

  • DOM 已更新

  • 适合操作更新后的 DOM


5️⃣ onBeforeUnmount:组件卸载前

调用位置:

复制代码
function unmountComponent(instance) {
  callHook(instance, LifecycleHooks.BEFORE_UNMOUNT)
  unmount()
}

执行时机:

  • DOM 仍存在

  • 可清理副作用

  • 取消定时器、事件监听


6️⃣ onUnmounted:组件卸载完成

调用位置:

复制代码
function unmountedComponent(instance) {
  callHook(instance, LifecycleHooks.UNMOUNTED)
  instance.isUnmounted = true
}

执行时机:

  • DOM 已移除

  • 组件实例已销毁

  • 不能再访问 DOM


六、父子组件生命周期执行顺序(VIP 重点)

1️⃣ 挂载阶段

复制代码
父 setup
父 onBeforeMount
  ├─ 子 setup
  ├─ 子 onBeforeMount
  ├─ 子 onMounted
父 onMounted

规律

  • setup:父 → 子

  • onBeforeMount:父 → 子

  • onMounted:子 → 父

👉 原因:子组件先挂载完成,父组件才能确定整体挂载完成。


2️⃣ 更新阶段

复制代码
父 onBeforeUpdate
  ├─ 子 onBeforeUpdate
  ├─ 子 onUpdated
父 onUpdated

规律

  • onBeforeUpdate:父 → 子

  • onUpdated:子 → 父


3️⃣ 卸载阶段

复制代码
父 onBeforeUnmount
  ├─ 子 onBeforeUnmount
  ├─ 子 onUnmounted
父 onUnmounted

七、为什么 onMounted 能访问 DOM?

1️⃣ 渲染流程回顾

复制代码
setupRenderEffect()
  ↓
componentUpdateFn()
  ↓
renderComponentRoot() → 生成 VNode
  ↓
patch() → 创建 / 更新 DOM
  ↓
mountedComponent()
  ↓
callHook(MOUNTED)

👉 DOM 在 mounted 钩子调用前已经创建完成。


2️⃣ 与 Vue2 的差异

框架 mounted 时机
Vue2 DOM 已挂载
Vue3 DOM 已挂载(一致)

Vue3 保持了行为一致性。


八、onUpdated 与 nextTick 的关系

1️⃣ onUpdated 的执行时机

复制代码
queuePostFlushCb(() => {
  callHook(instance, LifecycleHooks.UPDATED)
})

👉 onUpdated 本身就是 nextTick 回调。


2️⃣ 为什么需要 onUpdated?

复制代码
onUpdated(() => {
  // DOM 已更新
  el.getBoundingClientRect()
})

适合需要依赖更新后 DOM 的场景。


九、KeepAlive 下的生命周期

1️⃣ activated / deactivated

复制代码
onActivated(() => {
  // 组件被激活
})

onDeactivated(() => {
  // 组件被缓存
})

执行时机:

  • mountedactivated

  • unmounteddeactivated


2️⃣ 与 mounted 的区别

钩子 触发次数
mounted 一次
activated 多次

十、SSR 中的生命周期差异

1️⃣ 服务端执行的生命周期

复制代码
setup
onBeforeMount
onServerPrefetch

不执行

  • onMounted

  • onUpdated

  • onUnmounted


2️⃣ onServerPrefetch 的作用

复制代码
onServerPrefetch(async () => {
  await fetchData()
})

用于服务端数据预取。


十一、调试实战:观察生命周期调用栈

1️⃣ 打这些断点

复制代码
packages/runtime-core/src/apiLifecycle.ts
├── injectHook()
├── callHook()

packages/runtime-core/src/renderer.ts
├── mountComponent()
├── updateComponent()
└── unmountComponent()

2️⃣ 测试代码

复制代码
<script setup>
onBeforeMount(() => console.log('beforeMount'))
onMounted(() => console.log('mounted'))
onBeforeUpdate(() => console.log('beforeUpdate'))
onUpdated(() => console.log('updated'))
</script>

观察:

  • 钩子注册时机

  • 钩子调用顺序

  • 父子组件调用差异


十二、工程启示

✅ 在 onMounted 中访问 DOM

复制代码
onMounted(() => {
  el.value.focus()
})

✅ 在 onBeforeUnmount 中清理副作用

复制代码
onBeforeUnmount(() => {
  clearInterval(timer)
  removeEventListener('resize', handler)
})

✅ 避免在 onUpdated 中修改响应式数据

复制代码
// ❌ 可能导致无限更新
onUpdated(() => {
  count.value++
})

✅ KeepAlive 组件使用 activated / deactivated

复制代码
onActivated(() => {
  startTimer()
})

onDeactivated(() => {
  stopTimer()
})

十三、面试追问

  1. setup 和 onMounted 哪个先执行?

    setup 先执行,它是初始化阶段;onMounted 是挂载完成阶段。

  2. 为什么 onMounted 能访问 DOM?

    因为 DOM 在 mounted 钩子调用前已经创建完成。

  3. 父子组件生命周期执行顺序为什么是反的?

    因为子组件先挂载完成,父组件才能确定整体挂载完成。

  4. onUpdated 和 watch 的区别?

    onUpdated 关注 DOM 更新;watch 关注数据变化。


十四、本篇总结

✅ 生命周期钩子是组件实例上的回调队列

✅ 注册阶段:setup 中收集钩子

✅ 存储阶段:挂载到组件实例数组

✅ 调度阶段:在特定渲染节点批量执行

✅ 挂载顺序:setup → onBeforeMount → onMounted

✅ 父子顺序:setup 父→子,onMounted 子→父

✅ SSR 差异:服务端只执行部分钩子

✅ KeepAlive:activated / deactivated 替代 mounted / unmounted

一句话升华

Vue3 的生命周期系统,是一套**"基于组件实例的、可预测的、可扩展的回调调度机制"**。理解它,你就掌握了组件行为的"时间轴"。

相关推荐
全栈项目管理程序猿1 小时前
ArcGIS JS 基础教程(9):天空盒与大气效果
javascript
Narrastory1 小时前
我用 Claude Code 写代码不到 2 小时,却花了 3 天做完这个软件—Vibe Coding 的正确姿势,是设计不是生成
前端·人工智能·github
Enaium1 小时前
我实现了KMP的SDL绑定并编写了高性能软光追
前端·kotlin
古夕1 小时前
本地正常线上白屏:一次路由切换后刷新恢复问题的排查与修复
前端·ai编程
word1 小时前
从零接入 MCP:把任意工具变成 AI 的能力(协议级实践)
人工智能·前端框架
栀鸢ouo1 小时前
解决Element Plus表格展开行横向溢出、滚动截断问题(项目实战方案)
前端·vue.js
用户39051332192881 小时前
写了5年JS,才发现这10个方法能少写一半代码
前端
平凡的阿泽1 小时前
我用TRAE Work手搓了一个「谁是卧底」小游戏
前端·javascript
Highcharts1 小时前
Highcharts 实战:基于 Gauge 仪表盘打造实时动态时钟可视化开发全解析
javascript·数据可视化