一、生命周期是什么
Vue 组件从"被创建"到"被销毁",会经历一系列阶段。Vue 在每个阶段提供一个回调函数入口, 可以在特定时机执行代码 —— 这些回调函数就叫生命周期钩子(Lifecycle Hooks)
即每个人的一天:
睡醒 ------> 洗漱 ------> 吃饭 ------> 上班 ------> 下班 ------> 睡觉
| | | | | |
出生hook 洗漱hook 吃饭hook 上班hook 下班hook 睡觉hook
不需要在每个阶段都写代码,但是想在某个阶段做事时,知道去哪个钩子写就行。
二、Vue3 完整生命周期流程图
createApp(App).mount('#app')
│
▼
┌────────────────────────────────────────────┐
│ ① setup() │ ← Vue 3 组合式API的入口
│ 所有代码最先执行的地方 │
│ ref()、computed()、watch() 在这里注册 │
│ (immediate: true时回调在同一时刻同步执行一次) │
│ ⚠️ 此时还没有 DOM, template 还没渲染 │
└────────────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ ② onBeforeMount() │ ← DOM 还没挂载
│ Vue 已经编译好 template,生成了虚拟DOM │
│ 但还没插入到页面的 #app 容器里 │
└────────────────────┬────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ ③ onMounted() │ ← DOM 已经挂载完毕 ⭐
│ 页面上已经能看到渲染出来的 HTML 了 │
│ ✅ 这里可以操作 DOM(获取元素、初始化第三方库) │
│ ✅ 这里可以发网络请求(但不推荐,见后文) │
└────────────────────┬─────────────────────────┘
│
▼
┌─── 数据变化?──────┐
│ │
│ 响应式系统检测 │
│ ref/props 变了 │
│ │
▼ ▼
┌────────────────────┐ 不变,等待用户操作
│ ④ onBeforeUpdate()│
│ DOM 还没更新 │
│ 此时读到的是旧DOM │
└────────┬───────────┘
│
▼
┌──────────────────┐
│ ⑤ onUpdated() │ ← DOM 已经更新完毕
│ 新的 DOM 已就位 │ ⚠️ 不要在里面改状态,会死循环
└────────┬─────────┘
│
└─── 回到等待状态,直到下次数据变化或组件销毁
┌─── 用户导航离开 / v-if=false ──────┐
│ │
▼ ▼
┌────────────────────────────────┐ ┌────────────────────────────────┐
│ ⑥ onBeforeUnmount() │ │ ⑦ onUnmounted() │
│ 组件还存在于 DOM 中 │ │ 组件已从 DOM 移除 │
│ ✅ 清理定时器、取消订阅 │ │ ✅ 最后的清理机会 │
│ ✅ 移除全局事件监听 │ │ ⚠️ 这里访问不到 DOM 了 │
└───────────────────────────────┘ └────────────────────────────────┘
三、结合项目
- setup() - 最先执行:
// 这整个 <script setup> 就是 setup() 的语法糖
<script setup lang="ts">
import { ref, watch } from 'vue'
import rtpe { Porduct } from '@/types/product.ts';
// ===== 这些代码在 setup 阶段同步执行 =====
const products = ref<Product[]>([]) // 创建响应式引用,值 = []
const loading = ref(false) // 创建响应式引用,值 = false
const page = ref(1) // 创建响应式引用,值 = 1
const total = ref(0) // 创建响应式引用,值 = 0
// watch 注册,immediate: true 时,回调在同一时刻【同步】执行 --- 仍在setup阶段,早于挂载 onMounted
watch(
[searchParams, page],
([params, p]) => { load({ ...params, page: p }) },
{ immediate: true },
)
// 函数定义(只是注册,没被调用)
function load(params) { ... }
function handlePageChange(newPage) { ... }
</script>
setup() 时的状态:
- DOM:❌不存在
- 数据:✅已就绪(ref都创建好了)
- 网络请求:✅已发起(immediate 回调在 setup 阶段同步执行,请求已发出,响应稍后到达)
2.onBeforeMount() - 编译好了但还没插到页面
import { onBeforeMount } from 'vue'
onBeforeMount(() => {
// Vue 已经把 template 编译成了虚拟DOM树
// 但页面上还看不到任何东西
// 此时 DOM 里面没有 <div class="product-list">
console.log(document.querySelector('.product-list'))
// -> null (DOM 好没插入)
})
实际开发中很少用这个钩子。99% 的场景都不需要关心 "编译好了但还没挂载" 这个中间状态
3.onMounted() - ⭐️DOM 就绪,页面可见
import { onMounted } from 'vue'
onMounted(() => {
// ✅此时页面上已经有 HTML 了
console.log(document.querySelector('.product-list'))
// -> <div class="product-list">...</div> (能拿到真实 DOM)
// ✅ 可以操作 DOM:获取元素尺寸、初始化拖拽库等
// ✅ 可以启动轮询定时器
// ✅ 可以发网络请求(但项目用了 watch immediate,效果一样)
})
在 **onMounted()**里发请求有两种方式:
// 方式一:在 onMounted 里发请求(很多教程这么教)
onMounted(async () => {
const data = await getProducts({ page: 1 })
products.value = data.list
})
// 方式二:用 watch + immediate(本项目用的,更优雅)
watch([searchParams, page], ([params, p]) => {
load({ ...params, page: p })
}, { immediate: true })
方式二更佳,因为:
- 搜索或者翻页时,watch 会自动触发 load()
- 方式一需要在
handleSearch,handlePageChange里手动再调用过一次 load() - watch 把 “依赖变化 -> 重新加载” 这件事声明式地表达出来了
4.onBeforeUpdate() + 5.onUpdated() - 数据变了,DOM 要更新
用户点击第2页
│
▼
page.value = 2
│
▼
Vue 检测到 ref 变化,触发 re-render
│
▼
┌──────── onBeforeUpdate() ─────────────┐
│ ⚠️ 此时 DOM 还是旧的(第1页的数据) │
│ 如果你在这里读 DOM,拿到的是过时的内容 │
└──────────────────┬────────────────────┘
│
▼
Vue 用新的虚拟 DOM 对比旧的
计算出最小化的 DOM 操作(diff 算法)
│
▼
┌──────────── onUpdated() ───────────────┐
│ ✅ DOM 已经是第2页的数据了 │
│ ⚠️ 不要在这里修改状态(改 ref → 再触发 │
│ update → 再触发 onUpdated → 死循环) │
└───────────────────────────────────── ───┘
实际开发中也很少直接用。Vue的响应式系统已经处理好了大部分场景。只需要在操作 DOM 并且必须在更新后执行时采用。
6.onBeforeUnmount() + 7.onUnmounted() - 组件销毁前后
当用户从商品列表页导航到其他页面(比如用了 Vue Router), **「ProductList.vue」**就会被销毁;
import { onBeforeUnmount, onUnmounted } from 'vue'
let pollTimer: number
onMounted(() => {
// 假设在这里启动了一个轮询定时器
pollTimer = setInterval(() => {
load({ ...searchParams.value, page: page.value })
}, 30000)
})
onBeforeUnmount(() => {
// ✅ 组件即将被销毁,DOM 还在
// 清理定时器!否则组件销毁后定时器还在跑,内存泄漏
clearInterval(pollTimer)
})
onUnmounted(() => {
// ✅ 组件已销毁,DOM 已移除
// 最后的清理机会
})
为什么要清理:
- 用户进入商品列表页 -> mounted -> setInterval 开始跑(每30s刷新)
- 用户离开商品列表页 -> 组件销毁,但 setInterval 还在跑!
- 内存泄漏‼️(定时器引用了一个已经不存在的组件)
- 用户再次进入 -> mounted -> 又一个 setInterval
- 这就有2个定时器在运行,更严重
四、<script setup> 和 生命周期的关系
代码中其实并没有具体的写 setup() 函数,也没有显示调用 onMounted。这是因为 <script setup> 是 Vue 3 的语法糖,Vue 会自动把<script setup>中的代码放到 setup() 函数里执行。
对应关系:
| 代码 | 实际执行时机 |
|---|---|
| ref([]) | setup() 阶段,同步创建 |
| computed(...) | setup() 阶段,同步创建(但值是懒计算的) |
| watch(..., { immediate: true }) | setup() 注册,同一时刻同步执行回调(仍在setup阶段,遭遇onMounted) |
| watch(...)(没有 immediate) | setup() 注册,等依赖变化时才执行 |
<template> 里的内容 |
onBeforeMount 之前编译,onMounted 时已可见 |
五、什么时候用哪个钩子(速查)
| 你要做的事 | 用哪个钩子 |
|---|---|
| 发网络请求获取数据 | watch + immediate(推荐)或 onMounted(也可以) |
| 操作 DOM(获取元素、测量尺寸) | onMounted ✅ |
| 初始化第三方库(ECharts、地图等) | onMounted ✅ |
| 添加全局事件监听(window.resize) | onMounted ✅,配合 onUnmounted 清理 |
| 启动轮询/定时器 | onMounted,配合 onBeforeUnmount 清理 |
| 数据变了,需要在 DOM 更新后操作 | onUpdated ⚠️(慎用) |
| 组件销毁前清理资源 | onBeforeUnmount ✅ |
| 创建响应式数据、计算属性 | 直接写在 <script setup> 顶层 |
核心要点:
setup()是同步的,一次性跑完onMounted是所有子组件都挂载完后才触发watch({ immediate })的回调在onMounted阶段执行,而它触发的异步请求返回后会再出发一轮onUpdated,所以要注意不要在onUpdated里再去改状态,否则会死循环