☰
Vue生命周期全解:从创建到销毁,一张图掌握八个钩子
2026/10/6 13:55:46 网站建设 项目流程

1. 生命周期到底是什么,为什么你必须懂

接触 Vue 的人,不管你是刚看完教程的新手,还是已经写过几个后台管理页面的老手,最终都会遇到同一个坎:代码写对了,页面却不在预期时机执行。有人想在页面加载时请求接口,于是把请求塞进了created;有人想在 DOM 渲染完成后操作节点,结果在mounted里拿不到元素;还有人想在离开页面前弹个确认框,试遍了所有钩子都不知道该用哪个。这些困惑,全部集中在一个核心知识点上——Vue 生命周期。

说句实话,Vue 的入门门槛不算高,模板语法、响应式数据、组件通信这些东西,抄几遍文档基本能上手。但生命周期是真正的分水岭,不理解它的人写出的代码全靠"试错",今天能用明天可能就崩;理解它的人写代码就像装了导航,什么时机做什么事,一目了然。

简单定义一下:生命周期指的是 Vue 组件从被创建、渲染、更新到最终销毁的整个过程。Vue 在这个过程的不同阶段分别暴露了一些函数,让你可以在指定时机插入自己的逻辑,这些函数就是生命周期钩子。

这篇文章不会只停留在"背八个函数名"的层面。我会从底层机制出发,讲清楚每个钩子触发时组件到底处于什么状态、能做什么不能做什么,然后结合 Vue 3 和 Vue 2 的差异、Composition API 与 Options API 的区别、常见业务场景和真实线上问题,把整个生命周期彻底讲透。不管是准备面试、排查 bug,还是从零开始做一个正式项目,这篇都能直接当参考手册用。

2. 一张图看懂 Vue 3 的完整生命周期

2.1 八个核心钩子,一条执行主线

以 Vue 3 为例,组件从生到死一共经历四个大阶段,露在外面的核心钩子是八个。我先把它们按执行顺序列出来,后面再逐个拆解:

阶段选项式 API 钩子组合式 API 钩子触发时机
创建beforeCreate无直接对应实例初始化前,数据响应式尚未建立
创建created无直接对应实例创建完成,响应式数据已可用
挂载beforeMountonBeforeMountDOM 尚未挂载,模板已编译为渲染函数
挂载mountedonMountedDOM 已完成挂载,可访问真实元素
更新beforeUpdateonBeforeUpdate数据变更,DOM 即将重新渲染
更新updatedonUpdatedDOM 已完成更新
卸载beforeUnmountonBeforeUnmount组件即将卸载,实例仍然可用
卸载unmountedonUnmounted组件已卸载,实例上一切都已销毁

这里有个非常关键的背景知识:Vue 3 放弃了 Vue 2 的beforeDestroy和destroyed,改用beforeUnmount和unmounted。这不只是改名,语义上更准确——"销毁"听起来像实例完全没了,但实际过程更像"卸载组件、清理副作用"。如果你还在维护 Vue 2 的老项目,记住两组名字在不同版本中的对应关系就行;如果在 Vue 3 项目里写destroyed,控制台会直接报钩子不存在,而且生命周期全部错乱。

2.2 父子组件的执行顺序,比你想的更重要

生命周期单独一个组件好理解,但真实项目里组件是嵌套的。父组件里包着子组件,子组件里又包着孙子组件,这时候钩子的执行顺序就变得极其关键。

我第一次在项目里踩这个坑时,想在父组件的created里拿到子组件的数据,结果发现子组件还没初始化。调试了大半天,最后才意识到:Vue 实例化父组件时,并不代表所有子组件也跟着实例化了,两者之间存在明确的前后顺序。

实际执行顺序是这样的:

  1. 父组件beforeCreate→created→beforeMount
  2. 父组件模板中找到子组件,开始创建子组件
  3. 子组件走完自己的beforeCreate→created→beforeMount→mounted
  4. 子组件挂载完成后,父组件才执行mounted

所以父组件的mounted永远在子组件的mounted之后,这个顺序称为"由内到外"挂载。更新阶段则恰恰相反:父组件的beforeUpdate先触发,子组件随后触发beforeUpdate和updated,等子组件全部更新完,父组件的updated才执行——"由外到内"进入更新,再"由内到外"完成更新。卸载阶段,父组件先执行beforeUnmount,然后子组件依次执行的beforeUnmount和unmounted,最后父组件才执行自己的unmounted。

记住这个顺序能解决很多诡异问题。比如你在父组件mounted里用this.$refs.child.method()去调子组件的方法,一定能调通,因为此时子组件早已挂载完毕;但如果你在父组件created里去调,大概率会报错,因为这个时候子组件连created都还没进。

3. 创建阶段:组件诞生前的黄金时机

3.1 beforeCreate 里到底能做什么

beforeCreate是在 Vue 实例初始化最开始触发的钩子,此时实例刚刚被创建,数据观察(reactive 响应式系统)、事件机制、计算属性、方法都还没有初始化完成。换句话说,这个阶段你几乎什么都拿不到:访问this.data是undefined,调用this.method会报"不是函数"。

有开发经验的人可能会问:既然什么都干不了,那这个钩子存在的意义是什么?答案是极少用于业务,但适用于一些特殊场景。比如你想在组件初始化前设置一些非响应式的全局状态、或者在实例上提前挂一个标记位,这些不依赖响应式系统的逻辑可以放在这里。实际项目中,beforeCreate基本是面试里的理论考点,业务代码里很少见到。

3.2 created 是所有数据操作的起点

created是一个真正"能用"的钩子。到了这个阶段,Vue 已经完成了响应式数据的初始化、methods和计算属性的绑定、props的处理,所以你可以:

  • 访问this.message、this.userList等 data 里的数据
  • 调用已经在methods里定义好的方法
  • 发起网络请求获取数据
  • 对初始化数据进行一些处理和计算

有个细节值得注意:created阶段虽然 data 已经可用,但 DOM 还没有生成,this.$el拿不到真实节点,模板也没有渲染。所以在这个阶段操作 DOM 纯属白费力气。曾有人问我为什么在created里document.getElementById拿不到元素——原因很简单,页面还没渲染。

关于在created还是mounted里发请求,业界其实有个争论,我之前在团队里也和同事反复讨论过。我的实践结论是:请求的核心目的是获取数据,数据越早拿到越好,所以created是发请求最合理的位置。但这不是绝对的,如果你的请求需要依赖 DOM 节点的位置、尺寸等信息(比如获取某个图表容器的宽度再计算参数),那就必须放到mounted。还有一种常见场景是"请求参数依赖路由信息",这时候你会发现,created里通过this.$route拿路由信息已经绰绰有余,根本不用等到挂载。

3.3 选项式与组合式在创建阶段的差异

Vue 3 的 Composition API 有个容易让新手困惑的点:它没有beforeCreate和created的对应钩子。在<script setup>里,setup()本身就是一个"替代创建阶段"的入口。原来写在created里的逻辑,直接写在setup顶层代码里就行;原来写在beforeCreate里的逻辑,同样写在setup里(因为setup在创建阶段最先执行,此时响应式数据都还没建立)。

实际写代码的时候,很多人会这样处理:

// 选项式 export default { created() { this.fetchUserList() } } // 组合式 import { ref, onMounted } from 'vue' const userList = ref([]) async function fetchUserList() { const res = await api.getUserList() userList.value = res.data } fetchUserList() // 直接在 setup 顶层调用

注意,组合式里的顶层代码执行时机相当于原来的created之前、但响应式定义结束之后。如果你把fetchUserList()放在ref定义之前,此时userList还没初始化,会直接报错。所以组合式代码里,响应式变量的定义必须放在使用它的代码之前,这是写<script setup>的基本功。

4. 挂载阶段:DON'T TOUCH THE DOM YET

4.1 beforeMount 的短暂窗口

beforeMount触发时,模板已经被编译成渲染函数,但还没有将渲染结果插入到页面 DOM 中。这个阶段你仍然访问不到真正的 DOM 元素,this.$el如果非要说有值,那也只是 Vue 内部生成的占位符,没有任何意义。

这个钩子在业务代码里用得极少,几乎没有场景必须依赖它。如果非要说一个用途,大概是在某些特殊情况下,需要在挂载前对渲染数据进行最终微调。但说句实话,初学阶段你可以完全忽略它,面试时知道它存在的时机即可。

4.2 mounted 才是 DOM 操作的主战场

mounted是前端开发中最常使用的一个生命周期钩子,原因很简单:在这个阶段,组件对应的真实 DOM 已经渲染完毕,你可以放心地:

  • 获取 DOM 节点,修改样式、绑定事件
  • 初始化第三方库(ECharts、Swiper、Mapbox 等,它们大多依赖容器节点)
  • 执行需要等待元素渲染完成的测量和计算
  • 开启定时轮询或建立 WebSocket 连接

我在一个可视化大屏项目里,就深刻体会到mounted的重要性。那个项目用 ECharts 画地图和柱状图,最开始我把初始化图表的代码写在created里,结果控制台报"容器不存在",图表根本渲染不出来。后来改成在mounted里初始化,一切顺利。大家都爱用 ECharts,但很多人忘了 ECharts 需要拿到 DOM 容器才能初始化,而 DOM 容器只有在mounted之后才真实存在。

另外要注意,mounted只保证当前组件的 DOM 已挂载,如果组件里有子组件,父组件mounted触发时,子组件也都已经挂载完毕,但如果你引用了第三方异步组件或用了Suspense,情况会更复杂。封装一个"等待子组件挂载完成"的逻辑要谨慎,更多时候,直接在子组件的mounted里做自己的事更靠谱。

4.3 一个常见的时序坑:图片、字体等资源

mounted触发时,只是 DOM 节点挂上去了,不代表图片加载完成、字体加载完成、视频可以播放。曾经有个需求,需要在图片加载后测量它的宽高,我在mounted里document.querySelector('img').width,得到的结果是 0。这是因为mounted触发时 image 标签存在,但图片资源还在异步加载中。

解决方法是监听图片的load事件,或者用nextTick+setTimeout组合。如果你遇到"在mounted里做初始化但拿到的尺寸不对"的问题,先排查是不是资源尚未加载完成,再排查是不是用了缓存导致事件未触发。

5. 更新阶段:响应式数据的连锁反应

5.1 beforeUpdate 时发生了什么

只要 data 中的数据发生变化,Vue 就会进入更新流程。beforeUpdate在数据已经变了、但 DOM 还没重新渲染的节点触发。这个阶段有一个非常微妙的特点:

  • 你可以拿到最新的 data 数据
  • 但页面上的 DOM 还是旧的状态
  • 此时适合在 DOM 更新前做一些状态记录、临时逻辑

举个具体例子。假设页面上有一个列表,用户点击"删除"按钮后,我们需要在列表更新前记录"当前滚动条位置"或"当前选中项"。这类"在 DOM 即将变化前保存现场"的需求,用beforeUpdate正合适。但说实话,日常业务里直接操作beforeUpdate的频率不高,大多数更新逻辑都交给 Vue 的响应式系统自动处理了。

5.2 updated:更新完成后的检查窗口

updated触发时,DOM 已经完成了本次更新,页面上能看到最新的数据和视图。适合在这个钩子里做以下事情:

  • 依赖最新 DOM 状态的逻辑,比如重新计算某个元素的位置、尺寸
  • 某些需要"数据更新后重新初始化 DOM 插件"的场景

但这个钩子也是最容易造成死循环的钩子之一。原因很简单:如果你在updated里又修改了 data,Vue 检测到数据变化,会再次触发更新,再次进入updated,如果修改数据的逻辑没有终止条件,就会无限循环。比如:

updated() { this.count = this.count + 1 }

这段代码直接让页面卡死。理论上updated里改 data 再触发更新的路径是存在的,所以不要在updated里无脑修改数据,如果非要改,请加上明确的判断条件。我曾经过这样的教训:在updated里根据某个筛选条件修改列表的展示数据,由于条件判断写得不够严谨,导致筛选一次后不断重复更新,页面白屏。排查了很久才发现是生命周期 + 数据双绑定的问题。

5.3 谨慎使用:能不用 updated 就不用

我的个人观点是:updated是八个钩子里最容易被滥用的一个。很多新人以为"页面数据变了,我要在updated里做点什么",但实际上,绝大多数"数据变化后需要执行的操作"应该用watch监听来替代。两者的区别在于:

  • watch是精确监听某个数据的变化,变化一次执行一次,作用域明确
  • updated是组件内任何数据变化都会触发,你不知道这次更新到底是因为哪个变量

举个例子,你在 页面里同时改了name和age两个字段,updated会触发一次还是两次取决于 Vue 的渲染调度机制,但在这个钩子里你区分不出是谁导致的变化。而watch: { name() {...} }只对name变化做出响应。所以,涉及"数据驱动逻辑"的场景优先用watch,updated只留给真正需要"DOM 更新完成后再处理"的场景。

6. 卸载阶段:打扫战场比打胜仗更重要

6.1 beforeUnmount 的最后机会

当组件即将被移除(比如v-if变为 false、路由切换离开页面),Vue 会依次触发beforeUnmount和unmounted。

在beforeUnmount阶段,组件实例仍然是完整的,data 还能访问,方法还能调用,事件监听还在。这个阶段适合做"善后"工作:

  • 清掉setInterval、setTimeout定时器
  • 解绑第三方库的全局事件、DOM 监听
  • 关闭 WebSocket 连接
  • 保存草稿、记录用户行为日志

这类工作放在beforeUnmount而不是unmounted,是因为某些操作还需要用到实例数据。比如你有一个"离开页面时保存表单草稿到本地"的需求,需要在beforeUnmount里读取表单数据并写入localStorage,此刻实例还没有销毁,取数据是没有问题的。如果放到unmounted,很多属性可能已经不能正常访问了(Vue 3 中此时 DOM 节点和实例内部链接已被拆除)。

6.2 unmounted 之后还能做什么

unmounted触发时,组件的 DOM 节点已经被移除,实例的响应式数据连接已经被断开,一切跟这个组件相关的运行时资源基本都释放了。此时你还能做一件事:最后打印一次日志或清理稍后要用的全局标记,除此之外,不要在这个钩子里试图访问 DOM 子节点或继续调用依赖响应式数据的方法。

很多从 jQuery 时代过来的开发者容易犯一个错:在unmounted里调用this.$refs.someChild.someMethod()。组件都卸载了,$refs已经没有意义,这样调用十有八九报错。

6.3 keep-alive 带来的特殊生命周期

说到卸载,有一个不能忽略的特殊情况:<keep-alive>缓存的组件,它的"卸载"并不是真正的卸载,而是进入"失活"状态。

被<keep-alive>包裹的组件会多出两个钩子:

  • activated:组件被重新激活(缓存后再次进入页面)
  • deactivated:组件被缓存起来,从"页面可见"变成"页面不可见"

这个场景在移动端列表页 -> 详情页 -> 返回列表页的流程里极其常见。如果列表页用了<keep-alive>缓存,那么从详情页返回时,列表页不会重新走一遍created和mounted,而是触发activated。你在created里写的"每次进入页面都刷新数据"的逻辑不会执行,必须把逻辑挪到activated里才有用。

我在一个资讯 App 项目里就踩过坑:列表页进入详情页再返回,发现列表没有刷新新数据。排查后定位到<keep-alive>缓存导致created不触发。解决方式是使用onActivated钩子来重新拉取数据。这是生命周期知识在实际开发中最有含金量的一类应用之一。

7. Composition API 里的钩子写法与注意事项

7.1 函数式钩子的注册规则

如果用 Vue 3 的<script setup>写法,生命周期钩子必须从vue包里导入再使用。很多人容易写错的地方是钩子名:组合式 API 的钩子全都加了on前缀,并且首字母大写。

import { ref, onMounted, onBeforeUnmount, onActivated, onUnmounted } from 'vue' const timer = ref(null) onMounted(() => { timer.value = setInterval(() => { console.log('心跳检测中') }, 1000) }) onBeforeUnmount(() => { clearInterval(timer.value) })

有几个关键规则要记牢:

  • 钩子必须在setup执行期间同步注册,不能写在异步回调里(比如不能等请求返回后再注册)
  • 每个钩子可以被多次注册,执行顺序是自上而下按书写顺序执行
  • 同一个逻辑如果涉及多个钩子,你可以把代码封装成独立的 composable 函数,让复用变得更方便

第 2 条很有用。在选项式里,一个组件只能有一个created,如果你有三个功能模块,都得合并到同一个created函数里,代码会越来越臃肿。组合式里你可以写三次onMounted,每个功能模块各注册各的,互不干扰。这就是 Composition API 在组织代码时的核心优势。

7.2 setup 里 onMounted 和直接写代码的区别

这是新人最容易犯迷糊的地方。<script setup>顶层写的普通代码,执行时机是beforeCreate之后、created之前的阶段,也就是"实例初始化但响应式刚搭建"。而onMounted(() => {})内的代码,直到组件挂载完成后才会执行。

举一个实际例子:

<script setup> const userList = ref([]) // 顶层代码,立即执行 fetchUserList() onMounted(() => { console.log('组件挂载完成,DOM 可访问') }) </script>

fetchUserList()会直接执行,这相当于原来的created时机;而onMounted里的内容会延误到 DOM 挂载之后。所以,请求接口可以写在顶层,操作 DOM 必须放进onMounted,不要混为一谈。

7.3 响应式引用与生命周期配合的典型范式

前面提到我在做 ECharts 图表时遇到容器无法初始化,这个问题的最佳实践是:

<script setup> import * as echarts from 'echarts' import { ref, onMounted } from 'vue' const chartRef = ref(null) let chartInstance = null onMounted(() => { if (chartRef.value) { chartInstance = echarts.init(chartRef.value) chartInstance.setOption({ xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] }, yAxis: { type: 'value' }, series: [{ type: 'bar', data: [120, 200, 150] }] }) } }) onBeforeUnmount(() => { if (chartInstance) { chartInstance.dispose() chartInstance = null } }) </script>

这里有两个隐藏的技术点。一个是chartRef必须在onMounted里通过chartRef.value拿到真实 DOM,因为ref的绑定要到挂载完成后才生效。另一个是 ECharts 实例必须在组件卸载时手动dispose()释放,否则会出现内存泄漏。这两个点都用到了生命周期钩子,缺一不可。

8. 实战场景:生命周期钩子搭配使用的经典套路

8.1 页面加载数据 + 离开清理:每个组件都该有的基本盘

不管什么项目,最基础、最通用的生命周期套路就是:进入页面拿数据,离开页面清副作用。

setup() { const userList = ref([]) let intervalId = null // 进入页面,立即请求数据 fetchUserList() // 挂载完成后,开启定时轮询 onMounted(() => { intervalId = setInterval(() => { refreshList() }, 30000) }) // 离开页面,清理定时器 onBeforeUnmount(() => { if (intervalId) clearInterval(intervalId) }) }

我见过太多项目因为忘记清理setInterval导致内存泄漏:切页面越多次,卡顿越明显,最后整个应用越用越慢。排查这类问题的第一嫌疑对象,就是"有没有在onBeforeUnmount或unmounted里清理定时器和全局监听"。顺便提一个国产项目里常见的问题:全局的window.addEventListener('resize', handler)写在mounted里,却没有在卸载时移除。这样每次路由切换,旧的监听还挂在window上,事件越积越多,页面性能越来越差。

8.2 路由切换 + keep-alive:为什么不刷新数据

路由切换是生命周期使用频率最高的场景。特别是"列表页 -> 详情页 -> 返回列表页"这种流程,设计上有两种选择:

  1. 不用<keep-alive>:每次返回列表页,组件重新创建,created/onMounted都重新触发,数据自然刷新
  2. 用<keep-alive>缓存 组件:返回时不重新创建,created不触发,需要改用onActivated刷新数据

如果你采用了第二种,需要在onActivated里加上"获取最新数据"的逻辑。但需要注意,onActivated每次从缓存切换到显示都会触发,包括第一次进入页面时也会触发。所以如果你同时写了顶层代码和onActivated,会造成数据重复请求。我自己的经验是:用了<keep-alive>的页面,索性把"拉取数据"的逻辑统一放在onActivated里,顶层和onMounted不再重复请求。这样结构清晰,也避免多余的网络开销。

8.3 地图、图表、播放器:三方库初始化的标准姿势

前端项目里,Mapbox、ECharts、视频播放器等第三方库的生命周期管理非常依赖"挂载后初始化、卸载前销毁"这个固定节奏。

以 Mapbox 为例,标准做法是:

import mapbox from 'mapbox-gl' import { ref, onMounted, onBeforeUnmount } from 'vue' const mapContainer = ref(null) let map = null onMounted(() => { map = new mapbox.Map({ container: mapContainer.value, style: 'mapbox://styles/mapbox/streets-v11', center: [120.15, 30.28], zoom: 9 }) }) onBeforeUnmount(() => { if (map) { map.remove() map = null } })

很多地图类组件容易出现"再次进入页面地图空白"的问题,原因就是上次创建的 map 实例没有被销毁,这次重新初始化时 DOM 容器已经发生了变化。在onBeforeUnmount里执行map.remove()基本能解决 90% 的地图重复渲染问题。

8.4 多文件上传、Canvas 绘制:尺寸和节点都得等 mounted

热词里提到的"vue 多文件上传"、"canvas 2d vue"也是生命周期应用的典型场景。Canvas 初始化必须在mounted之后,因为需要拿到<canvas>的真实 DOM 节点并获取 2d context。而且,如果你在弹窗里动态渲染 Canvas,要注意v-if控制的时机:弹窗从不可见到可见,Canvas 的 DOM 节点是"挂载后"才生成的,所以弹窗组件里的 Canvas 初始化逻辑也应该写在对应的生命周期钩子里,而不是写在父组件里。

文件上传组件如果涉及拖拽区域,同样需要在mounted后给 DOM 元素绑定原生事件;离开页面时要移除事件监听,防止拖拽回调遗留栈内。

9. 常见问题排查实录:生命周期引发的典型 Bug

9.1 数据请求了但页面没渲染

症状:接口返回了数据,控制台也能打印出数据,但页面始终是空白或旧的。

排查思路:

  1. 先看数据是否被正确赋值到响应式变量。如果你是用let userList = []而不是ref管理的普通变量,数据变化不会触发视图更新
  2. 再看在created里赋值是否早于模板渲染。实际上,响应式数据在created赋值时可以正常驱动更新,因为响应式系统已经建立
  3. 确认你是否在请求返回前组件已经卸载。比如列表页发起了请求,但用户在请求返回前就跳走了,此时组件已卸载,this.userList = res.data会报警告。解决办法是在请求回调里判断组件是否仍然挂载,或在卸载标志位里中断请求

类似的问题在 Vue 3 中常见于onMounted里发请求,请求是异步的,等回调执行时组件可能已经卸载。Vue 3 不会报错但会有"组件已卸载"的警告,甚至在项目里会收到"Promise 未处理"的异常提示。我的做法是引入一个isUnmounted标记:

let isUnmounted = false onMounted(async () => { const res = await api.getData() if (!isUnmounted) { data.value = res.data } }) onBeforeUnmount(() => { isUnmounted = true })

9.2 页面重复进入,定时器越积越多

症状:用户反复进入同一个页面后,操作越来越卡,浏览器内存不断上涨。

排查思路:几乎可以断定是定时器或全局监听没有清理干净的锅。在mounted里创建的setInterval,必须有一个对应的beforeUnmount来清理。如果你发现清理了还是泄漏,检查是否在父组件中使用v-if切换子组件时,子组件里的onBeforeUnmount没有正确注册——比如钩子写在异步函数里了,Vue 收集不到,导致清理逻辑从未执行。

9.3 updated 里死循环

症状:页面卡死,控制台报"Maximum call stack size exceeded"。

排查思路:检查updated/onUpdated里是否修改了响应式数据,且没有终止条件。只要updated里改数据,就会再次触发更新,导致死循环。

9.4 动态路由参数变化为什么组件不更新

这个问题看似跟生命周期无关,其实密切相关。Vue Router 在切换路由时,如果复用了同一个组件(比如从/user/1跳到/user/2),组件实例会被复用,created和mounted不会重新触发,于是页面数据不更新。

正确做法是使用watch监听路由参数变化:

import { onMounted, watch } from 'vue' import { useRoute } from 'vue-router' const route = useRoute() onMounted(() => { fetchUserDetail(route.params.id) }) watch(() => route.params.id, (newId) => { fetchUserDetail(newId) })

这个看似"生命周期不工作"的问题,本质是理解"组件复用与生命周期触发条件"的关系。面试里被高频问到的"动态路由参数变化时组件为什么不刷新",答案就在这里。

9.5 组合式 API 钩子没有执行

症状:onMounted里的代码没有执行,但控制台不报错。

排查思路:最可能的原因是钩子写在了异步代码里,比如:

setTimeout(() => { onMounted(() => { ... }) // 无效,setup 上下文已丢失 }, 1000)

组合式 API 要求生命周期注册必须在setup的同步执行上下文中完成。任何在异步回调里注册的钩子都不会生效。还有一种情况是:onMounted写在了循环里,注册条件满足但执行顺序不符合预期。记住:钩子永远是同步注册的,这是组合式 API 的基本规则。

10. 选项式 API 和组合式 API 混用会怎样

在写今天的文章之前,又看到热词里同时出现了 Vue 2 和 Vue 3 的许多用法。这里补充一个非常实际的问题:如果用<script setup>组合式 API 写组件,同时又在defineComponent里混入了选项式created等钩子,会发生什么?

Vue 3 设计上是兼容的:选项式生命周期钩子和组合式生命周期钩子可以共存,执行优先级是选项式钩子先于组合式钩子执行。具体来说,选项式created会先执行,然后是setup顶层代码(可以理解成组合式的创建阶段),再然后是onMounted等组合式挂载钩子。

在实际项目中,我并不建议混用太多。混用会让代码的意图变得混乱,比如有些逻辑写在选项式created里、有些逻辑写在setup顶层,排查问题时很难确定执行顺序。我的经验是:新代码统一走组合式,老代码统一留在选项式,除非重构时明确迁移整个组件,否则不要同一组件里两种风格长期混存。

11. 给 Vue 入门者的学习路线建议

如果你刚接触 Vue,看到热词里的"vue快速学习路线"、"vue入门基础教程",我建议你掌握生命周期的路径是:

  1. 把八个核心钩子按阶段画在纸上,背下触发顺序
  2. 写一个最小 Demo,在每个钩子打印console.log,观察控制台输出顺序
  3. 用一个父组件嵌套一个子组件,观察父子生命周期顺序
  4. 重点练习created发请求、mounted初始化第三方库、beforeUnmount清理副作用这三件事
  5. 再看watch、computed与生命周期的配合,理解它们之间的差异
  6. 用<keep-alive>练习activated和deactivated,掌握缓存组件的独特习惯

自己动手打日志是学习生命周期最快的方式,比看任何教程都管用。你可以花十几分钟写个计数器组件,然后逐个钩子打印出来,你会对整个过程形成肌肉记忆。以后再遇到任何关于"什么时候做这件事"的疑问,第一反应不再是猜,而是回忆生命周期里对应节点能做什么。

12. 最后分享一点我的经验

我在实际项目中查 bug,只要问题涉及"时机不对",最后八成都能归因到生命周期上。举个例子,某次上线前测试发现,从 A 页面快速切换到 B 页面时,B 页面会闪现 A 页面的残留数据。排查了很久,最终发现问题出在 B 页面mounted里才开始请求数据,而请求返回前,模板已经用初始的ref([])渲染了一遍。这本质上是生命周期时机与异步请求之间的配合问题,解决办法是让初始数据状态为null而不是空数组,并在模板里对null做兜底展示,或者在setup顶层立刻发起请求。

另外,多提一个容易忽略的细节:nextTick与生命周期结合使用。当你在mounted里需要操作刚渲染的动态节点时,理论上mounted时期 DOM 已经就绪,但如果你使用了v-for、v-if等指令,或者某些数据是异步更新后才渲染的,那么可能需要await nextTick()再操作。Vue 的 DOM 更新是异步批量的,也就是说,你在数据变化后立即访问 DOM,拿到的不一定是最新值。理解"数据变更 -> 等待异步渲染 -> nextTick 回调"这条链路,能让你的生命周期代码更稳健。

Vue 生命周期不是一堆需要死记硬背的函数名,而是一张"组件从生到死的时间线"。掌握它,你写代码的主场感会完全不同。希望你在下一个项目里,每写一个钩子时都能清晰地知道:此刻组件正处于生命周期的哪个节点,我能做什么,不该做什么。这种认知上的清晰感,远比记住几个 API 名字有价值得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询