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>

七个阶段的执行顺序:

  1. setup:组件实例创建,响应式数据初始化。<script setup> 整体就是 setup。
  2. onBeforeMount:DOM 即将挂载,但还没生成。基本用不到。
  3. onMounted:DOM 已挂载到页面。最常用的钩子,90% 的初始化逻辑放这里。
  4. onBeforeUpdate:数据变化后,DOM 重渲染前。
  5. onUpdated:DOM 已完成重渲染。
  6. onBeforeUnmount:组件即将销毁。适合做最后的清理准备。
  7. 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>

哪些东西需要清理?

养成"有开就有关"的习惯: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 更新后做点什么,优先用:

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>

当你发现"某个组件莫名其妙重新渲染"时,用 onRenderTriggereddebugger,能精确找到"是哪个响应式数据变化触发的"。生产环境会自动移除,无性能影响。

9. 与 Vue 2 生命周期的对照

如果你看过 Vue 2 的资料,会注意到名字不一样。对照表:

小结

这一章你掌握了 Vue 组件从生到死的所有钩子。记两个最重要的:onMounted(初始化)和 onUnmounted(清理),它们配对出现就能避开 90% 的内存泄漏。下一篇进入工程化——用 Vue Router 做多页面应用。

← 上一篇 Vue Props 与 Emit

下一篇 Vue Router 路由

✈️💬