Vue 生命周期是每个 Vue 开发者必须深入理解的核心概念之一。它定义了组件从创建、挂载、更新、销毁的整个过程,以及在这个过程中各个阶段提供的钩子函数。掌握生命周期不仅能帮助你理解 Vue 的工作原理,还能让你在合适的时机执行特定的操作,优化应用性能,避免常见陷阱。本文将从源码实现到实际应用,全面解析 Vue 生命周期的各个阶段。
一、生命周期概览
Vue 组件的生命周期可以分为四个主要阶段:
- 初始化与挂载:创建组件实例,初始化数据,挂载 DOM
- 数据更新:响应式数据变更时触发更新流程
- 销毁:清理组件并释放资源
- 特殊场景:错误处理、服务器端渲染等
每个阶段都提供了相应的钩子函数,开发者可以在这些钩子中注入自定义逻辑。生命周期钩子的执行顺序是固定的,理解这个顺序对于编写正确的代码至关重要。
二、生命周期阶段详解
2.1 初始化与挂载阶段
这个阶段从组件实例创建开始,到 DOM 挂载完成结束。
2.1.1 beforeCreate
- 触发时机:实例初始化之后,数据观测 (data observer) 和
event/watcher
事件配置之前被调用。 - 特性:
- 此时
this
指向实例,但数据和方法均未初始化。 - 无法访问
data
、methods
或computed
。 - 通常用于初始化非响应式数据或全局插件。
- 此时
- 示例:
export default {beforeCreate() {// 初始化全局事件总线this.$bus = new Vue();// 记录组件创建时间this._createdAt = Date.now();} }
2.1.2 created
- 触发时机:实例已经创建完成之后被调用。在这一步,实例已经完成了数据观测、
property
和method
的计算、watch/event
事件回调的配置。然而,挂载阶段还没有开始,$el
属性目前不可用。 - 特性:
- 可以访问
data
、methods
和computed
,但 DOM 尚未挂载。 - 适合进行数据获取(如 API 请求)或初始化依赖数据的操作。
- 可以访问
- 示例:
export default {data() {return {users: []};},async created() {try {const response = await fetch('/api/users');this.users = await response.json();} catch (error) {console.error('Failed to fetch users', error);}} }
2.1.3 beforeMount
- 触发时机:挂载开始之前被调用。
- 特性:
- 模板编译/渲染函数已经完成,但尚未挂载到 DOM。
$el
是虚拟 DOM,不可访问实际 DOM 元素。- 适合在渲染前对模板进行最后的修改。
- 源码关键点:
// Vue 源码简化版 vm.$el = vm.$options.el; callHook(vm, 'beforeMount');// 编译模板生成 render 函数 const updateComponent = () => {vm._update(vm._render(), hydrating); };
2.1.4 mounted
- 触发时机:挂载完成后被调用。此时模板已经编译完成并挂载到 DOM 上。
- 特性:
- 可以访问
$el
和实际 DOM 元素。 - 子组件已经完成挂载(但不保证所有异步子组件都已完成)。
- 适合进行 DOM 操作、初始化第三方插件(如 Chart.js、Leaflet)或订阅事件。
- 可以访问
- 示例:
export default {mounted() {// 初始化图表this.chart = new Chart(this.$el.querySelector('#chart'), {type: 'bar',data: this.chartData});// 添加 DOM 事件监听器this.$el.addEventListener('click', this.handleClick);},beforeDestroy() {// 清理图表实例和事件监听器this.chart.destroy();this.$el.removeEventListener('click', this.handleClick);} }
2.2 数据更新阶段
这个阶段在组件数据发生变化时触发,包含虚拟 DOM 重新渲染和打补丁的过程。
2.2.1 beforeUpdate
- 触发时机:数据更新时调用,发生在虚拟 DOM 打补丁之前。
- 特性:
- 数据已经变更,但 DOM 尚未更新。
- 可以访问更新前的 DOM 状态。
- 适合在更新前保存当前 DOM 状态或执行一些预处理。
- 示例:
export default {data() {return {list: [1, 2, 3]};},beforeUpdate() {// 保存更新前的列表高度this.prevListHeight = this.$el.offsetHeight;} }
2.2.2 updated
- 触发时机:由于数据更改导致的虚拟 DOM 重新渲染和打补丁之后调用。
- 特性:
- 数据和 DOM 都已经更新。
- 可以访问更新后的 DOM 状态。
- 注意:不要在这个钩子中修改数据,否则可能导致无限循环更新。
- 示例:
export default {updated() {// 对比更新前后的列表高度,执行动画if (this.prevListHeight !== this.$el.offsetHeight) {this.animateListHeightChange();}} }
2.3 销毁阶段
这个阶段在组件实例销毁时触发,用于清理资源和事件监听器。
2.3.1 beforeDestroy (Vue 2) / beforeUnmount (Vue 3)
- 触发时机:实例销毁之前调用。此时实例仍然完全可用。
- 特性:
- 组件仍然完全正常工作。
- 适合进行资源清理(如定时器、事件监听器、WebSocket 连接等)。
- 示例:
export default {created() {this.timer = setInterval(() => {console.log('定时任务');}, 1000);this.$bus.$on('some-event', this.handleEvent);},beforeDestroy() {// 清理定时器clearInterval(this.timer);// 取消事件订阅this.$bus.$off('some-event', this.handleEvent);} }
2.3.2 destroyed (Vue 2) / unmounted (Vue 3)
- 触发时机:实例已经完全销毁之后调用。
- 特性:
- 所有的事件监听器和子实例已经被销毁。
- 组件实例完全不可用。
- 通常用于确认资源是否已经正确释放。
- 源码关键点:
// Vue 源码简化版 callHook(vm, 'beforeDestroy');// 递归销毁子组件 vm.$children.forEach(child => {child.$destroy(); });// 移除所有事件监听器 vm._events = Object.create(null);callHook(vm, 'destroyed');
2.4 特殊场景钩子
2.4.1 activated / deactivated
- 触发时机:
activated
:被<keep-alive>
缓存的组件激活时调用。deactivated
:被<keep-alive>
缓存的组件停用时调用。
- 特性:
- 只在使用
<keep-alive>
包裹的组件中触发。 - 适合处理缓存组件的特殊逻辑(如恢复滚动位置、刷新数据)。
- 只在使用
- 示例:
<keep-alive><router-view /> </keep-alive>
export default {activated() {// 组件被激活时刷新数据this.fetchData();},deactivated() {// 保存组件状态this.saveScrollPosition();} }
2.4.2 errorCaptured (Vue 2) / errorCaptured + renderTracked + renderTriggered (Vue 3)
- 触发时机:
errorCaptured
:捕获来自子孙组件的错误时调用。renderTracked
/renderTriggered
(Vue 3):用于调试响应式依赖的追踪和触发。
- 特性:
- 可以阻止错误继续向上传播。
- 适合实现全局错误处理或日志记录。
- 示例:
export default {errorCaptured(err, vm, info) {// 记录错误日志console.error('Error captured:', err, info);// 可以返回 false 阻止错误继续向上传播return false;} }
2.4.3 serverPrefetch (Vue 3 仅 SSR)
- 触发时机:在服务器端渲染(SSR)期间,组件实例在服务器上被创建时调用。
- 特性:
- 仅在 SSR 模式下有效。
- 用于在服务器端预取数据,避免客户端重复请求。
- 示例:
export default {async serverPrefetch() {// 在服务器端预取数据this.data = await fetchData();} }
三、生命周期流程图与执行顺序
3.1 Vue 2 生命周期流程图
创建实例↓
beforeCreate↓
初始化 data/methods↓
created↓
是否有 el 选项?↓├─ 否 → 等待 vm.$mount(el)↓├─ 是 → 是否有 template 选项?↓├─ 是 → 编译 template 为 render 函数↓└─ 否 → 使用 el 的 outerHTML 作为 template↓
beforeMount↓
创建 vm.$el 并替换 el↓
mounted↓
数据变更↓
beforeUpdate↓
虚拟 DOM 重新渲染 & 打补丁↓
updated↓
调用 vm.$destroy()↓
beforeDestroy↓
销毁所有子实例、事件监听器和子组件↓
destroyed
3.2 Vue 3 生命周期变更
Vue 3 对生命周期钩子进行了一些重命名,以更准确地反映其用途:
beforeDestroy
→beforeUnmount
destroyed
→unmounted
新增钩子:
setup()
:替代beforeCreate
和created
,是 Composition API 的入口点。renderTracked
/renderTriggered
:用于调试响应式依赖。
四、生命周期钩子的实际应用场景
4.1 数据获取
- 最佳位置:
created
或mounted
- 选择依据:
- 如果数据获取不依赖 DOM 操作,使用
created
(稍早执行)。 - 如果需要访问 DOM 元素,使用
mounted
。
- 如果数据获取不依赖 DOM 操作,使用
- 示例:
export default {data() {return {posts: [],loading: true,error: null};},async created() {try {const response = await fetch('/api/posts');this.posts = await response.json();} catch (error) {this.error = error.message;} finally {this.loading = false;}} }
4.2 DOM 操作与第三方插件集成
- 最佳位置:
mounted
- 示例:初始化 Chart.js 图表
export default {mounted() {const ctx = this.$el.querySelector('#myChart').getContext('2d');this.chart = new Chart(ctx, {type: 'line',data: this.chartData,options: this.chartOptions});},beforeUnmount() {// 销毁图表实例this.chart.destroy();} }
4.3 状态恢复与保存
- 最佳位置:
activated
/deactivated
(配合<keep-alive>
) - 示例:保存和恢复滚动位置
export default {data() {return {scrollPosition: 0};},deactivated() {// 保存当前滚动位置this.scrollPosition = window.scrollY;},activated() {// 恢复滚动位置window.scrollTo(0, this.scrollPosition);} }
4.4 资源清理
- 最佳位置:
beforeDestroy
/beforeUnmount
- 示例:清理定时器、取消订阅、关闭网络连接
export default {created() {this.socket = new WebSocket('ws://example.com');this.interval = setInterval(this.updateData, 5000);},beforeUnmount() {// 清理 WebSocket 连接this.socket.close();// 清除定时器clearInterval(this.interval);} }
4.5 全局状态初始化
- 最佳位置:
beforeCreate
- 示例:初始化全局事件总线或配置
export default {beforeCreate() {// 初始化全局事件总线this.$bus = new Vue();// 配置全局 API 基地址this.$apiBaseUrl = process.env.VUE_APP_API_BASE_URL;} }
五、生命周期钩子的性能考虑
5.1 避免在钩子中执行耗时操作
- 问题:在
mounted
或updated
等钩子中执行大量计算或同步 API 请求会阻塞 UI 渲染。 - 解决方案:
- 使用异步操作(如
async/await
)处理 API 请求。 - 将复杂计算移至
computed
属性或watch
中。
export default {async mounted() {// 错误:同步执行大量计算// this.result = heavyCalculation(this.data);// 正确:异步执行setTimeout(() => {this.result = heavyCalculation(this.data);}, 0);// 或使用 Web Workerthis.worker.postMessage(this.data);this.worker.onmessage = (e) => {this.result = e.data;};} }
- 使用异步操作(如
5.2 避免在 updated 中修改数据
- 问题:在
updated
中修改数据会触发新的更新周期,可能导致无限循环。 - 解决方案:
- 仅在数据满足特定条件时才修改,且确保不会再次触发更新。
export default {updated() {// 错误:可能导致无限循环// if (this.value < 10) this.value++;// 正确:使用 nextTick 避免立即触发更新if (this.value < 10 && !this.updating) {this.updating = true;this.$nextTick(() => {this.value++;this.updating = false;});}} }
5.3 合理使用生命周期钩子
- 问题:在不需要的钩子中添加逻辑会增加组件复杂度和执行时间。
- 解决方案:
- 只在真正需要的钩子中添加代码。
- 使用 Composition API 将相关逻辑组织在一起,减少对生命周期钩子的依赖。
六、Vue 3 Composition API 中的生命周期
Vue 3 的 Composition API 提供了与生命周期钩子等效的函数,使逻辑复用更加灵活:
6.1 等效钩子映射
选项式 API | Composition API |
---|---|
beforeCreate | setup() |
created | setup() |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeDestroy | onBeforeUnmount |
destroyed | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked (仅开发模式) |
renderTriggered | onRenderTriggered (仅开发模式) |
6.2 示例:使用 Composition API 访问生命周期
import { onMounted, onBeforeUnmount, ref } from 'vue';export default {setup() {const count = ref(0);let timer;// 等效于 mountedonMounted(() => {timer = setInterval(() => {count.value++;}, 1000);});// 等效于 beforeUnmountonBeforeUnmount(() => {clearInterval(timer);});return {count};}
};
6.3 Composition API 的优势
- 逻辑复用:可以将相关生命周期逻辑封装到可复用的函数中。
- 代码组织:将同一功能的代码集中在一起,提高可读性。
- 类型安全:更好地支持 TypeScript,提供更准确的类型推导。
七、生命周期钩子的常见误区与解决方案
7.1 误区:在 mounted 中直接操作子组件 DOM
- 问题:子组件可能尚未完全挂载,直接访问子组件 ref 会失败。
- 解决方案:
- 使用
nextTick
确保子组件已挂载。 - 使用事件或 props 进行组件间通信。
export default {mounted() {// 错误:子组件可能尚未挂载// this.$refs.child.doSomething();// 正确:使用 nextTickthis.$nextTick(() => {this.$refs.child.doSomething();});} }
- 使用
7.2 误区:在 destroyed 中访问组件实例
- 问题:在
destroyed
钩子中,组件实例已经完全销毁,访问this
可能导致错误。 - 解决方案:
- 在
beforeDestroy
中进行所有清理操作。
export default {beforeDestroy() {// 正确:此时组件仍然可用this.cleanupResources();},destroyed() {// 错误:不要在这里访问 this// this.cleanupResources(); // 可能导致错误} }
- 在
7.3 误区:过度使用生命周期钩子
- 问题:在多个生命周期钩子中重复相同的逻辑,导致代码冗余。
- 解决方案:
- 使用 Composition API 将相关逻辑封装到一个函数中。
- 使用
watch
或computed
处理数据变化逻辑。
// 坏:重复逻辑 export default {mounted() {this.initData();},activated() {this.initData();},methods: {initData() {// 初始化逻辑}} };// 好:使用 Composition API 封装 import { onMounted, onActivated } from 'vue';export function useInitData(initFn) {onMounted(initFn);onActivated(initFn); }// 在组件中使用 export default {setup() {useInitData(() => {// 初始化逻辑});} };
八、生命周期源码解析(简化版)
Vue 的生命周期实现主要涉及以下几个核心模块:
- 实例初始化:
src/core/instance/init.js
- 生命周期钩子:
src/core/instance/lifecycle.js
- 渲染流程:
src/core/instance/render.js
- 更新流程:
src/core/observer/watcher.js
8.1 关键源码片段
// src/core/instance/init.js
Vue.prototype._init = function(options) {const vm = this;// 初始化生命周期状态initLifecycle(vm);// 初始化事件系统initEvents(vm);// 初始化渲染initRender(vm);// 调用 beforeCreate 钩子callHook(vm, 'beforeCreate');// 初始化注入initInjections(vm);// 初始化 data、props、computed 等initState(vm);// 初始化 provideinitProvide(vm);// 调用 created 钩子callHook(vm, 'created');if (vm.$options.el) {// 挂载组件vm.$mount(vm.$options.el);}
};// src/core/instance/lifecycle.js
Vue.prototype.$mount = function(el) {// 编译模板并生成 render 函数const mount = Vue.prototype._mount;const vm = this;// 调用 beforeMount 钩子callHook(vm, 'beforeMount');// 执行渲染const vnode = vm._render();vm._update(vnode, hydrating);// 调用 mounted 钩子callHook(vm, 'mounted');return vm;
};// 数据更新触发的更新流程
Watcher.prototype.update = function() {const vm = this.vm;// 调用 beforeUpdate 钩子callHook(vm, 'beforeUpdate');// 执行虚拟 DOM 更新vm._update(vm._render(), hydrating);// 调用 updated 钩子callHook(vm, 'updated');
};// 组件销毁流程
Vue.prototype.$destroy = function() {const vm = this;// 调用 beforeDestroy 钩子callHook(vm, 'beforeDestroy');// 执行销毁操作:移除事件监听器、销毁子组件等vm._isBeingDestroyed = true;// 递归销毁子组件vm.$children.forEach(child => {child.$destroy();});// 移除所有事件监听器vm._events = Object.create(null);// 调用 destroyed 钩子callHook(vm, 'destroyed');vm._isDestroyed = true;
};
九、总结与最佳实践
9.1 生命周期关键要点总结
-
初始化与挂载:
beforeCreate
:实例初始化后,数据和事件系统尚未初始化。created
:数据观测、property
和method
计算完成,可进行数据获取。beforeMount
:模板编译完成,但尚未挂载到 DOM。mounted
:DOM 挂载完成,可进行 DOM 操作和第三方插件初始化。
-
数据更新:
beforeUpdate
:数据变更后,DOM 更新前。updated
:DOM 更新完成,避免在此修改数据。
-
销毁阶段:
beforeDestroy
/beforeUnmount
:实例销毁前,可进行资源清理。destroyed
/unmounted
:实例完全销毁,不可访问组件状态。
-
特殊场景:
activated
/deactivated
:<keep-alive>
缓存组件的激活/停用。errorCaptured
:捕获子孙组件的错误。
9.2 最佳实践建议
- 数据获取:优先在
created
中进行,避免阻塞 DOM 渲染。 - DOM 操作:仅在
mounted
或之后进行,确保 DOM 已渲染。 - 资源清理:在
beforeDestroy
/beforeUnmount
中清理定时器、事件监听器等。 - 避免重复逻辑:使用 Composition API 或 mixins 复用生命周期相关逻辑。
- 调试工具:利用 Vue DevTools 监控生命周期钩子的执行情况。
- 性能优化:避免在生命周期钩子中执行耗时操作,使用异步处理。
掌握 Vue 生命周期是成为一名优秀 Vue 开发者的基础。通过合理利用生命周期钩子,你可以更精确地控制组件行为,优化应用性能,避免常见的开发陷阱。在实际开发中,结合 Composition API 的强大功能,你可以更灵活地组织和复用代码,打造出高质量的 Vue 应用。