vue3源码解析:生命周期

本文,我们将核心流程再过一遍,重点关注所有生命周期钩子函数的执行时机,以及它们在不同场景下的执行顺序。

1. 生命周期钩子函数的执行时机

Vue3 的生命周期主要分为四个阶段,每个阶段都有对应的生命周期钩子:

初始化阶段

  • setup: 组件实例创建之前执行
  • beforeCreate: 实例初始化之前 (在 Composition API 中等同于 setup 开始)
  • created: 实例创建完成后 (在 Composition API 中等同于 setup 结束)

挂载阶段

  • beforeMount: DOM 挂载之前
  • mounted: DOM 挂载完成后

更新阶段

  • beforeUpdate: 数据更新时,DOM 更新之前
  • updated: DOM 更新完成后

卸载阶段

  • beforeUnmount: 组件卸载之前
  • unmounted: 组件卸载完成后

2. 组件初始化阶段

组件的生命周期从初始化开始。在这个阶段,Vue 会创建组件实例,设置响应式系统,并执行初始化相关的钩子函数。让我们详细看看这个阶段的实现:

2.1 setup

setup 是 Composition API 的入口,也是整个组件生命周期中最早执行的钩子。它在组件实例创建之前执行,这使得我们可以在这里进行一些预处理工作,比如创建响应式数据、注册生命周期钩子等。具体实现如下:

js 复制代码
// 调用链:
// createApp -> mount -> render -> patch -> processComponent ->
// 在 setupComponent 函数中
if (setup) {
  const setupContext = (instance.setupContext =
    setup.length > 1 ? createSetupContext(instance) : null);

  setCurrentInstance(instance);
  pauseTracking();
  const setupResult = callWithErrorHandling(
    setup,
    instance,
    ErrorCodes.SETUP_FUNCTION,
    [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
  );
  resetTracking();
  unsetCurrentInstance();
}

实际应用示例:

js 复制代码
// 在 setup 中初始化数据和方法
export default {
  setup() {
    // 1. 创建响应式数据
    const count = ref(0);
    const userInfo = reactive({
      name: "",
      age: 0,
    });

    // 2. 定义方法
    const increment = () => count.value++;

    // 3. 注册其他生命周期钩子
    onMounted(() => {
      // 组件挂载后的逻辑
    });

    return { count, userInfo, increment };
  },
};

2.2 beforeCreate & created

在 Vue3 的 Composition API 中,这两个钩子实际上等同于 setup 函数:

  • beforeCreate: 在 setup 开始时
  • created: 在 setup 结束时

如果使用 Options API,这两个钩子会在组件实例初始化时执行:

js 复制代码
// 调用链:
// createApp -> mount -> render -> patch -> processComponent ->
// mountComponent -> setupComponent -> setupStatefulComponent -> applyOptions

// componentOptions.ts
function applyOptions(instance: ComponentInternalInstance) {
  const options = resolveMergedOptions(instance);
  const publicThis = instance.proxy! as any;
  const ctx = instance.ctx;

  // 1. 处理生命周期钩子
  if (options.beforeCreate) {
    callHook(options.beforeCreate, instance, LifecycleHooks.BEFORE_CREATE);
  }

  // ... 其他选项的处理

  if (options.created) {
    callHook(options.created, instance, LifecycleHooks.CREATED);
  }
}

// 生命周期钩子的枚举定义
export const enum LifecycleHooks {
  BEFORE_CREATE = "bc",
  CREATED = "c",
  BEFORE_MOUNT = "bm",
  MOUNTED = "m",
  BEFORE_UPDATE = "bu",
  UPDATED = "u",
  BEFORE_UNMOUNT = "bum",
  UNMOUNTED = "um",
  DEACTIVATED = "da",
  ACTIVATED = "a",
  RENDER_TRIGGERED = "rtg",
  RENDER_TRACKED = "rtc",
  ERROR_CAPTURED = "ec",
}

// 钩子函数的调用实现
function callHook(
  hook: Function,
  instance: ComponentInternalInstance,
  type: LifecycleHooks
) {
  callWithAsyncErrorHandling(hook.bind(instance.proxy), instance, type);
}

钩子函数的执行过程:

  1. beforeCreate:

    • 在 applyOptions 开始时执行
    • 此时组件实例已创建,但还未进行其他选项的处理
    • 不能访问 data、computed 等选项
  2. created:

    • 在其他选项处理完成后执行
    • 此时已完成响应式数据、计算属性等的初始化
    • 可以访问组件的所有选项

在 Composition API 中,这两个钩子的功能被 setup 函数取代,因为:

  1. setup 函数提供了更灵活的组合式 API
  2. 可以更好地组织和复用逻辑
  3. 提供了更好的类型推导

实际应用示例:

js 复制代码
export default {
  // Options API
  beforeCreate() {
    // 这里不能访问 data、computed 等
    console.log("组件实例还未完全创建");
  },
  created() {
    // 1. 可以访问响应式数据
    console.log(this.someData);

    // 2. 发起初始化数据请求
    this.fetchInitialData();

    // 3. 添加全局事件监听
    window.addEventListener("resize", this.handleResize);
  },
};

3. 挂载和更新阶段

当组件实例创建完成后,Vue 会进入挂载和更新阶段。这两个阶段的生命周期钩子都在 setupRenderEffect 函数中实现:

3.1 beforeMount

在 setupRenderEffect 函数中,组件挂载前会执行:

js 复制代码
// createAppAPI -> mount -> render -> patch -> processComponent ->
// mountComponent -> setupComponent -> setupRenderEffect

const setupRenderEffect = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  namespace,
  optimized
) => {
  const componentUpdateFn = () => {
    if (!instance.isMounted) {
      // 挂载阶段
      // 1. 执行 beforeMount 钩子
      const { bm } = instance;
      if (bm) {
        invokeArrayFns(bm);
      }

      // 2. 渲染组件
      const subTree = (instance.subTree = renderComponentRoot(instance));

      // 3. 挂载子树
      patch(
        null,
        subTree,
        container,
        anchor,
        instance,
        parentSuspense,
        namespace
      );
      instance.isMounted = true;

      // 4. 执行 mounted 钩子(异步)
      const { m } = instance;
      if (m) {
        queuePostRenderEffect(m, parentSuspense);
      }
    } else {
      // 更新阶段
      // 1. 执行 beforeUpdate 钩子
      const { bu } = instance;
      if (bu) {
        invokeArrayFns(bu);
      }

      // 2. 更新组件
      const nextTree = renderComponentRoot(instance);
      const prevTree = instance.subTree;
      instance.subTree = nextTree;

      // 3. 更新 DOM
      patch(
        prevTree,
        nextTree,
        container,
        anchor,
        instance,
        parentSuspense,
        namespace
      );

      // 4. 执行 updated 钩子(异步)
      const { u } = instance;
      if (u) {
        queuePostRenderEffect(u, parentSuspense);
      }
    }
  };

  // 创建渲染副作用
  instance.update = effect(componentUpdateFn, {
    scheduler: () => queueJob(instance.update),
  });
};

特点:

  1. 同步钩子(beforeMount/beforeUpdate)在 DOM 操作前执行,可以访问更新前的状态
  2. 异步钩子(mounted/updated)通过 queuePostRenderEffect 调度,确保在 DOM 更新后执行
  3. 整个过程由 effect 包装,响应式数据变化时自动触发更新流程

实际应用示例:

js 复制代码
export default {
  setup() {
    onBeforeMount(() => {
      // 1. DOM 渲染前的准备工作
      initializeThirdPartyLib();
    });

    onMounted(() => {
      // 1. 访问或操作 DOM
      const element = document.getElementById("my-element");

      // 2. 初始化第三方库
      new ThirdPartyChart(element);

      // 3. 发起数据请求
      fetchData();
    });

    onBeforeUpdate(() => {
      // 1. 更新前保存一些状态
      saveScrollPosition();
    });

    onUpdated(() => {
      // 1. DOM 更新后恢复状态
      restoreScrollPosition();

      // 2. 更新第三方库的视图
      updateChartView();
    });
  },
};

4. 卸载阶段

当组件被卸载时(比如通过 v-if 移除组件或手动调用 unmount),Vue 会执行一系列清理工作。让我们看看卸载阶段的生命周期钩子实现:

js 复制代码
// unmount 调用链:
// 1. 组件被移除 -> render(null, container)
// 2. patch(prevVNode, null, ...)
// 3. unmount(prevVNode, ...)
// 4. unmountComponent(...)

const unmountComponent = (
  instance: ComponentInternalInstance,
  parentSuspense: SuspenseBoundary | null,
  doRemove?: boolean
) => {
  const { bum, scope, update, subTree, um } = instance;

  // 1. 执行 beforeUnmount 钩子
  if (bum) {
    invokeArrayFns(bum);
  }

  // 2. 停止响应式副作用
  scope.stop();
  if (update) {
    update.active = false;
  }

  // 3. 卸载子树
  unmount(subTree, instance, parentSuspense, doRemove);

  // 4. 执行 unmounted 钩子(异步)
  if (um) {
    queuePostRenderEffect(um, parentSuspense);
  }
};

特点:

  1. 卸载过程是自上而下的,先处理父组件,再处理子组件
  2. beforeUnmount 是同步执行的,确保在 DOM 移除前有机会进行清理
  3. unmounted 是异步执行的,确保所有子组件都完成卸载
  4. 完整的清理机制确保不会产生内存泄漏

实际应用示例:

js 复制代码
export default {
  setup() {
    // 1. 创建需要清理的资源
    const timer = setInterval(() => {
      // 定时任务
    }, 1000);

    const unsubscribe = store.subscribe(() => {
      // 状态订阅
    });

    onBeforeUnmount(() => {
      // 1. 清除定时器
      clearInterval(timer);

      // 2. 取消状态订阅
      unsubscribe();

      // 3. 移除事件监听
      window.removeEventListener("resize", handleResize);
    });

    onUnmounted(() => {
      // 1. 销毁第三方库实例
      chart.destroy();

      // 2. 清理缓存数据
      localStorage.removeItem("tempData");
    });
  },
};

4. 总结

Vue3 的生命周期钩子执行顺序如下:

初始化阶段

  1. setup (最早执行)
  2. beforeCreate (等同于 setup 开始)
  3. created (等同于 setup 结束)

首次挂载

  1. beforeMount (同步)
  2. mounted (异步)

更新过程

  1. beforeUpdate (同步)
  2. updated (异步)

卸载过程

  1. beforeUnmount (同步)
  2. unmounted (异步)

特点:

  1. 每个阶段都有组件级和 vnode 级的钩子函数
  2. beforeXXX 钩子都是同步执行的
  3. 其他钩子都是通过 queuePostRenderEffect 异步执行的
  4. 钩子函数的执行顺序是确定的,有助于我们控制代码逻辑
  5. setup 是一个特殊的钩子,它在组件实例创建之前执行
  6. Composition API 中 beforeCreate 和 created 被 setup 替代
  7. 特殊钩子针对特定场景提供了额外的生命周期控制
  8. 开发环境提供了更多的调试钩子

这种设计让我们能够在组件生命周期的不同阶段执行需要的逻辑,同时通过同步/异步执行的区分,保证了性能和正确性。

相关推荐
小飞悟5 分钟前
一打开文章就弹登录框?我忍不了了!
前端·设计模式
烛阴12 分钟前
Python模块热重载黑科技:告别重启,代码更新如丝般顺滑!
前端·python
midsummer_woo30 分钟前
基于springboot+vue+mysql工程教育认证的计算机课程管理平台(源码+论文)
vue.js·spring boot·mysql
吉吉611 小时前
Xss-labs攻关1-8
前端·xss
拾光拾趣录1 小时前
HTML行内元素与块级元素
前端·css·html
小飞悟1 小时前
JavaScript 数组精讲:创建与遍历全解析
前端·javascript
喝拿铁写前端1 小时前
技术是决策与代价的平衡 —— 超大系统从 Vue 2 向 Vue 3 演进的思考
前端·vue.js·架构
拾光拾趣录2 小时前
虚拟滚动 + 加载:让万级列表丝般顺滑
前端·javascript
然我2 小时前
数组的创建与遍历:从入门到精通,这些坑你踩过吗? 🧐
前端·javascript·面试
豆豆(设计前端)2 小时前
如何成为高级前端开发者:系统化成长路径。
前端·javascript·vue.js·面试·electron