Vue.js: 组件生命周期
最后更新:2026-10-20
组件生命周期是 Vue 组件从创建到销毁的完整过程:实例化 → 挂载到 DOM → 数据更新 → 卸载。Vue 提供了 8 个生命周期钩子让你在特定时刻执行代码。
掌握生命周期是写"会呼吸"的组件的关键——你可以在挂载时加载数据,在更新时做副作用,在卸载时清理资源。
1. 你将学到
- 组件从创建到销毁的完整流程图
- 8 个生命周期钩子(onBeforeMount / onMounted 等)
- Composition API 写法(onX 命名)
- 各钩子的最佳使用场景
- 父子组件生命周期执行顺序
- keep-alive 缓存组件(activated / deactivated)
- 错误处理 onErrorCaptured
2. 一个数据加载组件的"闪烁"问题
(1) 痛点:组件渲染了,但数据还没加载
Alice 构建了一个用户资料组件:
用户体验:
- 0ms:页面显示 "undefined undefined"(0.5 秒)
- 500ms:数据到达,显示 "Alice alice@example.com"
- 闪烁 0.5 秒,体验极差
产品经理 Charlie:
"Alice,用户看到 'undefined' 整整半秒。我们需要 loading 状态。数据到达之前先显示'Loading...'。"
(2) Vue 生命周期 + loading 状态解法
用户体验:
- 0ms:显示 "Loading..." ✅
- 500ms:数据到达,显示 "Alice alice@example.com" ✅
- 无闪烁 ✅
(3) 收益
加上生命周期后:
- 闪烁问题:100% → 0
- 用户体验:加载状态清晰
- 可扩展:同一模式可应用到所有数据组件
3. 完整生命周期流程图
graph TB
A[创建组件实例] --> B[setup()]
B --> C[setup 响应式]
C --> D[setup()]
D --> E[模板编译]
E --> F[onBeforeMount]
F --> G[挂载到 DOM]
G --> H[onMounted]
H --> I{数据更新?}
I -->|是| J[onBeforeUpdate]
J --> K[重新渲染]
K --> L[onUpdated]
L --> I
I -->|否| M{组件卸载?}
M -->|是| N[onBeforeUnmount]
N --> O[卸载]
O --> P[onUnmounted]
style H fill:#42b883,color:#fff
style L fill:#42b883,color:#fff
style P fill:#42b883,color:#fff
4. 8 个生命周期钩子详解
(1) 8 大钩子速查
| 钩子 | 触发时机 | 常用场景 | 频率 |
|---|---|---|---|
onBeforeMount |
组件挂载前 | 准备数据 | ⭐ |
onMounted |
组件挂载后 | 加载数据、DOM 操作 | ⭐⭐⭐⭐⭐ |
onBeforeUpdate |
数据更新前 | 性能优化 | ⭐⭐ |
onUpdated |
数据更新后 | DOM 重新渲染后操作 | ⭐⭐⭐ |
onBeforeUnmount |
组件卸载前 | 清理定时器 | ⭐⭐⭐ |
onUnmounted |
组件卸载后 | 最终清理 | ⭐⭐ |
onErrorCaptured |
捕获子组件错误 | 错误边界 | ⭐⭐⭐ |
onActivated |
keep-alive 激活 | 缓存组件恢复 | ⭐⭐ |
onDeactivated |
keep-alive 停用 | 缓存组件停用 | ⭐⭐ |
(2) onMounted:最常用
VUE
<script setup>
const { ref, onMounted } = Vue
const user = ref(null)
onMounted(async () => {
// 1. 加载初始数据
const res = await fetch('/api/user')
user.value = await res.json()
// 2. DOM 操作(操作真实 DOM)
document.title = `${user.value.name} - Dashboard`
// 3. 注册全局事件监听
const handleResize = () => { console.log('Window resized') }
window.addEventListener('resize', handleResize)
// 4. 设置定时器
const timer = setInterval(() => {
console.log('Tick')
}, 1000)
// ❌ 注意:onUnmounted 中要清理 timer 和 listener
})
</script>
(3) onUnmounted:清理资源
VUE
<script setup>
const { onMounted, onUnmounted } = Vue
let timer = null
onMounted(() => {
timer = setInterval(() => console.log('Tick'), 1000)
const handleResize = () => { console.log('Window resized') }
window.addEventListener('resize', handleResize)
})
// ✅ 清理:避免内存泄漏
onUnmounted(() => {
clearInterval(timer)
window.removeEventListener('resize', handleResize)
})
</script>
(4) onErrorCaptured:错误边界
VUE
<!-- App.vue 使用 -->
<ErrorBoundary>
<UserProfile :user-id="123" /> <!-- 如果出错,会被 ErrorBoundary 捕获 -->
</ErrorBoundary>
(5) 5 大使用场景
| 场景 | 钩子 | 代码 |
|---|---|---|
| 加载初始数据 | onMounted |
await fetch(...) |
| 设置定时器 | onMounted + onUnmounted |
setInterval + clearInterval |
| 监听全局事件 | onMounted + onUnmounted |
addEventListener + removeEventListener |
| DOM 操作 | onMounted |
document.querySelector(...) |
| 第三方库集成 | onMounted + onUnmounted |
new Chart(...) + chart.destroy() |
▶ 示例:8 个钩子使用速查
▶ 试一试输出:
TEXT
两个按钮「count++ (0)」和「卸载」,下方浅蓝区块显示「Props count: 0」,再下面是灰色小字「查看 console 输出」。
点 count++ → 按钮变「count++ (1)」、区块变「Props count: 1」,控制台依次打印「3. onBeforeUpdate」「4. onUpdated」。
点「卸载」→ 按钮文字变「挂载」、区块消失,控制台打印「5. onBeforeUnmount」「6. onUnmounted」。
▶ 示例:onErrorCaptured 错误边界
▶ 试一试输出:
TEXT
初始页面上只有一块空白区域(子组件还没出错时的内容)。
约 100ms 后子组件抛错,内容被替换成浅红提示条「⚠️ Error: Cannot set properties of null (setting 'foo')」,控制台同时打印「Caught: ...」和「Info: ...」。
因为回调 return false,错误不再继续向上冒泡。
5. 父子组件生命周期执行顺序
(1) 挂载顺序
sequenceDiagram
participant P as Parent
participant C as Child
P->>P: 1. Parent onBeforeMount
P->>P: 2. Parent onMounted
C->>C: 3. Child onBeforeMount
C->>C: 4. Child onMounted
Note over P,C: ❌ 这是错的:父 mounted 应该在子之后
正确顺序:
sequenceDiagram
participant P as Parent
participant C as Child
P->>P: 1. Parent onBeforeMount
C->>C: 2. Child onBeforeMount
C->>C: 3. Child onMounted
P->>P: 4. Parent onMounted
Note over P,C: ✅ 父组件等所有子组件挂载完才挂载
(2) 完整执行顺序(嵌套组件)
TEXT
1. Parent setup()
2. Parent setup
3. Parent setup()
4. Parent onBeforeMount
5. Child setup()
6. Child setup
7. Child setup()
8. Child onBeforeMount
9. Child onMounted
10. Parent onMounted
(3) 卸载顺序(反过来)
TEXT
1. Parent onBeforeUnmount
2. Child onBeforeUnmount
3. Child onUnmounted
4. Parent onUnmounted
▶ 示例:父子组件生命周期顺序(console 输出)
▶ 试一试输出:
TEXT
页面显示灰边容器里的「Parent 父组件」,下面套一个黄色块「Child 子组件」,顶部灰色小字提示「查看 console:先 Child 后 Parent」。
控制台依次打印「3. Child onMounted」→「4. Parent onMounted」,即父组件的 onMounted 晚于子组件。
6. keep-alive 缓存组件
(1) 什么是 keep-alive?
<keep-alive> 缓存组件实例,避免重复创建/销毁。常用在 Tab 切换、路由切换场景。
(2) 2 个新增钩子
VUE
<!-- 子组件:被 keep-alive 缓存 -->
<script setup>
const { onActivated, onDeactivated } = Vue
// 组件被激活时(从缓存中恢复)
onActivated(() => {
console.log('Component activated')
// 重新加载数据、恢复滚动位置等
})
// 组件被停用时(切走但保留在缓存)
onDeactivated(() => {
console.log('Component deactivated')
// 保存状态、暂停定时器等
})
</script>
(3) 5 大使用场景
| 场景 | 做法 |
|---|---|
| Tab 切换 | <keep-alive> 包裹 Tab 内容 |
| 路由切换 | <keep-alive> 包裹 <router-view> |
| 弹窗缓存 | <keep-alive> 包裹弹窗 |
| 列表页 | 缓存已加载的列表 |
| 表单页 | 缓存未提交的表单 |
▶ 示例:keep-alive 缓存组件
▶ 试一试输出:
TEXT
顶部三个 Tab 按钮:Home 为绿色(当前选中)、Profile 和 Settings 为灰色。
下方绿色面板:「Home」标题、按钮「内部计数: 0」、灰色提示「切换到其他 Tab 再切回,计数保留」。
点计数 → 1;切到 Profile(蓝底)再切回 Home,计数仍为 1,控制台在切换时打印 deactivated / activated 日志。
(1) 📊 5 个常见错误速查
| 错误 | 现象 | 解决 |
|---|---|---|
| 在 setup 顶层用 document | 服务端报错 | 用 onMounted |
| 没清理定时器 | 内存泄漏 | onUnmounted 中 clear |
| onMounted 中改 props | Vue 警告 | 改用 emit |
| 异步组件不处理错误 | 静默失败 | onErrorCaptured |
| 父子 mounted 顺序错乱 | 调试困惑 | 理解"父等子"原则 |
7. 完整示例:用户资料组件
▶ 示例:onMounted + onUnmounted(数据加载 + 清理)
▶ 试一试输出:
TEXT
顶部按钮「卸载组件」,下方用户卡片先显示「Loading...」。
约 500ms 后变为标题「Alice 1」和「alice1@example.com」,控制台打印「✅ Mounted: 数据已加载」,之后每 5 秒打印一条「⏰ Timer tick」。
点击按钮 → 卡片消失、按钮文字变「挂载组件」,控制台打印「🧹 Unmounted: 已清理定时器」;再点一次会重新走一遍加载流程。
❓ 常见问题
Q onMounted 和 setup() 区别?
A setup() 时组件已创建但 DOM 未生成(不能操作 DOM)。onMounted 时 DOM 已挂载(可操作 DOM)。加载数据一般用 onMounted(避免 SSR 问题)。
Q 生命周期钩子能异步吗?
A 能。
onMounted(async () => { await fetch(...) }) 是合法写法。Vue 不等异步完成才继续。Q keep-alive 的 onActivated 和 onMounted 区别?
A onMounted 只触发 1 次(首次挂载)。onActivated 每次从缓存恢复都触发。Tab 切换会触发 onActivated + onDeactivated(不是 onMounted + onUnmounted)。
Q onErrorCaptured 能捕获哪些错误?
A 能捕获 3 类:(1) 子组件渲染错误;(2) 子组件生命周期钩子错误;(3) 子组件 watch 回调错误。但不能捕获自身错误和异步错误。
Q 父组件能监听子组件的生命周期吗?
A 能。用 @hook 监听:
@hook:mounted="handleChildMounted"。但推荐用 props/emit 通信,不直接监听生命周期。Q Vue 2 的生命周期钩子和 Vue 3 区别?
A Vue 3 重命名为 onMounted(Vue 2 是 mounted),并新增 onErrorCaptured 和 onActivated/onDeactivated。Vue 2 钩子在 Vue 3 都可用(向后兼容)。
Q SSR(服务端渲染)时生命周期会怎样?
A onBeforeMount / onMounted / onBeforeUpdate / onUpdated 不会在服务端触发(无 DOM)。只有 setup() / setup() / onBeforeUnmount / onUnmounted / onErrorCaptured 触发。
📖 小节
- 8 个生命周期钩子:onBeforeMount / onMounted / onBeforeUpdate / onUpdated / onBeforeUnmount / onUnmounted / onErrorCaptured / onActivated
- onMounted 最常用:DOM 操作、加载数据、注册事件
- onUnmounted 必做清理:定时器、事件监听、第三方库
- 父子组件顺序:父 beforeMount → 子 beforeMount → 子 mounted → 父 mounted
- keep-alive + onActivated / onDeactivated 缓存组件
- onErrorCaptured 实现错误边界
- 服务端渲染时部分钩子不触发
📝 作业
-
基础题(难度⭐) 实现一个简单的 UserCard 组件:
- onMounted 加载用户数据(mock 1s 延迟)
- 显示 loading → 显示用户信息
- onUnmounted 打印 "Component destroyed"
-
进阶题(难度⭐⭐) 实现一个 Timer 组件:
- onMounted 启动定时器(每秒 +1)
- 显示当前秒数
- onUnmounted 清理定时器
- 加个按钮可以暂停/恢复(用 ref 存状态)
-
挑战题(难度⭐⭐⭐) 实现一个完整的 Tab 系统:
- TabContainer.vue:3 个 Tab(Home/Profile/Settings)
- 3 个 Tab 内容组件
- 用
<keep-alive>缓存组件 - 切换 Tab 时输出 "X activated, Y deactivated"
- 在其中一个 Tab 用 onMounted 加载数据
- 切走时不卸载(因为 keep-alive),切回时数据还在
- 用 onErrorCaptured 捕获子组件错误