Vue 3 生命周期完全指南:从流程图到最佳实践 -- pd的前端笔记
文章目录
-
- [Vue 3 生命周期完全指南:从流程图到最佳实践 -- pd的前端笔记](#Vue 3 生命周期完全指南:从流程图到最佳实践 -- pd的前端笔记)
- 生命周期流程图深度解析
- [12 个生命周期钩子函数详解(组合式 API 版)](#12 个生命周期钩子函数详解(组合式 API 版))
-
- [✅ 核心三剑客(90% 场景用它们)](#✅ 核心三剑客(90% 场景用它们))
- [🟡 进阶钩子(特定场景使用)](#🟡 进阶钩子(特定场景使用))
- [🔴 调试与高级钩子(慎用!)](#🔴 调试与高级钩子(慎用!))
-
- onErrorCaptured
- [onRenderTracked 和 onRenderTriggered](#onRenderTracked 和 onRenderTriggered)
- [🟣 KeepAlive 专属钩子(缓存组件专用)](#🟣 KeepAlive 专属钩子(缓存组件专用))
-
- [onActivated 和 onDeactivated](#onActivated 和 onDeactivated)
- [SSR 专属钩子](#SSR 专属钩子)
- [常见误区 & 最佳实践](#常见误区 & 最佳实践)
-
- [❌ 误区 1:在 setup() 外异步调用钩子](#❌ 误区 1:在 setup() 外异步调用钩子)
-
- [setTimeout 的正确打开方式](#setTimeout 的正确打开方式)
-
- [✅ 场景 1:等待 DOM 完全渲染(尤其是涉及图片、字体加载时)](#✅ 场景 1:等待 DOM 完全渲染(尤其是涉及图片、字体加载时))
- [✅ 场景 2:防抖(Debounce)与简单延时](#✅ 场景 2:防抖(Debounce)与简单延时)
- [比 setTimeout 更好的选择 ------ nextTick](#比 setTimeout 更好的选择 —— nextTick)
- [❌ 误区 2:在 updated 中修改数据导致死循环](#❌ 误区 2:在 updated 中修改数据导致死循环)
- [✅ 最佳实践 1:清理副作用](#✅ 最佳实践 1:清理副作用)
- [✅ 最佳实践 2:封装可复用逻辑(组合式函数)](#✅ 最佳实践 2:封装可复用逻辑(组合式函数))
- 四、实战场景速查表
生命周期流程图深度解析

流程图关键节点拆解:
1️⃣ 渲染器遇到组件 → 组件实例开始创建
- 这是生命周期的起点,Vue 内部开始初始化组件
- 此时还没执行任何用户代码
2️⃣ setup() / beforeCreate → "胎儿期"
- setup() 是组合式 API 的入口,最早执行的钩子
- beforeCreate 在选项式 API 中对应,此时实例刚创建,数据观测和事件都还没设置
- ⚠️ 注意:setup() 中无法访问 this,因为实例还没完全初始化
3️⃣ 初始化选项式 API → created
- 数据观测、计算属性、方法等已设置完成
- 可以访问 data、computed、methods
- ❗但 DOM 还没生成,不能操作 DOM
4️⃣ 模板编译阶段
- 判断是否有预编译模板(.vue 文件通常有)
- 没有则即时编译(运行时编译,性能稍差)
- 这个阶段用户一般不干预
5️⃣ beforeMount → "出生前一刻"
- 模板已编译成渲染函数,但还没挂载到 DOM
- 可以最后一次修改响应式数据,不会触发额外更新
6️⃣ mounted → "正式出生" 🎉
- DOM 节点已创建并插入页面
- ✅ 可以安全操作 DOM、发起异步请求、初始化第三方库(如图表、地图)
- 最常用钩子之一
7️⃣ 挂载后循环:数据变化 → beforeUpdate → 重新渲染 → updated
- 当响应式数据变化时,触发更新流程
- beforeUpdate:DOM 还没更新,可获取更新前的 DOM
- updated:DOM 已更新完成,可操作更新后的 DOM
- ⚠️ 避免在 updated 中修改数据,否则会无限循环!
8️⃣ 卸载阶段:beforeUnmount → unmounted
- 组件被移除(如 v-if="false" 或路由切换)
- beforeUnmount:清理定时器、取消订阅、解绑事件
- unmounted:组件已彻底销毁,无法再访问
12 个生命周期钩子函数详解(组合式 API 版)
💡 所有钩子都从 'vue' 导入,命名规范:on + 钩子名(首字母大写)
✅ 核心三剑客(90% 场景用它们)
onMounted
ts
import { onMounted } from 'vue'
onMounted(() => {
console.log('组件已挂载到 DOM')
// ✅ 最佳实践场景:
// - 发起 API 请求
// - 初始化 ECharts / Three.js 等重型库
// - 绑定 window 事件(记得在 onUnmounted 解绑!)
})
onUpdated
ts
import { onUpdated } from 'vue'
onUpdated(() => {
console.log('组件因数据变化而重新渲染')
// ⚠️ 警告:不要在这里修改响应式数据!
// ✅ 适用场景:
// - 更新后需要重新计算 DOM 尺寸(如滚动位置)
// - 同步第三方库状态(如更新图表数据)
})
onUnmounted
ts
import { onUnmounted } from 'vue'
onUnmounted(() => {
console.log('组件即将被销毁')
// ✅ 必做清理工作:
// - clearInterval / clearTimeout
// - removeEventListener
// - 取消 Axios 请求(使用 AbortController)
// - 断开 WebSocket 连接
})
🟡 进阶钩子(特定场景使用)
onBeforeMount
ts
// 很少用,除非你需要在 DOM 渲染前做最后的数据调整
onBeforeMount(() => {
// 此时模板已编译,但 DOM 未生成
// 修改数据不会触发额外更新
})
onBeforeUpdate
ts
// 用于在 DOM 更新前访问旧状态
onBeforeUpdate(() => {
const oldDom = document.querySelector('.my-element')
console.log('更新前的 DOM:', oldDom)
})
onBeforeUnmount
ts
// 和 onUnmounted 类似,但更早一点
// 通常两者选其一即可,推荐用 onUnmounted
onBeforeUnmount(() => {
// 清理工作
})
🔴 调试与高级钩子(慎用!)
onErrorCaptured
ts
// 捕获子组件抛出的错误
import { onErrorCaptured } from 'vue'
onErrorCaptured((err, instance, info) => {
console.error('捕获到错误:', err)
console.log('错误来源组件:', instance)
console.log('错误信息:', info)
// 返回 false 阻止错误继续向上传播
return false
})
onRenderTracked 和 onRenderTriggered
ts
// 🚨 仅用于调试!生产环境不要使用
// 追踪哪些响应式数据触发了渲染
onRenderTracked((event) => {
console.log('追踪到依赖:', event.key)
})
onRenderTriggered((event) => {
console.log('触发渲染的数据:', event.key)
})
🟣 KeepAlive 专属钩子(缓存组件专用)
onActivated 和 onDeactivated
ts
import { onActivated, onDeactivated } from 'vue'
// 只在使用 <KeepAlive> 包裹组件时生效
onActivated(() => {
console.log('组件被激活(从缓存恢复)')
// 重置定时器、重新请求数据等
})
onDeactivated(() => {
console.log('组件被停用(进入缓存)')
// 暂停定时器、保存状态等
})
💡 典型场景:Tab 切换、路由缓存( 配合 keep-alive)
SSR 专属钩子
onServerPrefetch
ts
// 仅在服务端渲染时执行
import { onServerPrefetch } from 'vue'
onServerPrefetch(async () => {
// 在服务端预取数据
const data = await fetch('/api/data').then(r => r.json())
// 赋值给响应式变量,服务端渲染时会包含这些数据
})
常见误区 & 最佳实践
❌ 误区 1:在 setup() 外异步调用钩子
ts
// 错误示范!
setTimeout(() => {
onMounted(() => { ... }) // 无效!组件实例已丢失
}, 1000)
// ✅ 正确做法:同步注册
onMounted(() => { ... })
setTimeout 的正确打开方式
- setTimeout 函数 用来指定某个函数或某段代码,在多少毫秒之后执行,返回一个整数,表示定时器的编号,以后可以用来取消这个定时器
- 接受两个参数, 第一个是要延迟执行的函数名或者代码,第二个参数是推迟执行的毫秒数
- 注意:this在定时器中指向window
接下来,对错误示范中的执行顺序进行说明。
🕵️♂️ 破案过程:
- 组件初始化阶段:
- Vue 开始创建组件实例。
- 执行 setup() 函数。
- 关键点:Vue 会在 setup() 同步执行完毕的那一刻,收集所有已注册的生命周期钩子(比如你在第一行直接写的 onMounted(...))。
- 此时,setTimeout 被放入浏览器的宏任务队列,等待 1 秒后执行。setup() 函数继续往下走,然后结束。
- 挂载阶段:
- Vue 检查收集到的钩子列表,发现有一个 onMounted。
- 执行 DOM 挂载。
- 挂载完成后,触发那个早已注册好的 onMounted 回调。
- 组件状态变为"已挂载"。
- 1 秒后:
- setTimeout 的回调终于执行了。
- 代码试图调用 onMounted(...)。
- 灾难发生:此时 setup() 的上下文(Context)已经结束了,Vue 内部无法知道这个新的 onMounted 应该绑定到哪个组件实例上(或者说,组件已经过了"注册钩子"的阶段,甚至可能已经挂载完了)。
- 结果:这个回调要么被忽略,要么绑定到了一个错误的/空的上下文,永远不会被触发。
虽然不能在 setTimeout 里注册钩子,但我们在 onMounted 等钩子内部经常使用 setTimeout。它是处理时序问题的神器。
✅ 场景 1:等待 DOM 完全渲染(尤其是涉及图片、字体加载时)
有时候,onMounted 触发了,DOM 节点虽然插入了,但浏览器还没来得及计算布局(Layout),或者图片还没加载出来,导致获取到的宽高为 0。
js
import { onMounted, ref } from 'vue'
const width = ref(0)
const myRef = ref(null)
onMounted(() => {
// ❌ 直接获取可能为 0,因为浏览器重绘还没完成
// width.value = myRef.value.offsetWidth
// ✅ 使用 setTimeout 推迟到下一个事件循环
setTimeout(() => {
width.value = myRef.value.offsetWidth
console.log('获取到的真实宽度:', width.value)
}, 0) // 延迟 0ms 即可,目的是放入宏任务队列
})
原理:setTimeout(..., 0) 会将回调放入宏任务队列,等到当前所有同步代码(包括 Vue 的渲染流程)都执行完后,浏览器完成了重绘,再执行这个回调。
✅ 场景 2:防抖(Debounce)与简单延时
在处理搜索输入、窗口大小调整时,避免频繁触发逻辑。
js
import { ref, onUnmounted } from 'vue'
let timer = null
const handleSearch = (e) => {
clearTimeout(timer) // 清除上一次的定时器
timer = setTimeout(() => {
console.log('发起搜索请求:', e.target.value)
// api.search(...)
}, 500) // 用户停止输入 500ms 后才执行
}
onUnmounted(() => {
// ⚠️ 重要:组件销毁时必须清除定时器,防止内存泄漏
if (timer) clearTimeout(timer)
})
比 setTimeout 更好的选择 ------ nextTick
在 Vue 中,如果你想等待 DOM 更新,首选方案不是 setTimeout,而是 nextTick。
| 特性 | setTimeout(fn, 0) |
await nextTick() |
|---|---|---|
| 执行时机 | 宏任务队列(较晚) | 微任务队列(紧跟在当前同步代码后,DOM 更新完立即执行) |
| 精确度 | 受浏览器最小延迟限制(通常 4ms+) | 精确对应 Vue 的 DOM 更新周期 |
| 性能 | 稍慢,需要等待事件循环一圈 | 更快,无需等待宏任务 |
| 语义 | "过一会儿执行" | "等 DOM 更新完立刻执行" |
| 推荐度 | ⭐⭐ (通用 JS 方案) | ⭐⭐⭐⭐⭐ (Vue 官方推荐) |
js
import { ref, onMounted, nextTick } from 'vue'
const count = ref(0)
const message = ref('初始')
const updateAndLog = async () => {
count.value++
message.value = '更新后的内容'
// ❌ 不推荐:不知道要等多久,可能太早也可能太晚
// setTimeout(() => { ... }, 0)
// ✅ 推荐:等待 Vue 完成 DOM 更新
await nextTick()
// 此时 DOM 绝对已经是最新的状态了
console.log(document.querySelector('.msg').textContent)
}
❌ 误区 2:在 updated 中修改数据导致死循环
ts
// 危险!
onUpdated(() => {
count.value++ // 触发更新 → 再次调用 updated → 无限循环
})
// ✅ 解决方案:加条件判断
onUpdated(() => {
if (shouldUpdate) {
// 安全操作
}
})
✅ 最佳实践 1:清理副作用
ts
let timer = null
onMounted(() => {
timer = setInterval(() => { ... }, 1000)
})
onUnmounted(() => {
clearInterval(timer) // 必须清理!
})
✅ 最佳实践 2:封装可复用逻辑(组合式函数)
js
// useMousePosition.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMousePosition() {
const x = ref(0)
const y = ref(0)
const update = (e) => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
// 组件中使用
const { x, y } = useMousePosition()
四、实战场景速查表
| 场景 | 推荐钩子 | 说明 |
|---|---|---|
| 发起 API 请求 | onMounted |
DOM 已就绪,可显示 loading |
| 操作 DOM | onMounted |
确保元素存在 |
| 清理定时器/事件 | onUnmounted |
防止内存泄漏 |
| 监听窗口大小变化 | onMounted + onUnmounted |
绑定和解绑事件 |
| 缓存组件状态恢复 | onActivated |
KeepAlive 场景 |
| 调试渲染性能 | onRenderTracked |
开发环境专用 |
| 服务端预取数据 | onServerPrefetch |
SSR 项目专用 |