Skip to content

✨ 组件生命周期 👌

要点速览

  • 阶段:初始化 → 模板编译 → 挂载 → 更新 → 卸载;每个阶段有对应钩子。
  • Vue3 钩子:onBeforeMount/onMounted/onBeforeUpdate/onUpdated/onBeforeUnmount/onUnmounted;初始化阶段由 setup() 承担。
  • 本质:在渲染器生命周期的关键时点执行你注册的回调(钩子)。
  • 顺序(父子嵌套):父 onBeforeMount → 子 onBeforeMount → 子 onMounted → 父 onMounted(卸载同理递归)。
  • 清理:在卸载阶段释放计时器、监听器、资源;在 KeepAlive 场景使用 onActivated/onDeactivated 管理状态。

官方生命周期图:

阶段总览

这里分为这么几个大的阶段:

  1. 初始化选项式 API
  2. 模板编译
  3. 初始化渲染
  4. 更新组件
  5. 销毁组件

1. 初始化(选项式/组合式)

当渲染器遇到一个组件的时候,首先是初始化选项式 API,这里在内部还会涉及到组件实例的创建。

组件实例创建的前后,对应如下钩子:

  • 组件实例创建前:setup、beforeCreate
  • 组件实例创建后:created
初始化阶段详解
  • 选项解析与准备:读取选项对象,提取 propsdatarender 等配置,用于实例化与后续渲染。
  • 创建组件实例:建立运行期实例对象(记录 state/isMounted/subTree 等),并挂到 vnode.component,形成与虚拟节点的绑定关系。
  • 响应式状态初始化:
    • 选项式:调用 data() 后用响应式系统包裹,得到组件本地 state;供渲染与更新依赖追踪。
    • 组合式:执行 setup(props, context) 创建 ref/reactive/computed/watch 等逻辑;<script setup> 顶层声明在编译后自动暴露给模板。
  • setup 上下文:props(只读,含默认值与类型校验)、contextemit/slots/attrs/expose)。需要暴露实例方法时在此阶段调用 exposedefineExpose
  • 钩子触发顺序:先触发 beforeCreate(实例与状态尚未完全就绪),再准备好响应式 state 与实例后触发 created(此时可安全访问 state)。
  • 渲染入口就绪:建立包裹渲染的副作用与调度器,为后续“挂载/更新”阶段提供执行入口;此时不涉及真实 DOM 操作。

2. 模板编译

接下来会进入模板编译的阶段,当模板编译的工作结束后,会执行 beforeMount 钩子函数。

模板编译阶段详解
  • 触发时机:在 SFC 构建阶段(Vite/Vue 编译器)预编译;或在包含运行时编译器的构建中于运行期按需编译(学习/原型场景)。
  • 编译流程:
    • 解析:将模板解析为 AST(抽象语法树),识别元素、文本、插值、指令。
    • 转换:将指令与结构化语法(v-if/v-for/v-show/v-slot/v-model 等)转换为渲染所需的中间节点,进行插槽归一化、绑定归一化。
    • 代码生成:输出渲染函数 render(),使用虚拟节点工厂与 PatchFlags 标记可变边界,静态节点进行常量提升(hoist)。
  • 产物与作用:
    • 生成的 render() 将在挂载与更新阶段被调用以产出 VNode 子树。
    • PatchFlags 指示哪些属性/节点需要参与 diff,提高更新性能;静态提升减少重复创建。
    • 插槽在编译期被整理为函数对象集合;子组件在运行期按需调用。
  • 钩子衔接:模板编译完成后进入首次挂载流程,随后触发 onBeforeMount/beforeMount

3. 初始化渲染(挂载)

接下来是初始化渲染,到了这个阶段,意味着已经生成了真实的 DOM. 完成初始化渲染后会执行 mounted 生命周期方法。

4. 更新组件

更新组件时对应着一组生命周期钩子方法:

  • 更新前:beforeUpdate
  • 更新后:updated

5. 卸载组件

销毁组件时也对应一组生命周期钩子方法:

  • 销毁前:beforeUnmount
  • 销毁后:unmounted

一般在销毁组件时我们会做一些清理工作,例如清除计时器等操作。

名称映射与并存(Vue2 vs Vue3)

在 Vue3 中生命周期钩子名称有所更新,但与 Vue2 的钩子可并存(不建议混用):

生命周期阶段Vue2 钩子Vue3 钩子
初始化(创建前后)beforeCreate/createdsetup
挂载前/后beforeMount/mountedonBeforeMount/onMounted
更新前/后beforeUpdate/updatedonBeforeUpdate/onUpdated
卸载前/后beforeDestroy/destroyedonBeforeUnmount/onUnmounted

Vue2 与 Vue3 钩子可并存,但 Vue3 钩子通常比同阶段的 Vue2 钩子更早执行。实际项目中不建议混用,以免增加心智负担。

概念与本质

生命周期就是在合适的时机调用你注册的回调函数。

首先需要了解组件实例和组件挂载。假设用户书写了这么一个组件:

jsx
// 选项式 API
export default {
  name: "UserCard",
  props: {
    name: String,
    email: String,
    avatarUrl: String,
  },
  data() {
    return {
      foo: 1,
    };
  },
  mounted() {
    // ...
  },
  render() {
    return h("div", { class: styles.userCard }, [
      h("img", {
        class: styles.avatar,
        src: this.avatarUrl,
        alt: "User avatar",
      }),
      h("div", { class: styles.userInfo }, [
        h("h2", this.name),
        h("p", this.email),
      ]),
    ]);
  },
};

这些内容是一个组件选项对象(并不是组件实例对象)。渲染该组件时,框架会创建并维护一个组件实例对象组件实例对象本质上是一个记录组件运行期状态的对象,例如:

  • 注册到组件的生命周期钩子函数
  • 组件渲染的子树
  • 组件是否已经被挂载
  • 组件自身的状态
jsx
function mountComponent(vnode, container, anchor) {
  // 获取【组件选项对象】
  const componentOptions = vnode.type;
  // 从【组件选项对象】上面提取出 render 以及 data
  const { render, data } = componentOptions;

  // 创建响应式数据
  const state = reactive(data());

  // 定义【组件实例对象】,一个组件实例本质上就是一个对象,它包含与组件有关的状态信息
  const instance = {
    // 组件自身的状态数据,即 data
    state,
    // 一个布尔值,用来表示组件是否已经被挂载,初始值为 false
    isMounted: false,
    // 组件所渲染的内容,即子树(subTree)
    subTree: null,
  };

  // 将组件实例设置到 vnode 上,用于后续更新
  vnode.component = instance;

  // 后面逻辑略...
}

运行机制(挂载与更新)

jsx
function mountComponent(vnode, container, anchor) {
  // 前面逻辑略...

  effect(
    () => {
      // 调用组件的渲染函数,获得子树
      const subTree = render.call(state, state);
      // 检查组件是否已经被挂载
      if (!instance.isMounted) {
        // 初次挂载,调用 patch 函数第一个参数传递 null
        patch(null, subTree, container, anchor);
        // 重点:将组件实例的 isMounted 设置为 true,这样当更新发生时就不会再次进行挂载操作,
        // 而是会执行更新
        instance.isMounted = true;
      } else {
        // 当 isMounted 为 true 时,说明组件已经被挂载,只需要完成自更新即可,
        // 所以在调用 patch 函数时,第一个参数为组件上一次渲染的子树,
        // 意思是,使用新的子树与上一次渲染的子树进行打补丁操作
        patch(instance.subTree, subTree, container, anchor); // diff 计算
      }
      // 更新组件实例的子树
      instance.subTree = subTree;
    },
    { scheduler: queueJob }
  );
}

其核心是根据组件实例的 isMounted 属性判断是否初次挂载:

  • 初次挂载:patch 的第一个参数为 null;会设置组件实例 isMounted 为 true
  • 非初次挂载:更新组件的逻辑,patch 的第一个参数是组件上一次渲染的子树,从而和新的子树进行 diff 计算

所谓生命周期,就是在合适的时间执行用户传入的回调函数

jsx
function mountComponent(vnode, container, anchor) {
  const componentOptions = vnode.type;
  // 从【组件选项对象】中取得组件的生命周期函数
  const {
    render,
    data,
    beforeCreate,
    created,
    beforeMount,
    mounted,
    beforeUpdate,
    updated,
  } = componentOptions;

  // 拿到生命周期钩子函数之后,就会在下面的流程中对应的位置调用这些钩子函数

  // 在这里调用 beforeCreate 钩子
  beforeCreate && beforeCreate();
  // 创建响应式数据
  const state = reactive(data());
  // 创建组件实例对象
  const instance = {
    state,
    isMounted: false,
    subTree: null,
  };
  vnode.component = instance;

  // 组件实例已经创建
  // 此时在这里调用 created 钩子
  created && created.call(state);

  effect(
    () => {
      const subTree = render.call(state, state);
      if (!instance.isMounted) {
        // 初次挂载
        // 在这里调用 beforeMount 钩子
        beforeMount && beforeMount.call(state);
        patch(null, subTree, container, anchor); // 创建 DOM
        instance.isMounted = true;
        // 在这里调用 mounted 钩子
        mounted && mounted.call(state);
      } else {
        // 非初次挂载(更新)
        // 在这里调用 beforeUpdate 钩子
        beforeUpdate && beforeUpdate.call(state);
        patch(instance.subTree, subTree, container, anchor);
        // 在这里调用 updated 钩子
        updated && updated.call(state);
      }
      instance.subTree = subTree;
    },
    { scheduler: queueJob }
  );
}

在上面的代码中,首先从组件选项对象中获取到注册到组件上面的生命周期函数,然后内部会在合适的时机调用它们。

嵌套结构与调用顺序

组件之间是可以进行嵌套的,从而形成一个组件树结构。那么当遇到多组件嵌套的时候,各个组件的生命周期是如何运行的呢?

实际上非常简单,就是一个递归的过程。

假设 A 组件下面嵌套了 B 组件,那么渲染 A 的时候会执行 A 的 onBeforeMount,然后是 B 组件的 onBeforeMount,然后 B 正常挂载,执行 B 组件的 mounted,B 渲染完成后,接下来才是 A 的 mounted.

  1. 组件 A:onBeforeMount
  2. 组件 B:onBeforeMount
  3. 组件 B:mounted
  4. 组件 A:mounted
卸载递归

卸载流程与挂载相反,也遵循自顶向下递归:父 onBeforeUnmount → 子递归卸载 → 子 onUnmounted → 父 onUnmounted

实际开发注意事项

  • 生命周期钩子的执行顺序和时机

    • 初始化:setup() 先于任何 Options API 钩子(包括 beforeCreate/created)。
    • 首次挂载:onBeforeMount/beforeMount → 首次渲染 → onMounted/mounted;父子顺序为父 beforeMount → 子 beforeMount → 子 mounted → 父 mounted。
    • 更新:状态变化触发渲染副作用 → onBeforeUpdate/beforeUpdate → DOM diff → onUpdated/updated
    • 卸载:onBeforeUnmount/beforeUnmount → 清理副作用与资源 → onUnmounted/unmounted
    • 示例:在 onMounted 中安全访问 DOM。
      js
      const el = ref(null);
      onMounted(() => el.value && el.value.focus());
  • 常见错误使用场景和最佳实践

    • 错误:在 setup()beforeMount 访问真实 DOM。
      • 最佳实践:将 DOM 操作统一放在 onMounted/onUpdated,必要时配合 nextTick()
    • 错误:在 updated 钩子中直接修改响应式状态,导致更新循环。
      • 示例(错误):
        js
        onUpdated(() => {
          count.value++;
        });
      • 最佳实践:使用 watch 针对性响应或将副作用迁移到业务触发点。
    • 错误:直接解构 props 失去响应性。
      • 最佳实践:使用 toRefs/toRef 保持响应性。
        js
        const { options } = toRefs(props); // 正确
    • 错误:卸载时未清理资源。
      • 最佳实践:在 onUnmounted 清理计时器、事件、订阅与网络请求。
        js
        onMounted(() => window.addEventListener("resize", onResize));
        onUnmounted(() => window.removeEventListener("resize", onResize));
  • 性能优化相关注意事项

    • 避免在 mounted 阶段执行重计算或大量同步任务;必要时延迟到空闲时间。
      js
      onMounted(
        () => requestIdleCallback?.(heavyWork) ?? setTimeout(heavyWork, 0)
      );
    • 事件节流/防抖,减少频繁状态更新与重渲染。
      js
      const onScroll = throttle(() => {
        /* update */
      }, 200);
      onMounted(() => window.addEventListener("scroll", onScroll));
      onUnmounted(() => window.removeEventListener("scroll", onScroll));
    • computed 替代 watch 处理纯派生数据,减少冗余计算。
    • KeepAlive 场景使用 onActivated/onDeactivated 管理数据刷新与订阅,避免每次进入都重新初始化。
  • 与 Composition API 的结合使用要点

    • <script setup> 顶层声明自动暴露到模板;宏必须顶层调用:defineProps/defineEmits/defineExpose/defineSlots/defineModel
    • 仅在 onMounted/onUpdated 做 DOM 相关副作用;在 setup() 中声明状态与纯逻辑。
    • 父侧通过 ref 获取子组件实例时,使用 defineExpose 显式暴露方法与数据。
      js
      const reset = () => {
        /* ... */
      };
      defineExpose({ reset });