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

资讯详情

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

Vue生命周期全解析:从组件渲染原理到面试实战应用

Vue生命周期全解析:从组件渲染原理到面试实战应用 刚入行那会儿我最怕面试里被问到“Vue生命周期”。不是不知道那几个钩子函数的名字而是每次被问到“父子组件执行顺序”“为什么 mounted 里拿不到数据”“keep-alive 下生命周期怎么变”这类问题时脑子里就一团浆糊。背过无数遍八股真到了项目里遇到诡异 bug还是只能靠 console.log 一点点试。后来做得久了才发现生命周期不是一堆需要死记硬背的钩子而是 Vue 组件从生到死的一整套时间线。你只要理解了这条时间线很多所谓的“面试难题”和“线上疑难杂症”其实是一回事。这篇文章不打算给你背答案而是把我从面试和实战里总结出来的经验连同踩过的坑一起讲清楚争取让你看完之后无论是应付面试还是写业务代码都能真正“拿捏”住 Vue 生命周期。1. 生命周期到底在管理什么1.1 从组件的“生老病死”说起很多人把生命周期理解成“一堆函数”其实它描述的是一件事一个 Vue 组件从创建、渲染、更新到销毁的完整过程。你可以把组件想象成一个“人”出生前要准备beforeCreate、出生后要睁眼看世界created、要站上舞台beforeMount、正式亮相mounted、中途还会不断变化beforeUpdate/updated最后退场beforeUnmount/unmounted。Vue 之所以要暴露这些阶段是因为你在不同阶段能拿到的东西不一样、适合做的事情也不一样。比如created的时候数据已经初始化了但 DOM 还没真正渲染出来mounted的时候 DOM 有了但如果你在数据里存了定时器在组件销毁前不清理就会造成内存泄漏。我见过不少刚接触 Vue 的同学拿到项目就习惯性地把所有逻辑都塞进mounted也不管是数据请求、DOM 操作还是事件监听全往里面堆。结果数据请求在mounted里发DOM 还没渲染完就去操作节点或者页面切走之后定时器还在跑。这些问题说到底是同一个根源没搞清楚生命周期的每个阶段到底“能用什么”“该干什么”。1.2 Vue 暴露给我们的六个关键阶段Vue2 的官方文档把生命周期分成 8 个钩子beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed。Vue3 里把最后两个改名为beforeUnmount和unmounted语义更准确不是“销毁”而是“卸载”。组合式 API 里则是一组onXxx函数onBeforeMount、onMounted、onBeforeUpdate、onUpdated、onBeforeUnmount、onUnmounted。注意组合式 API 里没有beforeCreate和created的对应钩子因为在setup执行的时候这两个阶段已经过了。这里有一个特别容易忽略的点setup本身是在beforeCreate和created之间执行的。换句话说你在setup里写代码其实就是在“创建阶段”里写代码。所以如果你在选项式写法里习惯用created来做初始化迁移到组合式 API 之后直接在setup顶层写即可不需要再找一个onCreated。2. 一张表搞懂 Vue2 与 Vue3 生命周期差异2.1 选项式 API 的命名对应关系如果你是从 Vue2 项目转 Vue3最需要先适应的就是改名这件事。我做一个对照表你直接保存就行Vue2 钩子Vue3选项式Vue3组合式触发时机beforeCreatebeforeCreatesetup 中直接写实例初始化之前数据和方法都拿不到createdcreatedsetup 中直接写实例创建完成可以访问 data 和 methodsbeforeMountbeforeMountonBeforeMount模板已经编译好但还没挂载到 DOMmountedmountedonMounted组件挂载完成DOM 可访问beforeUpdatebeforeUpdateonBeforeUpdate数据变化DOM 即将重新渲染updatedupdatedonUpdated数据变化DOM 已完成重新渲染beforeDestroybeforeUnmountonBeforeUnmount实例即将被卸载清理工作在这做destroyedunmountedonUnmounted实例已经卸载完成只要记住“destroyed → unmounted”这个变化选项式的迁移基本不会踩坑。组合式 API 那边则要注意所有onXxx钩子都必须在setup函数中同步调用你不能在一个setTimeout里写onMounted也不能在async函数里 await 之后再调用否则 Vue 会警告你而且钩子不会生效。2.2 为什么 Vue3 去掉了 beforeCreate 和 createdVue3 在组合式 API 里没有提供onCreated不是功能被砍掉了而是因为setup的执行时机已经覆盖了beforeCreate和created之间这一段。你直接在setup顶层写的代码其实就是在创建阶段执行的。但要理解一个细节Vue3 的setup执行时组件实例已经创建完毕所以props、attrs这些已经可以访问了但选项式里data和computed还没初始化完。这也是为什么在setup中使用this会拿不到预期的数据反而会报错。这也是很多人从 Vue2 转 Vue3 时最别扭的一点setup里没有this所有东西都是通过参数和返回值传递的。如果你需要在setup里模拟created的逻辑直接写就是了。最常见的做法就是把数据请求放在setup顶层配合异步函数调用。3. 面试必问的“生命周期顺序”到底怎么记3.1 父子组件挂载顺序的前因后果面试题里最常考的一道父组件和子组件的mounted谁先执行答案是子组件先父组件后。完整顺序是父组件beforeCreate父组件created父组件beforeMount子组件beforeCreate子组件created子组件beforeMount子组件mounted父组件mounted也就是说父组件的beforeMount执行完之后Vue 开始渲染父组件的模板结果发现模板里还有子组件于是先停下来把子组件创建、挂载完再回过头来继续挂载父组件。这就像一个管理员要开一场大会先通知各个小组长到位等所有小组长把各自组员带齐了才宣布大会正式开始。记起来一句话就行挂载阶段是从外到内“拆包”从内到外“完成”。拆包的时候父组件先启动完成的时候子组件先完成。3.2 更新和卸载阶段的执行规律更新阶段也有固定顺序。当某个响应式数据变化导致父、子组件的 DOM 都需要更新时父组件beforeUpdate子组件beforeUpdate子组件updated父组件updated卸载阶段类似父组件beforeUnmount子组件beforeUnmount子组件unmounted父组件unmounted这里只要记住一个规律就够了挂载和卸载的“起点”都在父组件但完成“挂载”是子组件先完成完成“卸载”也是子组件先完成。你可以理解为父组件负责发号施令子组件负责执行完毕后才汇报。这个顺序不只是为了面试它对写代码很有意义。比如你在父组件的mounted里去拿子组件实例因为子组件已经挂载完成所以你可以放心通过ref拿到子组件的方法和数据。反过来如果你在父组件的created里去访问子组件大概率拿不到。4. 日常开发里生命周期到底怎么用4.1 created / setup 里做数据初始化以前用 Vue2 时很多人习惯在created里发请求拿列表数据。这是因为created阶段已经可以访问data虽然不能访问 DOM但对数据请求来说DOM 存不存在根本不重要。换成 Vue3 组合式 API 之后直接在setup顶层调用请求函数即可。我自己的习惯是需要渲染的数据请求放在setup/created需要 DOM 节点初始化的工作放在mounted。举个例子页面进来要先显示用户信息同时要初始化一个图表。用户信息请求在setup里发图表的 ECharts 实例在mounted里创建因为图表渲染必须要真实的 DOM 容器。4.2 mounted 里做 DOM 操作和第三方库初始化mounted是操作 DOM 的最佳时机因为此时组件已经挂载到页面上this.$elVue2或模板 refVue3都能正常使用了。我做过一个后台管理系统里面有大量表格和图表几乎每个页面都要在mounted里初始化 ECharts 实例、绑定滚动事件、或者调用第三方插件的某个方法。这里有一个细节值得注意Vue3 中onMounted执行时组件的 DOM 已经插入文档流中但如果你嵌套使用了Suspense或者异步组件可能还需要nextTick之后才更稳妥。所以当你发现onMounted里拿到的 DOM 尺寸为 0 时第一反应应该是检查是不是子组件还是异步渲染的。提示mounted不保证所有子组件也一起挂载完成。如果你需要在父组件中确认“整个子树都渲染完”可以配合this.$nextTick或 Vue3 的nextTick使用等当前渲染周期彻底结束再操作。4.3 beforeUnmount 是清理现场的关键我认为项目里最容易遗漏的就是beforeUnmount里的清理工作。很多内存泄漏和奇怪报错都是因为没有在组件卸载前解除监听、清除定时器、销毁实例。比如你在mounted里给 window 加上了一个resize监听器如果在beforeUnmount里忘记移除组件销毁之后监听器还在响应函数就会被重复调用。下面是一段很典型的反面教材和正确写法对比// 反面教材只加不卸 onMounted(() { window.addEventListener(resize, handleResize) }) // 正确写法成对出现 onMounted(() { window.addEventListener(resize, handleResize) }) onBeforeUnmount(() { window.removeEventListener(resize, handleResize) })同样逻辑适用于setInterval、setTimeout、ECharts 实例、以及一些第三方地图实例。我给自己定了条规矩在onMounted里每创建一类“需要释放的资源”就必须在onBeforeUnmount里写对应的释放代码。4.4 activated 和 deactivated被 keep-alive 改变的“特殊生命周期”如果你用过keep-alive一定会发现组件切换后普通生命周期不会重新触发页面状态被缓存了。这时候就需要activated和deactivated这两个钩子来配合。activated组件被 keep-alive 缓存后再次进入时触发deactivated组件从视图中移除但被缓存时触发注意它们的执行顺序首次进入组件时mounted先执行紧接着执行activated。之后切换走触发deactivated再切回来只触发activated不会重新走mounted。根据这个特性我一般会把“每次进入页面都需要刷新”的数据请求放在activated里把“只初始化一次”的逻辑放在mounted里。否则组件被缓存之后页面切回来数据还是旧的。5. 实战中绕不开的坑与排查技巧5.1 为什么 mounted 里请求返回后组件已经销毁了一个典型的报错在mounted里发起异步请求等接口返回时用户已经切换路由组件已经卸载此时再去更新数据就会报 “Cannot read property of undefined” 或者干脆控制台报警告。解决办法有几个在onUnmounted里取消请求axios 支持AbortController或CancelToken用一个isUnmounted标记位判断组件是否卸载卸载后不再更新状态使用 Vue3 的onScopeDispose来统一处理组合式函数里的异步清理逻辑我通常会在请求函数里加个“组件是否存活”的判断。比如let isAlive true onMounted(async () { const res await fetchData() if (!isAlive) return state.list res.data }) onUnmounted(() { isAlive false })这种写法虽然朴素但能有效防止异步回调去操作已经不存在的组件。5.2 watch 数组新旧值为什么是一样的有个热词问“vue watch 数组的第一项为啥新值和旧值是一样的”这个问题在生命周期语境下也经常被拿来一起聊。其实很简单watch默认监听的是引用类型时新旧值指向同一个引用所以如果你直接修改数组的某个元素Vue 不会立刻得到“新旧两份拷贝”。这跟生命周期没有直接关系但它常出现在组件初始化和数据更新的场景里。如果你需要在 watch 回调里拿到变化前的旧数组建议用浅拷贝或深拷贝来触发引用变化watch( () [...state.list], (newVal, oldVal) { // 此时新旧数组是不同的引用可以对比 } )同理watch加immediate: true时oldVal初始值往往是undefined这也是很多人容易踩的坑。预期“第一次也该有旧值”实际上是拿不到的。5.3 兄弟组件之间的生命周期怎么配合除了父子组件兄弟组件的生命周期顺序其实没有严格的先后关系因为它们各自独立由父组件控制渲染。如果你需要兄弟组件之间通信最常见的方式是状态管理Vuex/Pinia或者通过父组件做中转父组件在某个生命周期里调用子组件的方法。我写过不少维护成本很高的组件通信代码后来总结出一条经验如果两个兄弟组件耦合很深往往说明公共逻辑应该抽到父组件或一个组合式函数里。生命周期是你的工具但别把它当成唯一的通信媒介。5.4 遇到更新顺序问题优先怀疑异步更新队列beforeUpdate和updated这两个钩子在日常业务里相对少用因为它们触发频率高而且容易引来性能问题。一个比较常见的场景是在updated里操作 DOM但每次都操作完下一次数据变化又来了然后陷入循环。这里要强调一点Vue 的 DOM 更新是异步的数据变化后updated要等下一个 tick 才会触发。所以如果你想在数据变化后立刻操作 DOM最好是使用watchnextTick而不是依赖updated。比如watch(() state.keyword, async () { await nextTick() // 此时 DOM 已经更新完可以安全操作 })用watch加nextTick的方式比在updated里判断“哪个数据变了”要可控得多这也是面试里常考察的“异步更新队列”知识点。6. 从“背八股”到“设计组件”生命周期思维6.1 把生命周期当成一种契约来设计组件写多了你会发现真正优秀的组件往往在生命周期里做的事情非常克制。它不会在setup里疯狂请求数据也不会在mounted里堆满各种if判断而是有一套清晰的“阶段计划”生命周期合理职责不合理用法setup / created初始化响应式数据、发起请求操作 DOM、访问不存在的实例属性mounted初始化第三方库、绑定事件频繁请求、同步耗时任务beforeUpdate记录旧数据、做状态暂存触发新的数据更新updated依据最新 DOM 做操作做复杂计算、死循环风险beforeUnmount释放资源、解绑事件发起异步请求、修改全局状态unmounted最终清理确认访问已被销毁的实例如果你的组件能按照这个表格去安排逻辑代码的可维护性会高很多面试官问起来你也能答出“为什么不是所有逻辑都放 mounted”这种设计层面的问题。6.2 组合式函数中的生命周期复用Vue3 的onMounted、onBeforeUnmount等钩子最大的妙处是它们可以写在组合式函数里。也就是说你可以把一个带生命周期逻辑的功能封装成一个useXxx函数然后在任意组件中复用。比如我要写一个“定时刷新列表”的功能我可能会这样组织// useCountdown.js import { ref, onMounted, onBeforeUnmount } from vue export function useCountdown(seconds, onFinish) { const countdown ref(seconds) let timer null const start () { if (timer) return timer setInterval(() { countdown.value-- if (countdown.value 0) { clearInterval(timer) timer null onFinish onFinish() } }, 1000) } const stop () { clearInterval(timer) timer null } onBeforeUnmount(stop) return { countdown, start, stop } }然后在组件里const { countdown, start, stop } useCountdown(60, () { // 倒计时结束后的逻辑 })这种方式把生命周期相关的资源释放都封装在了一个函数内部组件里不用再关心清理逻辑非常干净。这也是我觉得 Vue3 组合式 API 最值得推荐的地方生命周期从“组件级”变成了“逻辑级”。6.3 生命周期与 diff 算法、computed 的关系为了备战面试很多人会把生命周期、diff 算法、computed、watch 这几块知识割裂开背。其实它们是一条链上的数据变了 → 触发watch/computed重新计算 → 触发组件更新 → Vue 用 diff 算法对比新旧虚拟 DOM → 得到需要变更的最小单位 → 执行 DOM 更新 → 触发beforeUpdate/updated。所以你会发现beforeUpdate执行时数据已经变了但 DOM 还没变updated执行时DOM 已经是最新的了。理解了这条链路你就不会再问“为什么在 beforeUpdate 里拿到的 DOM 是旧的”这种问题了。7. 生命周期面试题进阶从源码层面看执行时机7.1 Vue3 中 setup 与生命周期钩子的执行顺序有一些面试官会问到setup里的代码和onMounted里的代码哪个先执行这当然没问题setup先执行onMounted是注册回调真正执行是在挂载完成之后。但有个进阶问题比较刁钻如果setup里既有同步代码又有onMounted还有一个异步的setTimeout它们的执行顺序是什么答案很简单setup同步代码先跑完然后组件挂载触发onMounted最后才是setTimeout里的回调。也就是说onMounted的触发时机是确定的不依赖你写代码的位置依赖的是 Vue 内部渲染流程。源码层面的执行顺序大致是createApp() → 初始化组件实例和 props → setup() → 编译模板生成 render 函数 → 执行 render 生成 vnode → 挂载 DOM → 执行 mounted 钩子onMounted之所以能在挂载后触发是因为 Vue 在注册钩子时把它们推入了一个数组内部会在 mount 流程的对应阶段取出这些回调依次执行。7.2 动态组件与异步组件的生命周期差异如果用动态组件component :iscurrentComponent切换组件时当前展示的组件会走完整的挂载和卸载流程。而如果配合keep-alive则组件不会卸载而是进入deactivated状态。异步组件则略有不同defineAsyncComponent包装的组件在首次加载完成前父组件的mounted可能已经触发了。所以如果你想在父组件里判断异步子组件是否渲染完成不能只看父组件的mounted。这些细节在实际项目中很常见比如在后台管理系统的标签页里切换页面使用的是keep-alive缓存那么组件里的mounted不会每次进入都执行。如果你没有意识到这一点就可能在页面里看到“第一次进入有数据第二次进入数据不刷新”的问题。7.3 关于 errorCaptured 和 onErrorCapturederrorCaptured是生命周期钩子里比较特殊的一个它用来捕获后代组件的错误。Vue3 组合式 API 里对应onErrorCaptured。如果某个子组件抛出了错误从它自身开始错误会沿着父链向上传递直到被某个errorCaptured捕获或全局错误处理。这个钩子的应用价值非常大你可以让父组件捕获子组件的异常渲染一个兜底 UI而不是让整个页面白屏。我曾在一条业务线里给所有异步组件包了一个统一的错误捕获逻辑用户看到错误提示的体验明显好很多。8. 写在最后的经验怎么把这些知识变成自己的如果你只是在面试前临时背一遍生命周期顺序那么大概率过两周就忘了。真正值钱的是在写代码时能主动去思考“现在这个动作发生在哪个阶段会不会影响后续流程”。我自己的学习方法比较“土”抽时间读一遍 Vue 源码中关于组件挂载和更新的核心函数对照官方文档将生命周期串一遍。虽然源码读起来吃力但一旦把“render → patch → mount → update → unmount”这条线串起来再看生命周期就会觉得理所当然而不是靠背单词。一个小技巧以后写组件时在每个钩子前面加一句注释说明这个钩子在当前组件里承担什么职责。如果写到某个钩子时发现想不出职责那说明这个钩子可能根本不需要写。用这种“注释倒逼思考”的方式我后期写的组件生命周期部分越来越精简bug 也少了很多。关于生命周期还有一个延伸方向值得留意Vue 生态里的路由守卫、组件缓存、状态管理插件本质上都是围绕“组件生命周期”做文章。搞懂了生命周期后续再看keep-alive的缓存回收策略、Pinia 在组件卸载时的清理逻辑都会轻松不少。这条线真的值得花时间摸透它几乎串联了整个 Vue 框架的设计哲学。
返回列表