尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Vue2生命周期钩子函数详解:从创建到销毁的完整流程与实战应用

Vue2生命周期钩子函数详解:从创建到销毁的完整流程与实战应用 1. 从“Hello World”到项目上线为什么我们需要理解Vue2生命周期刚接触Vue2的时候我们可能都是从一段简单的代码开始的new Vue({ el: #app, data: { message: Hello Vue! } })。然后一个响应式的页面就诞生了。但随着项目变得复杂我们开始遇到各种“奇怪”的问题为什么在created里获取不到DOM元素为什么用$refs操作子组件有时会报错为什么页面离开再回来数据状态不对了这些问题的答案都藏在Vue2的生命周期里。生命周期听起来有点抽象但它其实就是Vue实例从“诞生”到“销毁”所经历的一系列关键时刻。Vue在这些关键时刻为我们预留了“钩子函数”让我们能在对的时机做对的事。理解生命周期不是为了应付面试而是为了写出更健壮、性能更好、bug更少的代码。它决定了我们何时去请求数据、何时去操作DOM、何时去清理定时器或事件监听器是Vue开发从“会用”到“精通”的关键一步。网上关于生命周期的图示很多但往往只列出了钩子函数的执行顺序。这篇文章我想从一个一线开发者的视角结合真实的开发场景和踩过的坑带你深入理解每个钩子函数背后的“为什么”和“怎么做”。我们会从一次完整的页面加载过程说起拆解每个阶段并重点探讨那些容易混淆和出错的地方比如父子组件生命周期的交错执行、keep-alive带来的特殊钩子以及如何在Vue2项目中优雅地处理异步操作和资源清理。2. 一次完整的旅程Vue实例的创建、挂载与更新阶段一个Vue实例的完整生命周期可以分为三大阶段创建Creation、挂载Mounting、更新Updating和销毁Destruction。为了更直观我们先抛开抽象的术语跟随一个组件从代码变成屏幕上视图的整个过程。2.1 初始化与响应式数据注入beforeCreate和created当我们执行new Vue(options)时旅程就开始了。Vue内部并不是一步到位的它分步初始化实例。beforeCreate 实例的“胚胎”期这是第一个被调用的生命周期钩子。此时实例刚刚被初始化但最核心的部分都还没有被创建。数据观测 (dataobserver) 未初始化你无法通过this.xxx访问到在data中定义的数据它们目前还是undefined。事件/侦听器配置未完成methods和watch中的方法也无法调用。$el属性不存在显然DOM相关的任何操作都无从谈起。那么这个钩子有什么用呢在实际开发中它的使用场景相对较少。因为它能做的事情非常有限。一个可能的用途是在某些插件或混合 (mixin) 的早期注入一些逻辑但这些逻辑不能依赖于实例的数据。对于绝大多数业务开发你几乎不会用到它。我个人的经验是除非你在开发一个底层库或框架需要极早地介入实例化过程否则可以暂时忽略它。created 实例“诞生”数据已就绪紧接着Vue完成了响应式系统的初始化。此时created钩子被调用。数据观测已就绪你可以安全地访问和修改data中定义的所有属性并且修改是响应式的。计算属性 (computed) 和侦听器 (watch) 尚未计算/执行它们要等到挂载阶段才开始工作。$el属性仍然为undefined实例还没有开始编译模板或挂载DOM。created是最常用、最重要的钩子之一。因为在这里实例的数据已经可用但DOM还未生成没有渲染开销。因此这里是进行异步数据获取的理想场所。例如从后端API加载页面初始化所需的数据。export default { data() { return { userList: [], loading: true }; }, async created() { try { const response await axios.get(/api/users); this.userList response.data; } catch (error) { console.error(Failed to fetch users:, error); } finally { this.loading false; } } }注意在created中发起异步请求请求返回时组件很可能已经进入挂载甚至更新阶段。这意味着你不能假设请求返回时DOM一定可用。如果需要操作DOM必须将相关逻辑放在mounted或使用$nextTick。2.2 模板编译与首次渲染beforeMount和mounted在created之后Vue会判断实例是否有el选项或是否后续调用了$mount方法。如果有它就进入下一个关键阶段将模板编译成渲染函数并准备挂载到真实的DOM上。beforeMount 挂载前的最后准备这个钩子在挂载开始之前被调用。此时编译模板已完成Vue已经将模板template选项或el指定的外部HTML编译成了渲染函数。虚拟DOM树已创建但尚未将其转换为真实的DOM节点并插入到页面中。$el属性即将被替换目前它仍然指向原始的挂载元素例如#app但很快就会被新创建的DOM元素替换。和beforeCreate类似beforeMount在常规业务开发中使用频率不高。你可以在这里执行一些需要在渲染前一刻完成的最终准备工作但这些工作同样不能依赖于真实的DOM。mounted 视图就位可以操作DOM了这是另一个极其重要的钩子。当mounted被调用时意味着实例已被挂载到DOMthis.$el现在指向的就是渲染出来的根DOM元素。子组件也已挂载注意这并不保证所有子组件都已经被挂载。Vue的挂载是递归的父组件的mounted钩子在其所有子组件的mounted钩子之后才被调用。这是一个关键细节。DOM是可交互的你可以在这里安全地操作DOM、初始化需要DOM的第三方库如图表库ECharts、地图库、富文本编辑器等、添加事件监听器尽管更推荐用Vue的方式或执行依赖于DOM尺寸的操作如获取元素宽高。export default { mounted() { // 初始化一个需要DOM的图表 const chart echarts.init(this.$refs.chartContainer); chart.setOption({...}); // 监听窗口变化调整图表大小 window.addEventListener(resize, this.handleResize); // 记得在 beforeDestroy 或 destroyed 中移除 }, methods: { handleResize() { // 重绘图表 } }, beforeDestroy() { window.removeEventListener(resize, this.handleResize); } }一个常见的坑在mounted中操作子组件因为父组件的mounted在子组件之后执行所以在父组件的mounted里通过$refs访问子组件通常是安全的。但是如果子组件是异步组件或者其渲染依赖于父组件传入的props且props是异步获取的那么在父组件mounted中子组件可能还未完成其自身的异步初始化。此时直接调用子组件的方法可能会失败。稳妥的做法是使用$nextTick或在子组件内部通过事件通知父组件。// 父组件 Parent.vue export default { mounted() { // 使用 $nextTick 确保子组件已处理完初始渲染和可能的异步逻辑 this.$nextTick(() { if (this.$refs.childComponent this.$refs.childComponent.someMethod) { this.$refs.childComponent.someMethod(); } }); } }2.3 响应式数据的舞蹈beforeUpdate和updated当组件依赖的响应式数据data、props、computed等发生变化时组件就会进入更新阶段。Vue会通过虚拟DOM的比对计算出最小化的DOM操作然后高效地更新视图。beforeUpdate 更新前的快照这个钩子在数据变化之后、虚拟DOM重新渲染和打补丁之前调用。此时数据已经是最新的但视图DOM还是旧的。你可以在这个钩子里访问现有的DOM状态例如获取更新前的滚动位置以便在更新后恢复它。updated 视图同步完成这个钩子在数据变化导致的虚拟DOM重新渲染和打补丁之后调用。此时组件的DOM已经更新完成可以执行依赖于更新后DOM的操作。重要警告慎用updated钩子updated是一个很容易被滥用的钩子。因为任何数据变化都会触发它如果你在updated里又去修改数据可能会导致无限循环的更新。// 危险可能导致无限循环 updated() { if (this.someCondition) { this.someData newValue; // 这又会触发新一轮的更新... } }updated的典型使用场景是当你使用了非Vue控制的DOM操作例如直接使用jQuery插件修改了DOM需要在Vue更新后手动同步第三方库的状态。对于绝大多数纯Vue管理的数据到视图的更新你不应该在updated中修改状态。数据变化的逻辑应该放在methods里由用户交互或异步回调触发。3. 组件销毁与资源清理beforeDestroy和destroyed当组件被移除时例如v-if从true变为false或路由切换它会进入销毁阶段。这是防止内存泄漏的关键环节。beforeDestroy 最后的清理机会实例销毁之前调用。在这一步实例仍然完全可用。这是你进行清理工作的最后也是最佳时机。你必须在这里手动销毁那些Vue无法自动管理的外部资源。需要清理的常见资源包括定时器(setInterval,setTimeout) 如果不清除即使组件销毁了定时器回调仍然会执行可能导致错误或内存泄漏。事件监听器 使用addEventListener手动添加的DOM事件或者使用第三方库如Bus事件总线注册的事件。取消未完成的异步请求 例如使用Axios的CancelToken在组件销毁时取消可能还在pending状态的网络请求避免在已销毁的组件上执行setData操作。清理第三方库实例 如ECharts图表实例的dispose方法地图实例的销毁等。export default { data() { return { timer: null, chartInstance: null, cancelTokenSource: null }; }, created() { this.timer setInterval(() { console.log(Timer tick); }, 1000); // 创建请求取消令牌 const CancelToken axios.CancelToken; this.cancelTokenSource CancelToken.source(); axios.get(/api/data, { cancelToken: this.cancelTokenSource.token }).then(...).catch((thrown) { if (axios.isCancel(thrown)) { console.log(Request canceled, thrown.message); } else { // 处理错误 } }); }, mounted() { this.chartInstance echarts.init(this.$el); window.addEventListener(resize, this.handleResize); }, beforeDestroy() { // 1. 清除定时器 if (this.timer) { clearInterval(this.timer); this.timer null; } // 2. 取消异步请求 if (this.cancelTokenSource) { this.cancelTokenSource.cancel(Component destroyed); } // 3. 销毁第三方库实例 if (this.chartInstance) { this.chartInstance.dispose(); this.chartInstance null; } // 4. 移除事件监听器 window.removeEventListener(resize, this.handleResize); }, methods: { handleResize() { /* ... */ } } }destroyed 实例的终结实例销毁后调用。调用后Vue实例指示的所有东西都会解绑定所有的事件监听器会被移除所有的子实例也会被销毁。此时实例已不可用所有属性和方法都无法访问。通常你不需要在这个钩子里做任何事情因为清理工作应该在beforeDestroy中完成。这个钩子更像是一个确认“实例已完全销毁”的通知。4. 进阶场景与特殊钩子activated与deactivated在单页应用SPA中为了提升用户体验我们经常使用keep-alive包裹动态组件或路由组件以实现组件状态的缓存避免重复渲染。keep-alive带来的变化被keep-alive包裹的组件在切换时不会被销毁而是被失活并缓存起来。因此它们的beforeDestroy和destroyed钩子不会触发。取而代之的是一对新的生命周期钩子activated和deactivated。activated 组件从缓存中激活时调用当被缓存的组件再次被切换到前台时例如通过路由切回该页面activated钩子会被触发。此时组件保持在mounted之后的状态所有数据、DOM都保持原样。这是执行一些“页面显示时需要刷新”的操作的好地方。例如一个数据仪表盘页面被缓存了当用户再次切回来时你可能希望自动刷新一下数据。export default { data() { return { freshData: null }; }, activated() { // 组件被激活刷新数据 this.fetchFreshData(); }, methods: { async fetchFreshData() { this.freshData await axios.get(/api/latest-data); } } }deactivated 组件被缓存失活时调用当组件被keep-alive缓存并从当前视图切换走时deactivated钩子会被触发。这是执行一些“页面离开时需要暂停”的操作的好地方。例如清除本组件内的轮询定时器、暂停视频播放、暂停复杂的动画等。注意与beforeDestroy不同这里通常不是做最终清理如移除全局事件监听器的地方因为组件实例还在只是被隐藏了。最终清理会在组件真正被销毁时例如keep-alive的max属性导致组件被销毁或父组件被销毁的beforeDestroy中进行。export default { data() { return { pollTimer: null }; }, activated() { this.startPolling(); }, deactivated() { // 页面失活停止轮询以节省资源 this.stopPolling(); }, beforeDestroy() { // 最终清理确保定时器被清除 this.stopPolling(); }, methods: { startPolling() { this.pollTimer setInterval(() { this.fetchData(); }, 5000); }, stopPolling() { if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer null; } } } }使用include/exclude时的注意事项keep-alive可以通过include和exclude属性来有选择地缓存组件。如果一个组件没有被缓存那么它依然会走正常的创建、销毁流程。因此如果你的组件逻辑同时用到了activated/deactivated和beforeDestroy/destroyed需要仔细设计确保资源在任何一种销毁路径下都能被正确清理。5. 父子组件生命周期的执行顺序理解单个组件的生命周期后我们还需要知道它们在嵌套组件中是如何交织执行的。这关系到我们何时能安全地跨组件通信和操作。渲染过程的执行顺序父 - 子假设有父组件A和子组件B在初始渲染时生命周期钩子的执行顺序是父组件AbeforeCreate父组件Acreated父组件AbeforeMount子组件BbeforeCreate子组件Bcreated子组件BbeforeMount子组件Bmounted父组件Amounted这个顺序是符合直觉的父组件先准备数据created然后开始挂载beforeMount。在挂载过程中它发现模板里有子组件B于是开始创建和挂载子组件。等所有子组件都挂载完毕mounted后父组件才最终完成自己的挂载mounted。所以在父组件的mounted钩子里通常可以安全地通过$refs访问子组件。更新过程的执行顺序父 - 子 - 父当父组件的数据变化导致父子组件都需要更新时父组件AbeforeUpdate子组件BbeforeUpdate子组件Bupdated父组件Aupdated同样是父组件先进入更新流程然后驱动子组件更新。所有子组件更新完毕后父组件才完成更新。销毁过程的执行顺序父 - 子 - 父当父组件被销毁时父组件AbeforeDestroy子组件BbeforeDestroy子组件Bdestroyed父组件Adestroyed父组件先准备销毁然后递归地销毁其所有子组件最后自己再销毁。一个实践中的陷阱异步更新的交错Vue的更新是异步的。当你修改响应式数据后Vue并不会立即更新DOM而是开启一个队列缓冲在同一事件循环中发生的所有数据变更。在下一个事件循环的“tick”中Vue才会清空队列执行实际的DOM更新。这就是$nextTick存在的意义。考虑这个场景父组件在mounted中修改了一个传递给子组件的prop并立即试图通过$refs调用子组件的一个方法。// 父组件 mounted() { this.parentData new value; // 这会触发子组件的更新 this.$refs.child.doSomething(); // 危险子组件可能还未根据新的prop更新其内部状态 }由于更新是异步的在父组件mounted中直接调用时子组件可能还处在“已收到新prop但尚未执行beforeUpdate/updated”的状态。安全的做法是使用$nextTickmounted() { this.parentData new value; this.$nextTick(() { // 此时DOM更新已完成子组件状态也已同步 this.$refs.child.doSomething(); }); }6. 生命周期在常见业务场景中的实战应用理解了理论我们来看看在真实的Vue2项目中如何利用生命周期钩子优雅地解决实际问题。6.1 场景一数据获取与加载状态管理这是最经典的应用。我们通常在created钩子中发起异步请求获取初始数据。export default { data() { return { posts: [], loading: false, error: null }; }, created() { this.loadPosts(); }, methods: { async loadPosts() { this.loading true; this.error null; try { const response await fetch(/api/posts); this.posts await response.json(); } catch (err) { this.error Failed to load posts.; console.error(err); } finally { this.loading false; } } } }优化点如果组件可能被频繁创建和销毁例如列表项可以考虑将数据获取逻辑上移到父组件或使用状态管理如Vuex避免重复请求。对于被keep-alive缓存的页面可以在activated钩子中刷新数据提供更好的用户体验。6.2 场景二集成第三方DOM库如ECharts、地图这类库通常需要在DOM元素可用后初始化并在组件销毁时清理。export default { data() { return { chart: null }; }, mounted() { this.initChart(); // 响应窗口变化 window.addEventListener(resize, this.handleChartResize); }, beforeDestroy() { // 销毁图表实例释放内存 if (this.chart) { this.chart.dispose(); } // 移除事件监听器 window.removeEventListener(resize, this.handleChartResize); }, // 如果被 keep-alive 缓存 deactivated() { // 可以暂停图表动画或轮询 if (this.chart) { this.chart.clear(); } }, activated() { // 重新绘制图表 if (this.chart this.$el) { this.chart.resize(); this.chart.setOption(this.options, true); // notMerge: true 表示完全替换 } }, methods: { initChart() { // 确保 $el 存在 if (!this.$el) return; this.chart echarts.init(this.$el); this.chart.setOption(this.options); }, handleChartResize() { if (this.chart) { this.chart.resize(); } } } }6.3 场景三监听路由参数变化在Vue Router中当同一个组件被复用时例如从/user/1切换到/user/2组件实例不会重新创建因此created和mounted钩子不会再次触发。为了响应路由参数的变化我们需要使用侦听器 (watch) 来监听$route对象。export default { data() { return { userId: null, userInfo: {} }; }, created() { // 初始化加载 this.userId this.$route.params.id; this.fetchUser(this.userId); }, mounted() { // 这里只执行一次 }, watch: { // 监听路由参数变化 $route.params.id(newId, oldId) { if (newId ! oldId) { this.userId newId; this.fetchUser(this.userId); } } }, methods: { fetchUser(id) { // 获取用户数据... } } }另一种更简洁的方式是使用beforeRouteUpdate导航守卫如果组件内使用了该守卫但生命周期钩子与侦听器的组合是更通用的模式。6.4 场景四性能优化与内存泄漏防范这是生命周期钩子尤其是beforeDestroy和deactivated最重要的用途之一。内存泄漏在SPA中是一个隐形杀手。需要清理的资源清单全局事件总线 (EventBus) 监听器如果你使用了全局的Vue实例作为事件总线一定要在组件销毁前用$off移除监听。created() { EventBus.$on(some-event, this.handleEvent); }, beforeDestroy() { EventBus.$off(some-event, this.handleEvent); }DOM事件监听器如前所述addEventListener添加的事件必须手动移除。定时器setInterval和setTimeout必须用clearInterval和clearTimeout清理。第三方库实例地图、图表、富文本编辑器等通常都有destroy或dispose方法。未完成的Promise/网络请求使用Axios的CancelToken或Fetch API的AbortController来取消请求。Vuex的Action订阅如果你使用了store.subscribeAction记得在组件销毁时取消订阅。建立一个清晰的清理清单并在beforeDestroy中系统性地处理它们是高级Vue开发者必备的习惯。7. 从Vue2到Vue3生命周期钩子的变化与迁移虽然本文聚焦Vue2但了解Vue3的变化有助于我们更好地理解其设计思想并为未来迁移做准备。Vue3引入了Composition API其生命周期钩子名称和用法有所变化但核心概念一脉相承。钩子名称的对应关系beforeCreate- 使用setup()函数。在setup()中响应式数据、计算属性等都已可用相当于created阶段。created- 使用setup()函数。beforeMount-onBeforeMountmounted-onMountedbeforeUpdate-onBeforeUpdateupdated-onUpdatedbeforeDestroy-onBeforeUnmountdestroyed-onUnmountedactivated-onActivateddeactivated-onDeactivated关键变化beforeCreate和created被setup()替代在Composition API中组件的初始化逻辑都在setup()函数中完成它会在beforeCreate之前调用。销毁钩子更名为onBeforeUnmount和onUnmount更准确地反映了其行为卸载。需要显式导入Vue3的钩子需要从vue中导入然后在setup()中使用。import { onMounted, onUnmounted } from vue; export default { setup() { onMounted(() { console.log(组件已挂载); }); onUnmounted(() { console.log(组件即将卸载); }); } }更好的逻辑组织Composition API允许将相关的逻辑数据、方法、生命周期组织在一起而不是像Options API那样分散在data、methods、created等不同选项中。对于现有的Vue2项目理解这些对应关系并在编写新组件或重构旧组件时有意识地向Composition API的思维靠拢会让未来的升级之路更加平坦。不过Vue3也完全支持Options API所以不必急于一次性重写所有代码。
返回列表