超实用!一文读懂 Vue2 - Vue3 组件生命周期及差异对比

518 阅读12分钟

Vue 生命周期作为 Vue 框架的核心概念之一,对于开发者理解和掌控组件的运行过程起着至关重要的作用。深入了解 Vue 生命周期,能够帮助开发者在合适的时机执行特定操作,从而优化应用的性能和交互体验。

一、Vue 实例基础

Vue 实例是什么

Vue 实例是 Vue 应用的核心载体,本质上是通过 Vue 构造函数创建出来的一个对象。它管理着数据响应式、模板渲染以及与 DOM 的交互逻辑。

每个 Vue 应用至少有一个根实例,通过配置选项(如 datamethods)定义其行为。在实际开发中,Vue 实例会将 JavaScript 数据与 HTML 模板结合,最终渲染到页面指定的 DOM 元素上。

在 Vue 2 中,使用 new Vue() 构造函数创建实例,而在 Vue 3 里,则是通过 createApp 函数来创建。

Vue 2 创建实例

在 Vue 2 中,通过 new Vue() 构造函数创建实例,并传入配置对象:

const Vue = require('vue');  
const app = new Vue({  
    data: {  
        message: 'Hello, Vue 2!'  
    },  
    methods: {  
        sayHello() {  
            console.log(this.message);  
        }  
    }  
});  

 el 选项与挂载

el 是 Vue 2 实例的一个关键选项,用于指定实例挂载的 DOM 元素,值可以是 CSS 选择器(如 '#app')或 DOM 元素对象。

  • 直接挂载

    const app = new Vue({  
        el: '#app', // 实例创建时直接挂载到 id="app" 的元素  
        data: {  
            message: 'Direct mount'  
        }  
    });  
    
  • 延迟挂载

    若创建实例时未指定 el,可通过 $mount 方法手动挂载:

    const app = new Vue({  
        data: {  
            message: 'Delayed mount'  
        }  
    });  
    app.$mount('#app'); // 调用 $mount 后实例才挂载到 DOM  
    

Vue 3 创建实例

Vue 3 采用了组合式 API,通过 createApp 函数来创建应用实例。示例如下:

import { createApp, ref } from 'vue';  
const app = createApp({  
    setup() {  
        const message = ref('Hello, Vue 3!');  
        const sayHello = () => {  
            console.log(message.value);  
        };  
        return {  
            message,  
            sayHello  
        };  
    }  
});  

Vue 3 中无 el 选项,需通过 mount 方法指定挂载目标:

app.mount('#app'); // 将实例挂载到 id="app" 的元素  

在 Vue 3 里,createApp 函数返回一个应用实例,之后可以使用 mount 方法将其挂载到 DOM 元素上。

二、Vue 生命周期概述

Vue 生命周期指的是 Vue 实例从创建、初始化、挂载到 DOM、数据更新,再到卸载销毁的这一系列过程。在这个过程中的不同阶段,Vue 提供了对应的钩子函数,方便开发者在特定阶段执行自定义代码。

image.png

图片来源:官方文档

从图中可以清晰地看到 Vue 实例从开始创建到最终销毁所经历的各个关键节点。

什么是生命周期钩子

生命周期钩子是 Vue 在组件实例从创建到销毁的各个阶段自动调用的特殊函数。它就像是在 Vue 实例生命周期的特定时刻设置的 “钩子点”,开发者可以将自定义代码 “挂” 在这些点上,让代码在对应的阶段执行。

钩子的本质与原理

从本质上讲,Vue 内部维护了一套状态管理机制,在组件实例的不同状态变化时,会去检查是否有开发者定义的对应钩子函数。如果有,就执行这些函数。

  • 例如,当组件准备将模板渲染为真实 DOM 时,Vue 会检查是否定义了onBeforeMount钩子函数,如果定义了,就会调用该函数。

三、Vue2 生命周期

(一)创建阶段

Vue 实例的创建阶段是从实例被初始化到挂载到 DOM 之前的过程,核心目的是完成实例的配置解析与数据初始化。

beforeCreate

  • 在 Vue 实例刚被创建时,beforeCreate 钩子函数会被调用。

    • 此时,Vue 实例仅完成了最基础的初始化工作,像实例的一些内部属性和方法被创建,但实例的 datacomputedwatchmethods 等选项还未进行初始化。

    • 由于上述选项尚未初始化,所以无法访问 data 里的数据,也不能调用 methods 中的方法。同时,由于还未进行数据劫持,数据的响应式系统也未启动。

  • 此阶段可执行的操作有限,但在某些特殊场景下,它可用于添加全局配置。

created

当 Vue 实例完成数据劫持(通过 Object.defineProperty 或 Proxy 实现响应式转换)以及属性和方法的初始化后,created钩子函数被触发。

  • 此阶段可通过this访问以下内容:

    • 组件内部:
      • data:已完成响应式处理,数据可进行读写操作。
      • methods:组件内定义的方法,可以直接调用。
      • computed:基于响应式数据的计算属性已完成初始化,访问时会自动触发依赖收集。
      • watch:监听器已完成注册,虽不能直接调用监听器函数,但修改被监听的数据会触发相应回调。
    • 依赖注入:
      • 通过 this 访问以 inject 选项声明的依赖,从而直接获取父组件或祖先组件经 provide 提供的值。
    • 全局扩展:
      • 当使用全局混入(Vue.mixin)或者插件(Vue.use)时,它们所添加的属性和方法会合并到当前组件实例中,此时可通过 this 来访问这些属性和方法。
    • 内置功能:
      • $options:可用于访问组件的原始选项配置,例如this.$options.propsData 能够获取 props 的初始值。
      • $emit:用于触发自定义事件。
      • 其他实例方法:通过 $root 访问根实例,借助 $parent 访问父组件实例等。
  • 此阶段需要注意的:

    • 无法操作 DOM:组件尚未挂载到真实 DOM 树,this.$el 为 undefined,直接操作 DOM(如 document.getElementById)会报错,需在 mounted 钩子中进行。

    • 异步操作无法依赖 DOM 状态:例如在 created 中发起异步请求并试图根据返回数据更新 DOM 时,因 DOM 未渲染,无法确保更新效果正确。

(二)挂载阶段

Vue 实例的挂载阶段,是指从模板编译完成到实例被渲染并插入真实 DOM 树的过程。在此阶段,Vue 会将编译好的模板转化为真实的 DOM 元素,并将其插入到指定的挂载点。

beforeMount

当 Vue 实例准备将编译好的模板挂载到真实 DOM 时,beforeMount钩子函数被调用。此时:

  • 模板状态:模板已完成编译,生成了虚拟 DOM 树,但尚未被渲染为真实的 DOM 元素并插入到页面中。

  • 可操作内容:在此阶段,可以对数据或其他实例选项进行最后的调整。因为此时对数据的修改仍会触发虚拟 DOM 的重新计算与更新,最终反映到真实 DOM 上。

    • 对于数据,可检查准确性(如缺失、类型错误)、调整排序(升/降序等)或调整关联数据关系。
    • 对于实例选项,可以检查各项配置是否符合业务需求,比如某些功能的开启或关闭状态、参数的设置等,若不合适则进行调整。
  • 不可操作内容:由于真实 DOM 还未挂载,使用this.$el访问 DOM 元素时,它可能只是一个注释节点或者未渲染的占位符,不能进行实际的 DOM 操作。

mounted

当 Vue 实例成功将模板渲染为真实 DOM 元素,并插入到文档中指定的挂载点后,mounted钩子函数被触发。此时:

  • DOM 状态:实例已经挂载到真实 DOM 树,this.$el指向了挂载的 DOM 元素。

  • 可操作内容

    • 可执行DOM 操作:例如获取特定元素的尺寸或位置:
    export default {
        mounted() {
            const element = document.getElementById('my - element');
            console.log('元素宽度:', element.offsetWidth);
        }
    };
    
    • 第三方插件初始化:许多第三方插件(如轮播图插件 swiper、图表插件 Echarts 等)需要在 DOM 元素渲染完成后进行初始化。

    • 数据请求 :虽然在created阶段也可以发起数据请求,但在mounted阶段进行数据请求,有时可以确保在 DOM 渲染完成后,根据数据更新页面时不会出现闪烁或布局抖动的情况,尤其是当数据展示依赖特定的 DOM 结构时。

  • 注意:在mounted钩子中进行异步操作时,要注意避免频繁触发重绘和回流。

    • 如果在mounted中通过定时器不断修改 DOM 元素的样式,可能会导致性能问题。可以考虑使用requestAnimationFrame等优化手段来批量处理 DOM 更新。

(三)更新阶段

Vue 实例的更新阶段,是指当响应式数据发生变化时,从虚拟 DOM 重新计算到真实 DOM 更新完成的过程。

beforeUpdate

当 Vue 检测到响应式数据(如 data 中的属性)发生变化,且即将重新渲染组件时,beforeUpdate 钩子函数被调用。此时:

  • 数据与 DOM 状态

    • 数据已更新为最新值,但真实 DOM 尚未同步更新,仍保留着上一次渲染的状态;
    • 虚拟 DOM 正在重新计算差异(Diff 算法),尚未应用到真实 DOM。
  • 可执行操作

    • 数据校验:可在数据更新前验证新值是否符合业务规则。例如,表单组件中校验输入值是否合法,若不合法可回滚数据:
    export default {
      data() {
        return {
          inputValue: ''
        };
      },
      beforeUpdate() {
        if (this.inputValue.length > 10) {
          // 限制输入长度,超出则恢复原值
          this.inputValue = this.inputValue.slice(0, 10);
        }
      }
    };
    
    • 状态记录:记录更新前的 DOM 状态(如元素样式、滚动位置),便于后续对比或恢复。
  • 注意
    虽然在该阶段可以访问更新前的 DOM,但要避免直接修改 DOM。因为此时修改会被 Vue 在下一轮渲染时覆盖,可能导致性能浪费或逻辑冲突。

updated

当 Vue 完成虚拟 DOM 差异计算,并将更新应用到真实 DOM 后,updated 钩子函数被触发。此时:

  • DOM 状态:真实 DOM 已完全同步为最新数据对应的结构和内容,this.$el 指向更新后的 DOM 元素。

  • 可执行操作

    • 进行 DOM 操作:例如,根据更新后的 DOM 重新初始化插件、获取动态元素尺寸:
    export default {
      data() {
        return {
          showChart: false
        };
      },
      methods: {
        initChart() {
          // 使用第三方图表库(如 ECharts)初始化图表
          const chartDom = this.$el.querySelector('.chart-container');
          // ...
        }
      },
      updated() {
        if (this.showChart) {
          this.initChart();
        }
      }
    };
    
    • 异步操作:在 DOM 更新后执行延迟任务(如动画触发)。
  • 注意事项

    • 避免循环更新:不要在 updated 钩子内修改响应式数据,否则会再次触发 beforeUpdate 和 updated,导致死循环。

    • 性能考量:频繁在 updated 中操作 DOM 可能影响性能,建议合并多次更新或使用 nextTick 批量处理。

(四)销毁阶段

Vue 实例的销毁阶段,是指从实例主动或被动触发销毁指令,到所有关联资源被释放的过程。

beforeDestroy

触发时机:

  • 手动调用销毁方法:当在组件内部调用 this.$destroy() 方法时,beforeDestroy 钩子会立即触发,这通常用于手动控制组件的销毁过程。

  • 父组件销毁:若父组件被销毁,其所有子组件也会随之被销毁。

  • 路由切换:在使用 Vue Router 进行路由切换时,如果当前组件所在的路由被切换到其他路由,且该组件不在新路由的渲染范围内,那么该组件会被销毁,进而触发 beforeDestroy 钩子。

  • 动态组件移除:使用动态组件(如 <component :is="currentComponent"></component>)时,当 currentComponent 的值改变,旧组件会被卸载,此时旧组件的 beforeDestroy 钩子会被触发。

  • 条件渲染变更:当使用 v-if 指令控制组件的显示与隐藏时,若条件变为 false,组件会从 DOM 中移除并销毁。

此时:

  • 实例状态

    • 实例尚未真正销毁,仍可访问所有属性(如 datamethods等)和生命周期钩子。
    • 但 Vue 已开始停止响应式系统的追踪,新的数据变化不会触发更新。
  • 必做操作

    • 资源清理:手动清除定时器(setInterval/setTimeout)、WebSocket 连接、第三方库实例等。例如:
    export default {
      data() {
        return {
          timerId: null,
          socket: null
        };
      },
      created() {
        this.timerId = setInterval(() => {
          console.log('定时任务');
        }, 1000);
        this.socket = new WebSocket('ws://example.com');
      },
      beforeDestroy() {
        clearInterval(this.timerId); // 清除定时器
        this.socket.close(); // 关闭 WebSocket 连接
      }
    };
    
    • 事件解绑:移除自定义或 DOM 事件监听器,避免内存泄漏。例如:
    export default {
      mounted() {
        window.addEventListener('resize', this.handleResize);
      },
      beforeDestroy() {
        window.removeEventListener('resize', this.handleResize);
      },
      methods: {
        handleResize() {
          // 窗口 resize 逻辑
        }
      }
    };
    
  • 可执行操作
    可进行数据备份或状态上报,但避免修改响应式数据(因即将销毁,修改无意义)。

destroyed

当 Vue 完成实例销毁流程,包括移除所有子组件、解绑事件、停止响应式追踪后,destroyed 钩子函数被触发。此时:

  • 实例状态

    • 实例已彻底销毁,所有与 Vue 相关的资源(如 datacomputed)均不可访问。
    • 但 DOM 元素仍保留在文档中(除非通过其他逻辑手动移除)。
  • 典型用途

    • 日志记录:记录组件销毁时间或状态,用于调试或监控。
    export default {
      destroyed() {
        console.log('组件已销毁');
        // 可上报至监控系统
        // analytics.track('ComponentDestroyed', { name: 'MyComponent' });
      }
    };
    
    • 手动 DOM 清理(可选) :若组件创建了额外的 DOM 元素(如弹窗、临时容器),可在此移除:
    export default {
      data() {
        return {
          customDiv: null
        };
      },
      mounted() {
        this.customDiv = document.createElement('div');
        document.body.appendChild(this.customDiv);
      },
      destroyed() {
        if (this.customDiv) {
          this.customDiv.parentNode.removeChild(this.customDiv);
        }
      }
    };
    
  • 注意事项
    避免访问实例属性(如 this.data)或调用实例方法(如 this.methods),因为实例已失效,可能导致报错。

四、Vue3 生命周期

(一)初始化阶段

Vue3 的初始化阶段以 setup 函数为核心入口,替代了 Vue2 中 beforeCreate 和 created 的功能,同时新增了 onRenderTracked 和 onRenderTriggered 用于调试响应式依赖。

setup

setup 函数在组件创建之初执行,早于其他生命周期钩子,是组合式 API 的起点。

  • 特性

    • 无法访问 this(需通过 getCurrentInstance 获取实例上下文)。
    • 可用 refreactive 创建响应式数据,watch 监听变化,computed 定义计算属性。
    • 接收 props 和 context 参数,用于获取组件属性和触发事件。
import { ref, onMounted } from 'vue';  
export default {  
  setup(props, { emit }) {  
    const count = ref(0);  
    const increment = () => {  
      count.value++;  
    };  
    onMounted(() => {  
      console.log('组件已挂载');  
    });  
    return {  
      count,  
      increment  
    };  
  }  
};  

(二)挂载阶段

Vue3 的挂载钩子名称调整为 onBeforeMount 和 onMounted,逻辑与 Vue2 对应钩子相似,但需通过组合式 API 调用。

onBeforeMount

在模板即将渲染为真实 DOM 前触发,可用于最后阶段的数据修正。

  • 操作场景

    • 动态修改响应式数据(仍会触发虚拟 DOM 的重新计算);
    • 检查异步加载的资源是否准备就绪。

onMounted

组件成功挂载到 DOM 后触发,可操作真实 DOM 或初始化第三方库。

import { onMounted } from 'vue';  
export default {  
  setup() {  
    onMounted(() => {  
      const el = document.getElementById('my-element');  
      console.log('元素宽度:', el.offsetWidth);  
    });  
    return {};  
  }  
};  

(三)更新阶段

更新钩子同样调整为 onBeforeUpdate 和 onUpdated,需结合组合式 API 使用。

onBeforeUpdate

数据更新导致组件重新渲染前触发,可用于记录旧状态。

import { ref, onBeforeUpdate } from 'vue';  
export default {  
  setup() {  
    const text = ref('初始文本');  
    onBeforeUpdate(() => {  
      const oldText = text.value;  
      console.log(`即将从 "${oldText}" 更新`);  
    });  
    return { text };  
  }  
};  

onUpdated

DOM 更新完成后触发,可执行依赖最新 DOM 的操作。

  • 注意事项:避免在此期间修改响应式数据,防止循环更新。

(四)卸载阶段

Vue3 将销毁逻辑聚焦于组件从 DOM 中移除的过程,使用 onBeforeUnmount 和 onUnmounted 替代 Vue2 的 beforeDestroy 和 destroyed

onBeforeUnmount

组件即将从 DOM 中卸载时触发,用于清理资源。

  • 必做操作

    • 清除定时器( clearInterval )。
    • 解绑 DOM 事件( removeEventListener )。
    • 销毁第三方实例(如 ECharts 图表)。

onUnmounted

当组件成功从 DOM 移除后,onUnmounted 钩子会被触发。此时所有相关的响应式作用已停止,响应式数据修改不会触发更新,计算属性和侦听器也自动清理完毕。

这是由于 Vue 自动清理了渲染副作用(如虚拟 DOM 绑定),使得 DOM 事件监听器和指令失效,属性修改不再触发 DOM 更新,组件也不再监听 DOM 事件,之前绑定的 DOM 操作和指令均不再起作用。

  • 用途

    • 必须操作:手动清除非响应式资源,如定时器、自定义 DOM 事件、WebSocket 连接等,以此避免内存泄漏。
    • 建议操作:释放内存,例如清空全局变量引用;记录组件卸载日志,方便后续调试和监控。
    • 避免操作:不要修改响应式数据或调用已清理的侦听器,因为此时这些操作不会产生预期的效果。

(五)其他生命周期钩子

onErrorCaptured

  • 触发时机:当捕获到一个来自子孙组件的错误时被调用。它可以捕获到组件渲染期间、生命周期钩子函数执行期间以及 watch 回调函数执行期间抛出的错误。

  • 用途:在组件内部捕获子孙组件抛出的错误,进行统一的错误处理,避免错误直接导致整个应用崩溃。同时可以记录错误日志,方便后续调试。

  • 返回值:可以返回 false 以阻止错误继续向上传播。

onActivated / onDeactivatedkeep-alive 专用)

  • 触发时机

    • onActivated:被 keep-alive 缓存的组件重新激活时调用;
    • onDeactivated:组件进入缓存时调用。
  • 应用场景

    • 缓存表单组件时保存输入状态;
    • 暂停 / 恢复组件内的动画或定时器。

onRenderTracked / onRenderTriggered(调试专用)

  • 用途:追踪响应式数据的依赖收集和触发情况,便于排查性能问题。

    • onRenderTracked :组件渲染期间,当响应式数据的依赖被收集时触发,借助它能清晰知晓组件在渲染时依赖了哪些响应式数据,帮助找出可能因依赖不必要数据而导致的频繁渲染问题。
    • onRenderTriggered :响应式数据发生变化,从而触发组件重新渲染时被调用,通过它可以明确是哪些响应式数据的变化引发了组件的重新渲染,帮助排查组件频繁重新渲染的性能问题。
    import { onRenderTracked, onRenderTriggered } from 'vue';  
    export default {  
      setup() {  
        const state = { count: 0 };  
        onRenderTracked((event) => {  
          console.log('依赖被追踪:', event);  
        });  
        onRenderTriggered((event) => {  
          console.log('依赖被触发:', event);  
        });  
        return { state };  
      }  
    };  
    
  • 仅用于调试 onRenderTracked / onRenderTriggered 是专门为调试设计的钩子,不应该在生产环境中使用。因为它会增加额外的性能开销,并且在生产环境中我们通常不需要这些调试信息。

onServerPrefetch

  • 触发时机:在服务端渲染期间,组件实例在服务器上被创建后,渲染之前调用。此钩子只会在服务端渲染时触发,在客户端不会触发。

  • 用途:适合在服务端获取数据,以便在渲染组件之前就把数据准备好。这样可以避免在客户端再进行额外的数据请求,有助于提升首屏加载速度,并且对搜索引擎优化(SEO)也有好处。

  • 返回值要求:该钩子需要返回一个 Promise,服务端会等待这个 Promise 完成后才会继续渲染组件。

五、父子组件生命周期执行顺序

(一)加载渲染过程

父组件 setup -> 父组件 onBeforeMount -> 子组件 setup -> 子组件 onBeforeMount -> 子组件 onMounted -> 父组件 onMounted

父组件的 setup 在父子组件加载渲染时,首先会执行父组件的 setup(在 setup 里逻辑开始执行),接着执行父组件的 onBeforeMount,然后进入子组件的加载渲染流程。子组件会先执行 setup,接着依次执行 onBeforeMountonMounted。最后,父组件才会执行 onMounted

这种顺序的原因在于,父组件在创建和挂载的过程中,需要先确定自身的基本状态,然后子组件才能基于父组件传递的一些信息(如 props 等)进行自身的创建和挂载。

在实际应用场景中,比如我们在父组件 setup 里进行数据初始化,然后传递给子组件,子组件可以在自己合适的生命周期钩子中接收并处理这些数据。

(二)更新过程

父组件 onBeforeUpdate -> 子组件 onBeforeUpdate -> 子组件 onUpdated  -> 父组件 onUpdated

当父组件或子组件的数据发生变化时,生命周期钩子函数的执行顺序为:如果是父组件数据变化,会先触发父组件的 onBeforeUpdate,然后是子组件的 onBeforeUpdate,接着子组件 onUpdated,最后父组件 onUpdated

在实际开发中,我们需要根据这个顺序来协调父子组件的操作。

  • 例如,当父组件传递给子组件的 props 数据发生变化时,我们可能需要在子组件的 onBeforeUpdate 钩子函数中对即将更新的数据进行一些预处理,以保证组件的正常渲染和功能实现。

(三)卸载过程

父组件 onBeforeUnmount -> 子组件 onBeforeUnmount -> 子组件 onUnmounted -> 父组件 onUnmounted

在卸载阶段,父子组件都需要进行资源清理。尤其是当父子组件中存在一些相互关联的资源(如共同绑定的事件等)时,要按照这个顺序确保资源被正确清理,避免出现内存泄漏等问题。

六、常见问题

在哪个生命周期调用异步请求?

在 Vue 3 中,调用异步请求(如接口数据获取)通常推荐使用 onMounted 或 onServerPrefetch 生命周期钩子。

  • onMounted:适合客户端渲染场景,组件挂载到 DOM 后触发,常用于获取初始数据;
  • onServerPrefetch:仅在服务端渲染(SSR)时生效,在组件即将被渲染到客户端前执行,可优化首屏加载性能。

keep-alive 缓存组件时,生命周期如何变化?

  • 首次加载:onBeforeCreate → setup() → onBeforeMount → onMounted → onActivated

  • 离开并缓存:触发 onDeactivated,不触发的钩子: onBeforeUnmountonUnmounted

  • 重新激活:触发 onActivated,组件状态保持缓存时的值。

  • 销毁(父组件卸载或缓存被清除):onDeactivated → onBeforeUnmount → onUnmounted

Vue 2 与 Vue 3 生命周期钩子的主要区别是什么?

Vue 2 生命周期钩子Vue 3 对应钩子变化类型使用方式备注
beforeCreatebeforeCreate保留Options API 中直接定义Composition API 中不再需要,setup() 替代了 beforeCreate 和 created 的逻辑。
createdcreated保留Options API 中直接定义在 Composition API 中,逻辑应写在 setup() 函数中。
beforeMountonBeforeMount重命名Composition API 中通过 import { onBeforeMount } from 'vue' 并在 setup() 调用功能相同,但以函数形式调用。
mountedonMounted重命名同上同上
beforeUpdateonBeforeUpdate重命名同上同上
updatedonUpdated重命名同上同上
beforeDestroyonBeforeUnmount重命名同上语义更清晰,强调“卸载”而非“销毁”。
destroyedonUnmounted重命名同上同上
errorCapturedonErrorCaptured重命名同上功能相同,捕获子孙组件错误。
activated(keep-alive 专用)onActivated重命名同上用于 keep-alive 缓存的组件激活时。
deactivated(keep-alive 专用)onDeactivated重命名同上用于 keep-alive 缓存的组件失活时。
-onServerPrefetch新增Composition API 中通过 import { onServerPrefetch } from 'vue' 调用服务端渲染(SSR)专用,用于在服务端预取数据。
-onRenderTracked(调试专用)新增同上仅在开发模式有效,追踪响应式依赖的收集。
-onRenderTriggered(调试专用)新增同上