讲解
组件从创建到销毁经历固定阶段:初始化(setup)、挂载(mounted)、更新(updated)、卸载(unmounted)。Vue 在每个阶段提供钩子函数,让你把「时机敏感」的代码挂到正确的时间点。Composition API 里钩子都是 on 开头的函数:onMounted、onUpdated、onUnmounted、onBeforeUnmount 等,在 <script setup> 顶层直接调用。
记住三个最高频的钩子就够了。onMounted:DOM 已生成,适合做「必须摸到真实节点」的事——初始化图表库、测量尺寸、聚焦输入框,也是客户端发请求的常见位置(注意:SSR 时不执行)。onUnmounted:组件即将销毁,做清理——清定时器、取消订阅、移除 window 事件监听,漏掉就是内存泄漏。onUpdated:数据变化导致 DOM 更新后触发,用得少,而且容易滥用(大多数时候你需要的其实是 watch 或 computed)。
两个心智要点。第一,<script setup> 的顶层代码本身就是「创建阶段」,不需要 onBeforeCreate/onCreated——变量初始化、发请求(不依赖 DOM 的)直接写顶层。第二,钩子的执行顺序:setup → mounted →(每次更新)updated → unmounted;父子嵌套时,子组件先 mounted,父组件后 mounted(子的 DOM 先就位)。
示例
用 JS 模拟组件树的挂载顺序和定时器泄漏问题(在本教程构建时被真实执行):
import assert from 'node:assert/strict';
const order = [];
// 迷你组件系统:setup → mounted → unmounted
function createComponent(name, children = []) {
const hooks = { mounted: [], unmounted: [] };
return {
name,
onMounted: (fn) => hooks.mounted.push(fn),
onUnmounted: (fn) => hooks.unmounted.push(fn),
mount() {
order.push(name + ':setup');
for (const c of children) c.mount(); // 子组件先挂载
for (const fn of hooks.mounted) fn();
order.push(name + ':mounted');
},
unmount() {
for (const c of children) c.unmount(); // 卸载同样递归子组件
for (const fn of hooks.unmounted) fn();
order.push(name + ':unmounted');
},
};
}
const child = createComponent('Child');
const parent = createComponent('Parent', [child]);
// 定时器:unmount 时清理
const timers = [];
child.onMounted(() => timers.push(setInterval(() => {}, 1000)));
child.onUnmounted(() => timers.forEach(clearInterval));
parent.mount();
assert.deepStrictEqual(order, [
'Parent:setup',
'Child:setup',
'Child:mounted', // 子先 mounted
'Parent:mounted', // 父后 mounted
]);
assert.strictEqual(timers.length, 1);
parent.unmount();
assert.deepStrictEqual(order.at(-1), 'Parent:unmounted');
console.log('钩子执行顺序:', order.join(' → '));
console.log('定时器已随 unmounted 清理,无泄漏');
真实组件:进入页面加载数据,离开页面停止轮询。
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const list = ref([]);
let timer;
onMounted(async () => {
const res = await fetch('/api/todos');
list.value = await res.json();
timer = setInterval(async () => {
const r = await fetch('/api/todos');
list.value = await r.json(); // 每 30 秒刷新
}, 30000);
});
onUnmounted(() => clearInterval(timer)); // 离开页面停止轮询
</script>
<template>
<li v-for="t in list" :key="t.id">{{ t.text }}</li>
</template>
常见坑
- 在 setup 顶层摸 DOM:此时模板还没渲染,ref 拿到的是 null,DOM 操作必须放 onMounted。
- 忘记清理 window 监听和定时器:组件卸载后回调还在跑,轻则报错重则泄漏;onUnmounted 里逐一对称清理。
- 用 onUpdated 代替 watch:updated 对每次 DOM 更新都触发,太宽泛;精确依赖用 watch。
- SSR 场景在顶层发请求:服务端渲染没有 mounted,需要两端都跑的逻辑用 onServerPrefetch 或确保只在客户端执行。
- 父子钩子顺序想当然:子的 mounted 先于父,父组件 mounted 里可以安全访问子组件 DOM。
小结
顶层即创建阶段;onMounted 摸 DOM 发请求,onUnmounted 做清理;子先挂载父后挂载。下一章讲摸 DOM 的正规手段:模板引用。