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

相关推荐
涛涛ing2 小时前
OpenAI Astra 泄露:零样本生成 3D 网页,前端开发者慌了吗?
前端
ssshooter2 小时前
现在网页都能提供 MCP 了?!
前端·人工智能·程序员
杉氧2 小时前
状态管理变迁史:为什么我们放弃了 Redux 选择 Zustand?
android·前端·react native
cidy_982 小时前
OpenCode 手动配置火山方舟 Agent Plan 教程
前端
鹏多多2 小时前
PC 网站接入微信登录,这 10 个坑我替你踩完了!
前端·javascript·vue.js
律宏阔3 小时前
微信小程序使用 Orval + OpenAPI 自动生成接口:Axios 兼容踩坑记录
前端·微信小程序
律宏阔3 小时前
微信小程序集成 TDesign 完整记录:解决 NPM packages not found
前端·微信小程序
律宏阔3 小时前
微信小程序 ECharts 瘦身实战:分包异步化 + componentPlaceholder 避开主包 2MB 限制
前端·微信小程序
夏天要喝冰可乐3 小时前
从 Idea 到开源插件:我用 Vibe Coding 做了「文章摆渡」
前端·ai编程·vibecoding
小小小小宇3 小时前
pi agent
前端