生命周期钩子的执行时机
组合式 API 与生命周期 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
生命周期钩子是「在组件生命周期的特定时刻执行代码」的入口。这一节要掌握常用钩子的触发时机与顺序、父子组件之间的执行次序、以及每个钩子适合做什么、不适合做什么。
问题场景
你在 onMounted 里请求数据、在 onUpdated 里同步滚动位置、在 onUnmounted 里清理定时器。某天发现:父组件 onMounted 里拿不到子组件渲染出的 DOM 尺寸;onUpdated 里的逻辑把组件自己又更新了一次,形成死循环;定时器没清,切页面后控制台还在打印。三个问题都源于对钩子时机和职责的误解。
核心解释
Vue 组件的生命周期分四个阶段:创建 → 挂载 → 更新 → 卸载。组合式 API 里对应的钩子:
| 钩子 | 时机 | 典型用途 |
|---|---|---|
onBeforeMount |
挂载前,模板已编译未插入 DOM | 很少用 |
onMounted |
组件 DOM 已插入文档 | 请求数据、初始化第三方库、读 DOM 尺寸 |
onBeforeUpdate |
数据变了、DOM 还没更新 | 更新前记录滚动位置 |
onUpdated |
DOM 已按新数据更新 | 依赖新 DOM 的操作(少用) |
onBeforeUnmount |
卸载前,实例仍可用 | 保存草稿、解绑事件 |
onUnmounted |
已卸载,DOM 移除 | 清理定时器、取消请求、销毁实例 |
<script setup>
import { onMounted, onUnmounted, onUpdated, ref } from "vue";
const el = ref(null);
const count = ref(0);
onMounted(() => {
console.log("已挂载,可以拿到 DOM:", el.value?.tagName);
const id = setInterval(() => count.value++, 1000);
onUnmounted(() => clearInterval(id)); // 也可以在外部单独写 onUnmounted
});
onUpdated(() => {
console.log("DOM 已更新,当前文本:", el.value?.textContent);
});
onUnmounted(() => {
console.log("组件已卸载,清理完成");
});
</script>
<template>
<p ref="el">计数:{{ count }}</p>
</template>
父子顺序是高频考点。挂载时:父 beforeMount → 子 beforeMount → 子 mounted → 父 mounted(子先完成挂载,父才能确认整棵子树就绪)。卸载时:父 beforeUnmount → 子 beforeUnmount → 子 unmounted → 父 unmounted。更新时:父 beforeUpdate → 子 beforeUpdate → 子 updated → 父 updated。
onMounted 不等于「所有子组件都渲染完」:父的 onMounted 触发时,子组件的 DOM 已在文档里(子先 mounted),但如果子组件内部还在异步加载内容,父拿到的尺寸可能仍是初始值。需要「内容稳定后」再测量,用 nextTick 或 ResizeObserver。
不要滥用 onUpdated:它在每次重渲染后都触发,在里面改状态极易造成死循环(改状态 → 触发更新 → 又触发 onUpdated)。派生逻辑用 computed,DOM 同步用 watch + nextTick 更可控。
SSR 注意:服务端没有 DOM,onMounted 及之后的钩子不会在服务端执行。所以依赖 window/document 的代码放 onMounted 里是安全的——这也是它最常见的用法。
示例拆解
一个「元素尺寸监听」组件,把 onMounted / onBeforeUnmount / onUnmounted 串起来:
<script setup>
import { onBeforeUnmount, onMounted, onUnmounted, ref } from "vue";
const box = ref(null);
const width = ref(0);
let observer = null;
onMounted(() => {
if (!box.value) return;
width.value = box.value.clientWidth;
observer = new ResizeObserver((entries) => {
width.value = Math.round(entries[0].contentRect.width);
});
observer.observe(box.value);
});
onBeforeUnmount(() => {
// 卸载前停止观察,避免回调访问已卸载组件的状态
observer?.disconnect();
});
onUnmounted(() => {
observer = null;
console.log("清理完成");
});
</script>
<template>
<div ref="box" class="box">
当前宽度:{{ width }}px
</div>
</template>
<style scoped>
.box { resize: horizontal; overflow: auto; border: 1px dashed #999; padding: 8px; }
</style>
拖拽右下角改变宽度,数字实时更新;切走页面后观察控制台没有残留回调。ResizeObserver 这类「外部系统」必须在卸载时断开,否则它持有组件引用,造成内存泄漏。
动手任务
- 写一个父子组件各带全套钩子日志的实验,记录实际执行顺序,和上面的理论顺序对照。
- 在
onMounted里用setInterval每秒更新一次时间,在onUnmounted里清理;用 Vue Devtools 反复挂载卸载,确认没有重复定时器。 - 在
onUpdated里故意count.value++,观察控制台报「Maximum recursive updates exceeded」,理解为什么。
常见错误
- 在 setup 顶层读 DOM:组件未挂载,
ref.value是 null。放onMounted。 - 在 onUpdated 里改状态:递归更新,Vue 会抛「Maximum recursive updates exceeded」。
- 忘记清理定时器/监听器/observer:组件卸载后回调继续执行,报错或泄漏。onUnmounted 统一清理。
- 以为 onMounted 在 SSR 也执行:服务端不执行,依赖它的逻辑要考虑首屏数据由服务端预取。
检查与复盘
- 能默写六个常用钩子及其时机
- 能说出父子组件挂载/卸载的顺序
- 能判断一段初始化代码该放 setup 顶层、onMounted 还是 watch
参考资料
- Vue 官方文档「生命周期钩子」:全部钩子与执行顺序图
- Vue 官方文档「组合式 API 生命周期」:与 Options API 的对照表
读完这一课了?
标记完成后,工作台会安排接下来的学习。