Vue2生命周期有哪些核心阶段?
p>咱做Vue2项目的时候,经常得处理组件从创建、渲染到销毁的各种逻辑,比如啥时候发请求、啥时候操作DOM、啥时候清理定时器…这时候就得搞懂生命周期!可Vue2的生命周期到底有多少阶段?每个阶段能干啥、不能干啥?父子组件之间生命周期咋配合?今天就把这些问题一个个拆明白,哪怕是刚学Vue的新手也能看懂~
Vue2的生命周期可以分成创建、挂载、更新、销毁四大阶段,每个阶段对应不同的“钩子函数”(就是Vue自动调用的函数,你可以在里面写逻辑),咱一个个拆:创建阶段:beforeCreate → created
- beforeCreate:组件实例刚初始化(比如
new Vue()
之后),但数据观测(data里的变量还没变成响应式)、事件配置(methods里的方法还不能用)都没完成,这时候this.data
拿不到,this.methods
也调不了,实际开发中很少用,顶多做些“最最早期”的初始化(比如框架级别的埋点配置,但场景极少见)。 - created:组件实例创建完成!这时候数据是响应式的(data能访问)、方法也能调用(methods可用),但DOM还没渲染(
$el
是undefined
,页面上看不到真实元素),这阶段最适合发Ajax请求——因为数据和方法都准备好了,请求回来的数据能直接存到data里,等DOM渲染时就能用。
挂载阶段:beforeMount → mounted
- beforeMount:Vue开始编译模板(把
<template>
里的代码转成虚拟DOM),但真实DOM还没挂载到页面,这时候$el
是“虚拟DOM”(能拿到,但不是页面上的真实元素),操作DOM没用(因为页面上还没有),实际用得少,偶尔用来做“模板编译前的最后调整”(比如动态改模板里的某个配置,但场景不多)。 - mounted:真实DOM已经挂载到页面!这时候
$el
是页面上的真实元素,this.$refs
也能拿到DOM节点了。操作DOM、初始化第三方库(比如轮播图、富文本编辑器)都得在这阶段做——因为只有真实DOM存在,第三方库才能找到容器渲染。
更新阶段:beforeUpdate → updated
组件里的响应式数据变化后(比如data
里的变量被修改),就会触发更新阶段:
- beforeUpdate:数据已经变了,但DOM还没更新(页面上显示的还是旧数据),这时候能拿到“更新前的数据”,可以做一些判断(比如对比新旧数据,决定是否要拦截更新)。
- updated:DOM已经跟着数据更新完了,页面上显示的是新内容,这时候可以基于新DOM做操作(比如重新计算表格高度、调整动画)。
销毁阶段:beforeDestroy → destroyed
当组件被销毁时(比如用v-if
把组件从页面移除),会触发这两个钩子:
- beforeDestroy:组件实例还没销毁,数据、方法、DOM都还能访问,这阶段必须做“资源清理”——比如清除定时器(
clearInterval
)、移除事件监听(removeEventListener
),不然组件销毁后这些东西还在运行,会导致内存泄漏。 - destroyed:组件实例彻底销毁,所有数据绑定、事件监听、子组件都被清理,这时候基本没什么可操作的了,因为实例已经“拆干净”了,顶多做些最终的日志上报。
每个生命周期钩子适合做啥实际操作?
光知道阶段还不够,得结合业务场景用对地方,咱用“做一个博客详情页”“实现轮播图”这些例子,看看每个钩子咋用:created:发Ajax请求,初始化数据
比如做博客详情页,需要先请求文章数据:
export default { data() { return { article: {} } }, created() { // 这里发请求,拿到数据后存到data this.fetchArticle() }, methods: { async fetchArticle() { const res = await axios.get('/api/article/123') this.article = res.data } } }
为啥不在mounted
发请求?因为created
更早,请求在DOM渲染前就发起,能减少首屏等待时间(用户感觉加载更快)。
mounted:操作真实DOM,初始化第三方库
比如做轮播图,需要依赖真实DOM容器:
export default { mounted() { // 初始化swiper轮播图,this.$refs.slider是真实DOM new Swiper(this.$refs.slider, { loop: true }) } }
如果在beforeMount
里初始化,this.$refs.slider
还是虚拟DOM,Swiper找不到真实容器,就会报错。
beforeUpdate:数据更新前,做“拦截”或“记录”
比如购物车数量变化时,想在DOM更新前记录旧数量:
export default { data() { return { cartCount: 0 } }, beforeUpdate() { // 记录更新前的数量 this.oldCount = this.cartCount }, updated() { // DOM更新后,对比新旧数量,提示用户 if (this.cartCount > this.oldCount) { alert('新增了' + (this.cartCount - this.oldCount) + '件商品') } } }
beforeDestroy:清理定时器、事件监听
比如组件里有定时请求,必须在销毁前清除:
export default { data() { return { timer: null } }, created() { this.timer = setInterval(() => { this.fetchNewMsg() }, 5000) }, beforeDestroy() { // 销毁前清除定时器,否则组件没了,定时器还在跑 clearInterval(this.timer) } }
父子组件生命周期执行顺序是啥逻辑?
实际项目里组件都是嵌套的(父组件里包着子组件),这时候生命周期执行顺序很关键!分**加载、更新、销毁**三种场景:加载阶段(父组件渲染子组件)
执行顺序是:
父beforeCreate
→ 父created
→ 父beforeMount
→ 子beforeCreate
→ 子created
→ 子beforeMount
→ 子mounted
→ 父mounted
逻辑:父组件先完成自己的“创建阶段”,然后准备挂载(beforeMount
),这时候要渲染子组件,所以子组件开始自己的“创建→挂载”流程,等子组件挂载完成(mounted
),父组件才会完成自己的挂载(mounted
)。
更新阶段(父/子组件数据变化)
假设父组件数据变化,导致子组件的props
变化,执行顺序是:
父beforeUpdate
→ 子beforeUpdate
→ 子updated
→ 父updated
逻辑:父组件数据变了,先进入自己的beforeUpdate
,因为子组件的props
依赖父组件数据,所以子组件也会触发更新,进入beforeUpdate
,等子组件完成DOM更新(updated
),父组件才会完成自己的DOM更新(updated
)。
销毁阶段(父组件销毁,子组件也被销毁)
执行顺序是:
父beforeDestroy
→ 子beforeDestroy
→ 子destroyed
→ 父destroyed
逻辑:父组件要销毁时,先进入自己的beforeDestroy
,然后通知子组件“你也得销毁”,子组件先执行beforeDestroy
(清理自己的资源),再执行destroyed
(彻底销毁),最后父组件执行destroyed
,完成整个销毁流程。
举个代码例子(看控制台输出顺序更直观):
父组件里引入子组件,分别在每个钩子里console.log
,加载时输出顺序就是上面说的“父创建→子创建→子挂载→父挂载”,更新时修改父组件数据,输出顺序是“父beforeUpdate→子beforeUpdate→子updated→父updated”,销毁时用v-if
把父组件隐藏,输出顺序是“父beforeDestroy→子beforeDestroy→子destroyed→父destroyed”。
开发中利用生命周期优化性能有啥技巧?
懂了生命周期,就能在合适的时机做合适的事,减少性能浪费:数据请求时机:用created
而不是mounted
created
比mounted
早执行,请求发起得更早,能减少首屏加载时间,比如首页列表数据,在created
里发请求,数据回来后mounted
阶段DOM才渲染,用户感觉“加载快”。
延迟加载非必要资源:mounted
里做“条件加载”
有些资源(比如图表库、地图SDK)不是页面一加载就需要的,可以在mounted
里判断“用户是否滚动到该组件”,再加载资源。
mounted() { // 监听页面滚动,判断组件是否进入视口 const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { // 进入视口后,才加载ECharts import('echarts').then((echarts) => { this.initChart(echarts) }) observer.unobserve(this.$el) // 只加载一次 } }) observer.observe(this.$el) }
及时清理资源:beforeDestroy
里做“善后”
组件销毁后,定时器、事件监听、WebSocket连接这些如果不清理,会一直占用内存(内存泄漏),所以必须在beforeDestroy
里做:
beforeDestroy() { // 清除定时器 clearInterval(this.timer) // 移除事件监听 window.removeEventListener('resize', this.handleResize) // 关闭WebSocket this.ws.close() }
避免不必要的更新:beforeUpdate
里做“防抖/节流”
如果数据变化太频繁(比如输入框实时搜索),会导致DOM频繁更新,性能变差,可以在beforeUpdate
里判断“数据变化间隔是否足够大”,再决定是否更新:
data() { return { searchValue: '', lastUpdateTime: Date.now() } }, beforeUpdate() { const now = Date.now() // 间隔小于500ms,就阻止这次更新(节流) if (now - this.lastUpdateTime < 500) { // 阻止更新(需要结合Vue的更新机制,比如用标志位) this.shouldUpdate = false } else { this.shouldUpdate = true this.lastUpdateTime = now } }, // 配合计算属性或watch,根据shouldUpdate决定是否渲染
生命周期常见的坑和避坑方法是啥?
刚学的时候,很容易因为“时机不对”踩坑,这几个典型错误得注意:坑1:beforeMount
里操作DOM,拿不到元素
场景:想在组件加载时给按钮加点击事件,用document.getElementById('btn')
,但beforeMount
里执行,控制台报错“null”。
原因:beforeMount
阶段真实DOM还没挂载到页面,getElementById
找不到元素。
解决:把DOM操作移到mounted
里。
坑2:created
里用this.$refs
,拿不到子组件
场景:父组件想在created
里调用子组件的方法,写this.$refs.child.method()
,结果报错“method is not a function”。
原因:created
阶段DOM还没渲染,$refs
是基于DOM的,所以子组件的$refs
还没生成。
解决:要么把逻辑移到mounted
,要么用this.$nextTick
(等DOM更新后再执行):
created() { this.$nextTick(() => { this.$refs.child.method() // DOM渲染后,$refs才有值 }) }
坑3:销毁阶段没清理定时器,内存泄漏
场景:组件里有setInterval
,切换页面后定时器还在跑,控制台一直报错“找不到DOM”。
原因:组件销毁后,定时器没被清除,还在执行回调函数。
解决:在beforeDestroy
里clearInterval
:
data() { return { timer: null } }, created() { this.timer = setInterval(() => { ... }, 1000) }, beforeDestroy() { clearInterval(this.timer) }
坑4:误解mounted
只执行一次
场景:组件用v-if
控制显示隐藏,每次显示时mounted
里的初始化逻辑会重复执行(比如重复初始化地图,导致性能变差)。
原因:v-if
销毁组件后,再次显示会重新创建组件实例,所以mounted
会再执行。
解决:加个“是否已初始化”的标志位:
data() { return { isMapInit: false } }, mounted() { if (!this.isMapInit) { this.initMap() // 只初始化一次 this.isMapInit = true } }
实际项目案例:用生命周期做TodoList
光讲理论太虚,咱用“TodoList”这个经典需求,把生命周期串起来:created:请求待办数据
组件创建后,立刻请求后端的待办列表:
created() { this.fetchTodos() // 发请求,把数据存到this.todos }
mounted:让输入框自动聚焦
页面渲染后,让输入框自动获得焦点(提升用户体验):
mounted() { this.$refs.input.focus() // $refs.input是输入框的DOM }
beforeUpdate:记录更新前的待办数量
用户添加/删除待办时,记录更新前的数量,方便后续提示:
data() { return { todos: [], oldCount: 0 } }, beforeUpdate() { this.oldCount = this.todos.length // 记录更新前的数量 }
updated:提示待办数量变化
DOM更新后,对比新旧数量,给用户反馈:
updated() { const newCount = this.todos.length if (newCount > this.oldCount) { alert('新增了' + (newCount - this.oldCount) + '条待办') } else if (newCount < this.oldCount) { alert('完成了' + (this.oldCount - newCount) + '条待办') } }
beforeDestroy:清理键盘事件监听
组件销毁前,移除全局的键盘事件(比如按Enter键添加待办的监听):
created() { window.addEventListener('keydown', this.handleKeydown) }, beforeDestroy() { window.removeEventListener('keydown', this.handleKeydown) }
通过这个案例,能直观看到每个生命周期钩子在业务中的作用——从数据请求、DOM操作到资源清理,全流程覆盖。
Vue2的生命周期就像组件的“成长日记”:从创建时的懵懂(beforeCreate)、拿到数据的兴奋(created)、挂载到页面的成熟(mounted)、数据变化时的迭代(updated),到最终销毁时的善后(beforeDestroy)…每个阶段都有明确的分工,搞懂这些,写组件时就知道“啥时候该干啥”,既能避免BUG,又能优化性能,下次写Vue组件,不妨先想想:这个逻辑该放在哪个生命周期里?
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。