Vue 生命周期
每个 Vue 组件都经历"创建 → 挂载 → 更新 → 卸载"四个阶段。Vue 在每个阶段都提供对应的"钩子函数",你可以在里面插入自己的逻辑——发请求、操作 DOM、清理资源。本章讲清所有钩子,以及最常踩的"内存泄漏"陷阱。
1. 生命周期全景
Vue 3 Composition API 的生命周期钩子全部以 on 开头,从 vue 中 import 使用:
<script setup>
import {
onBeforeMount, onMounted,
onBeforeUpdate, onUpdated,
onBeforeUnmount, onUnmounted,
} from "vue";
// 1. setup() 本身就是第一个阶段(同步执行,组件实例创建)
console.log("setup 执行");
// 2. 挂载前:DOM 还没生成,基本用不到
onBeforeMount(() => {
console.log("挂载前(DOM 还不存在)");
});
// 3. 挂载后:DOM 已经渲染,最常用 ★
onMounted(() => {
console.log("已挂载(可以操作 DOM 了)");
});
// 4. 更新前:数据变了,DOM 即将重渲染
onBeforeUpdate(() => {
console.log("DOM 即将更新");
});
// 5. 更新后:DOM 已重渲染完成
onUpdated(() => {
console.log("DOM 已更新");
});
// 6. 卸载前:组件即将销毁,适合做清理 ★
onBeforeUnmount(() => {
console.log("即将销毁");
});
// 7. 卸载后:组件已完全销毁 ★
onUnmounted(() => {
console.log("已销毁");
});
</script>七个阶段的执行顺序:
- setup:组件实例创建,响应式数据初始化。
<script setup>整体就是 setup。 - onBeforeMount:DOM 即将挂载,但还没生成。基本用不到。
- onMounted:DOM 已挂载到页面。最常用的钩子,90% 的初始化逻辑放这里。
- onBeforeUpdate:数据变化后,DOM 重渲染前。
- onUpdated:DOM 已完成重渲染。
- onBeforeUnmount:组件即将销毁。适合做最后的清理准备。
- onUnmounted:组件已销毁。清理钩子,移除事件监听、清定时器。
2. onMounted:最常用的钩子
组件挂载到页面后触发——此时可以安全地操作 DOM、发请求、初始化第三方库。三个高频场景:
<script setup>
import { ref, onMounted } from "vue";
const data = ref(null);
const isLoading = ref(true);
// 最经典用法:发请求加载数据
onMounted(async () => {
const res = await fetch("/api/users");
data.value = await res.json();
isLoading.value = false;
});
// 第二个常见用法:操作 DOM(此时 ref 已经填充)
const canvas = ref(null);
onMounted(() => {
const ctx = canvas.value.getContext("2d");
ctx.fillStyle = "red";
ctx.fillRect(0, 0, 100, 100);
});
// 第三个用法:启动定时器、订阅事件
const timer = ref(0);
onMounted(() => {
window.setInterval(() => timer.value++, 1000);
});
</script>
<template>
<canvas ref="canvas"></canvas>
</template>为什么不能在 setup 顶层直接发请求?因为 setup 执行时DOM 还没生成,如果你立刻操作 DOM(document.querySelector),会拿到 null。即使发请求也建议放 onMounted——虽然放 setup 也能跑,但放 onMounted 更符合心智模型,且能保证 DOM 先渲染出来显示 loading。
3. onUnmounted:清理钩子(防内存泄漏)
组件销毁时,你在 onMounted 里启动的"副作用"必须在这里清理。否则组件销毁了,定时器还在跑、事件监听还在响应——这就是经典的"内存泄漏"。
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
const windowWidth = ref(window.innerWidth);
onMounted(() => {
// 注册全局事件
window.addEventListener("resize", onResize);
});
// ⚠️ 必须在卸载时移除,否则组件销毁后事件还在跑(内存泄漏)
onUnmounted(() => {
window.removeEventListener("resize", onResize);
});
function onResize() {
windowWidth.value = window.innerWidth;
}
</script>哪些东西需要清理?
- 定时器:setInterval / setTimeout。
- 全局事件监听:window.addEventListener / document.addEventListener。
- WebSocket / EventSource:订阅消息的连接。
- 第三方库实例:ECharts、Mapbox、视频播放器的 dispose 方法。
- 订阅:Observable、Pub/Sub。
养成"有开就有关"的习惯:onMounted 里 add,onUnmounted 里 remove,配对出现。
4. 实战:计时器组件
看一个完整的"启动-清理"配对示例:
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
const elapsed = ref(0);
let intervalId = null;
onMounted(() => {
// 启动定时器
intervalId = window.setInterval(() => {
elapsed.value++;
}, 1000);
});
onUnmounted(() => {
// 清理定时器
if (intervalId) {
window.clearInterval(intervalId);
intervalId = null;
}
});
</script>
<template>
<p>已经过去 {{ elapsed }} 秒</p>
</template>如果不写 onUnmounted,组件销毁后定时器还在后台跑(只是看不到输出),用户切换 100 次页面就有 100 个定时器同时跑,浏览器会越来越卡。这是新手最常踩的坑。
5. onUpdated:慎用
每次数据变化导致 DOM 重渲染后触发。听起来有用,但实际项目中很少用——大部分依赖数据的逻辑都该用 watch 或 computed。onUpdated 容易导致死循环(在里面改数据 → 触发更新 → 再次触发 onUpdated ...)。
真正需要在 DOM 更新后做点什么,优先用:
watch(foo, callback, { flush: 'post' }):在 DOM 更新后触发回调。nextTick(() => ...):等下一次 DOM 更新后执行。
6. nextTick:等 DOM 更新
Vue 的更新是异步的——你改了数据,DOM 不会立刻变,而是在下一个"tick"批量更新。如果你需要在改数据后立刻读 DOM,要等 nextTick:
<script setup>
import { ref, nextTick } from "vue";
const list = ref([1, 2, 3]);
const containerRef = ref(null);
async function add() {
list.value.push(4);
// ❌ 此时 DOM 还没更新,读到的是旧高度
// const height = containerRef.value.scrollHeight;
// ✅ 等 DOM 更新后再读
await nextTick();
const height = containerRef.value.scrollHeight;
console.log("新高度:", height);
}
</script>这是 Vue 的新手高频疑惑:"为什么我改了数据,立刻读 DOM 还是旧值?" 答案就是异步更新——用 nextTick 等一下即可。
7. watch 的清理函数
对于"数据变化时发请求"的场景,watch 回调里也可以返回清理函数,处理"上一次请求还没回来,数据又变了"的竞态:
<script setup>
import { ref, watch } from "vue";
const userId = ref(1);
// watch 的回调可以返回一个清理函数
// 下次触发前,Vue 会自动调用上一次的清理函数
watch(userId, (newId, oldId, onCleanup) => {
const controller = new AbortController();
fetch(`/api/user/${newId}`, { signal: controller.signal })
.then((r) => r.json())
.then((data) => {
console.log("收到:", data);
});
// 如果 userId 再次变化前请求没完成,abort 掉
onCleanup(() => {
controller.abort();
});
});
</script>这种模式叫"AbortController 模式",能避免旧请求覆盖新请求的结果(比如用户快速输入时,先请求的 'abc' 后回来,把后请求的 'abcd' 结果覆盖了)。
8. 调试钩子
Vue 还提供两个调试用的钩子,只在开发模式生效:
<script setup>
import { onRenderTracked, onRenderTriggered } from "vue";
// 仅开发环境用,调试响应式依赖追踪
onRenderTracked((event) => {
console.log("依赖被收集:", event);
});
onRenderTriggered((event) => {
console.log("依赖变化触发重渲染:", event);
// event 包含:effect, target, key, type, newValue, oldValue
// 用 debugger 可以打断点找出"为什么重新渲染了"
});
</script>当你发现"某个组件莫名其妙重新渲染"时,用 onRenderTriggered 加 debugger,能精确找到"是哪个响应式数据变化触发的"。生产环境会自动移除,无性能影响。
9. 与 Vue 2 生命周期的对照
如果你看过 Vue 2 的资料,会注意到名字不一样。对照表:
- Vue 2
beforeCreate/created→ Vue 3 用 setup() 替代。 beforeMount→onBeforeMount(同名加 on 前缀)。mounted→onMounted。beforeUpdate→onBeforeUpdate。updated→onUpdated。beforeDestroy→onBeforeUnmount(改名,因为"destroy"语义不准)。destroyed→onUnmounted。
小结
这一章你掌握了 Vue 组件从生到死的所有钩子。记两个最重要的:onMounted(初始化)和 onUnmounted(清理),它们配对出现就能避开 90% 的内存泄漏。下一篇进入工程化——用 Vue Router 做多页面应用。
← 上一篇 Vue Props 与 Emit
下一篇 Vue Router 路由 →