讲解

组件从创建到销毁经历固定阶段:初始化(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 的正规手段:模板引用。