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

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

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


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

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

"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(() => {
  // 组件被缓存
})

执行时机:

  • mounted → activated

  • unmounted → deactivated


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

相关推荐
jktantan2 小时前
Vue keep-alive 切标签后滚动位置回顶?因为 onDeactivated 里读到的 scrollTop 是 0
前端·javascript·vue.js
总要学点什么3 小时前
第89篇:三维特效-螺旋圆效果——让螺旋臂卷起一圈圈外扩的涟漪
开发语言·javascript·ecmascript·webgl·cesium·空间分析·1024程序员节
2601_962885723 小时前
quotes 字段全解:涨跌幅、振幅、换手率的口径与单位
java·前端·spring
excel4 小时前
encodeURI 和 encodeURIComponent 有什么区别?
前端
前端精髓4 小时前
调用openrouter的免费API模型
java·前端·javascript
赛博守夜人4 小时前
AI安全攻防与治理之十四:AI 安全测试不能脱离传统安全,Web、API、移动端与模型攻击面的组合拳
前端·人工智能·安全
开开心心_Every4 小时前
快速隐藏程序工具自定义快捷键一键切换
java·开发语言·前端·spring·智能手机·word·intellij-idea
networknomad4 小时前
OpenReplay:自己搭的会话回放工具,给前端排错用的
前端·其他
缓慢更新5 小时前
AI 辅助生成单测:从接口定义到用例的流水线
java·服务器·前端·网络·数据库·人工智能
陈随易6 小时前
AI 运维服务器,安全又可靠的方法
前端·后端·程序员