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

资讯详情

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

Vue3生命周期全解析:钩子函数、执行顺序与实战避坑指南

Vue3生命周期全解析:钩子函数、执行顺序与实战避坑指南 1. 生命周期到底在管什么先别急着背钩子函数很多人学Vue3生命周期第一反应就是打开官方文档对着那张生命周期图开始背onBeforeMount、onMounted、onBeforeUpdate、onUpdated……背完就忘忘了再看看完还是不知道什么时候该用哪个。我带的团队里新人问我最多的问题不是生命周期有哪几个而是这东西我到底什么时候能用上。先说个最直观的认知生命周期本质上是框架给你暴露的几个时间节点让你能在组件从创建到销毁的各个阶段插手干活。Vue3的Composition API把这一套逻辑从配置项里写函数变成了在setup里调钩子但底层的时序逻辑并没有变——组件还是那套生老病死的流程只是换了一种更灵活的方式让你响应这些时刻。为什么Vue3要改成setup onXxx函数的形式核心原因有两个第一Options API的created / mounted / destroyed这类散落在各个选项里的生命周期函数在逻辑聚合上很被动——你想把监听某个事件和组件销毁时取消监听这两件事写在一起不好意思它们必须分家到created和destroyed里。Composition API用函数调用的方式让你能在同一个代码块里把进入时的初始化和退出时的清理放在一起逻辑内聚性好太多。第二setup本身取代了beforeCreate和created的位置所有代码都在setup执行期间跑不再区分实例已创建但属性未初始化这类细粒度阶段整体心智负担更轻。那生命周期到底管什么说白了就是三件事资源准备、状态更新、资源清理。组件挂载前你要准备数据、初始化状态组件数据变化后你要响应更新比如重新计算、操作DOM组件销毁前你要清理定时器、取消订阅、解绑事件。所有生命周期钩子无论名字怎么变本质都在服务这三类诉求。这一章我们先建立整体框架后面的内容再逐层拆解。注意Vue3的setup执行时机非常早它同时取代了beforeCreate和created。你在setup里做的一切初始化响应式状态声明、变量定义都已经算是在组件实例创建阶段进行的操作了。2. 组合式API下的钩子全拆解一条时间线走完组件的一生2.1 挂载阶段从beforeMount到mounted这中间发生了什么Vue3的挂载阶段包含两个钩子onBeforeMount和onMounted。我直接说真实场景下的体感差异onBeforeMount触发时组件对应的真实DOM还没有插入文档里但Vue已经拿到了模板的渲染函数并且完成了数据到虚拟DOM的首次渲染。这个阶段你能拿到的还是虚拟DOM访问不到实际页面元素。有个我常用的判断技巧如果你要在挂载前做最后的配置调整比如修改某些传给子组件的props值或者读取响应式数据来计算某个样式状态放在onBeforeMount里是合理的但如果你要操作DOM、测量元素尺寸、绑定窗口事件就必须等到onMounted。因为到了onMounted组件已经真实付到页面上了this.$el如果用Options API的话或通过模板ref引用的元素此刻才真正可用。onMounted是实际项目里用得最多的钩子之一。数据请求、DOM操作、事件监听、图表初始化几乎都在这里做。但要特别注意onMounted里拿到的DOM是当前渲染完成的DOM不是永远不变的DOM。如果组件数据后续变化触发重新渲染你在onMounted里绑定的某些东西可能就过期了这就要配合下一个阶段处理。我举个例子非常直观你做一个列表组件初次加载时需要在onMounted里拉取列表数据然后渲染。结果某天产品说列表要支持筛选参数变了要重新请求。你如果只在onMounted里写请求逻辑筛选条件变化后根本不会触发重新拉取。这时候你需要的是watchffect或者onBeforeUpdate/onUpdated组合来处理更新阶段。2.2 更新阶段onBeforeUpdate与onUpdated的实战定位更新阶段的触发条件是组件内部任意响应式数据发生变化且组件的模板依赖了该数据。注意这个前提——如果数据变了但没被模板使用不会触发组件更新。很多人在这里困惑其实是混淆了响应式数据变化和组件重新渲染两回事。onBeforeUpdate触发在数据变了但DOM还没重新渲染的临界点。这个阶段最适合做一件事获取旧值快照。比如你有个可拖拽的面板组件拖拽过程中panelWidth这个响应式值一直变你想在每次重绘前记录旧的宽度值用于计算偏移量放这里就对了。另外这个阶段千万别修改那些会导致再次渲染的状态否则会进入更新死循环这是新手极容易踩的坑。onUpdated则是在DOM更新完成后触发。这里你拿到的是最新渲染结果适合做依赖新DOM状态的操作。比如某个状态变化后需要对某个元素焦点管理——渲染完拿到新的DOM节点直接调focus方法。再比如表格组件列宽变化后要重新计算滚动条位置也可以放这。但我要提个反面意见如果你的代码里频繁使用onUpdated往往说明设计思路出了问题。正确的做法是优先考虑使用计算属性推导值、或者watch监听数据变化触发副作用而不是每次都等DOM更新完再手动操作。onUpdated更像兜底方案而不是常规方案。大多数场景下watchEffect配合flush: post选项能更精准地做到数据变化后、DOM更新后再执行而且写法更内聚。2.3 卸载阶段组件“死亡”前的最后救赎onBeforeUnmount和onUnmounted这两个钩子对应的就是组件实例销毁过程。onBeforeUnmount触发时组件还是完整的DOM还挂在树上可以在这里做最后的通知或状态上报onUnmounted触发表示组件实例已经从DOM结构中移除所有指令的解绑、子组件的销毁都已经完成实例的响应式依赖也停掉了。我之前遇到一个实际事故某个监控大屏页面组件里注册了window.addEventListener(resize, handler)但卸载时忘了移除。结果这个组件每次切走再切回来就多一个监听器页面频繁切换几十次后resize事件触发一次handler连带调用几十次每次都在操作已经不存在的组件状态轻则内存泄漏重则控制台疯狂报错。这就是典型的资源清理没做好。正确的操作姿势是但凡在生命周期里注册了外部资源事件监听、定时器、WebSocket连接、IntersectionObserver实例一律在onBeforeUnmount或onUnmounted里成对清理。为什么我建议优先用onBeforeUnmount因为此时组件实例仍然可用如果你想在清理之前读取一下当前组件的某些状态作为判断条件还能拿到到了onUnmounted阶段虽然组件相关资源基本还在局部作用域内可访问但语义上已经进入销毁后了再做复杂逻辑容易踩到不可预期的问题。2.4 三个容易忽略的钩子onActivated、onDeactivated、onErrorCaptured挂载、更新、卸载是主线的三个阶段但实际业务里还有三个钩子使用频率也不低分别是onActivated、onDeactivated和onErrorCaptured很多初学者甚至都不知道它们的存在。onActivated和onDeactivated这两兄弟专门配套KeepAlive组件使用。当组件被keep-alive缓存后它不会被销毁只是失活——即从DOM中移除但实例保留。每次从缓存中重新插入页面时触发onActivated插入缓存区隐藏时触发onDeactivated。这相当于给组件增加了睡眠和唤醒两道生命周期。实际项目里最常见的场景就是列表页和详情页互相切换列表页滚动了800px后点进详情返回时希望列表页还保持原来的滚动位置和筛选条件。用KeepAlive包裹列表组件再配合onActivated里恢复滚动位置体验非常顺滑。onErrorCaptured则是一个专门接收后代组件抛错信息的钩子。它接收三个参数错误对象、实例、以及一个包含错误来源信息的状态对象。这个钩子主要用于错误边界处理——你可以在这个钩子里捕获子组件渲染过程中的错误阻止错误向上传播到全局同时记录错误信息用于上报。需要注意的是错误被捕获后你需要在钩子里自行决定是否继续抛出。如果不处理错误会继续向上冒泡如果返回false注意是返回false不是不抛错则会阻止错误继续传播。3. 父子组件与异步场景的执行顺序面试必问开发必踩3.1 父子组件挂载顺序一张表看懂生命周期在父子组件之间的执行顺序是面试高频题也是在真实开发里排查为什么子组件的数据我在父组件里拿不到这类问题的基础认知。Vue3的机制是这样的父组件的挂载过程会先触发自身beforeMount阶段接着进入子组件的挂载流程子组件完整挂载完成后父组件的mounted才触发。简单说就是父组件先开始子组件先完成。整理成表格更直观执行次序钩子所属组件1onBeforeMount父组件2onBeforeMount子组件3onMounted子组件4onMounted父组件更新阶段的逻辑也类似父组件的beforeUpdate先触发然后子组件的beforeUpdate和updated依次完成最后父组件的updated才触发。卸载阶段方向相反父组件的beforeUnmount先触发然后子组件的beforeUnmount和unmounted依次执行最后才是父组件的unmounted。实际操作心得如果你要在父组件mountd后读取子组件某个通过请求拿到的数据而子组件的数据请求也是写在onMounted里那么父组件的onMounted执行时子组件的onMounted肯定已经执行完了数据很可能还处于请求刚发出、响应未返回的状态。请求回来的异步回调根本不在这个流程里所以别指望通过生命周期顺序来拿到异步数据正确方式是子组件通过defineExpose暴露数据父组件用ref加watch机制去监听。3.2 异步组件配合Suspense生命周期从哪开始变脸Vue3引入的异步组件和Suspense让组件加载流程更灵活但也让生命周期时序产生了变化。如果组件是通过defineAsyncComponent加载的异步组件并且它内部还有异步依赖比如setup函数是async的那么组件的生命周期时间线会被拉长。这里的关键点是setup函数变成异步之后onBeforeMount / onMounted这类钩子实际上是在异步setup完成后才注册和执行的。换句话说Suspense的等待阶段对应的是异步setup的执行期此时组件的挂载尚未开始你写的所有onXxx钩子都处于待命状态。等异步逻辑resolve、组件正式渲染后续生命周期才依次触发。这个机制带来的实际影响是如果你在异步setup里直接访问某个DOM ref拿到的必然是undefined——因为整个挂载流程还没开始。我见过不止一位同事在这种场景下把ref放在setup顶层读取结果怎么调都是null。正确做法是把需要在挂载后执行的逻辑放到onMounted里或者在Suspense的fallback里做好占位展示不要试图在异步setup里完成与DOM相关的操作。4. 实操中容易踩的坑与排查思路把走过的弯路一次说清4.1 坑一onMounted里请求接口依赖刷新时数据不更新这个坑太经典了。我在内部培训时经常拿它举例子页面有个Tab切换Tab变化后需要根据当前Tab重新拉取最新列表数据。不少新手把请求逻辑写在onMounted里结果Tab切换后数据一动不动。原因前面提过——onMounted只在组件初次挂载时执行一次后续数据变化触发的是更新阶段。我个人的推荐方案是不需要给请求单独建一个函数然后在onMounted和watch里各调一次直接用watchEffect就能一次搞定。因为watchEffect默认会追踪它的依赖初次执行一次依赖变化后再执行一次。代码长这样const tab ref(all) const list ref([]) // 初次执行 tab每次变化时重新执行 watchEffect(async () { const currentTab tab.value list.value await fetchList(currentTab) })简单把这个改造成可复用逻辑也方便——把请求逻辑提取成loadData函数在需要精确控制触发时机的时候再结合watch去调用。但日常处理数据跟着参数走的场景watchEffect是心智负担最小的方案。4.2 坑二在onBeforeUpdate里修改响应式数据导致死循环正常情况下onBeforeUpdate执行时意味着数据和DOM即将进入更新流程。如果你在这个钩子里又修改了某个模板依赖的响应式数据Vue会再次触发更新流程然后onBeforeUpdate又执行一次你又改数据……好了死循环。这个坑的隐蔽之处在于有时候数据变了不一定导致视图更新——如果这个数据不参与模板渲染你改了也不会触发更新。但一旦它被模板引用问题就炸了。我的建议是养成习惯onBeforeUpdate里只读状态、记录状态永远不要写状态。真要在更新流程里改数据就用watch flush: post在DOM更新后统一处理。4.3 坑三KeepAlive缓存后onMounted只执行一次数据却是旧的不少项目里用了KeepAlive缓存列表页结果发现切回来之后列表依旧停留在旧状态。原因同样是onMounted只在组件首次挂载时执行被缓存后组件并没有重新挂载所以onMounted不会再触发。这时候要刷新数据就得在onActivated里做——组件每次从失活状态恢复激活时onActivated都会执行这是处理缓存页面数据刷新的最佳时机。但这里也有个小细节onActivated里写请求首次进入页面时组件处于挂载状态流程是先onMounted再onActivated请求逻辑会在onMounted和onActivated里分别执行一次导致首次加载发两个一模一样的请求。我的做法是加一个标记位控制首帧跳过或者把数据刷新统一放在onActivated里onMounted里什么都不写。两者选其一并让整个团队保持一致。4.4 排查工具与调试思路不是瞎猜是精准定位生命周期问题排查最有效的工具就是Vue DevTools的组件树和事件流面板。打开DevTools切换组件时能看到当前活跃组件是高亮的选中后可以查看该组件的生命周期状态、props、响应式数据。配合Console里的日志基本能把生命周期时序理顺。我自己调试时常用的套路是给每个钩子加带标记的console.log不是打单个词而是打组件名 钩子名 关键参数。比如在子组件onMounted里打console.log([Child] mounted, props: , props.id)。这样在控制台里利用过滤功能输入子组件名所有钩子的执行顺序一目了然。等排查完了再把日志删掉就好。这个方法看起来土但比脑子里空想执行顺序有效得多。5. 生命周期钩子的最佳实践清单照着做能少走很多弯路这一节把前面提到过的经验整理成可以直接参考的实践清单。不是什么高深理论是我和团队在项目里沉淀出来的行为准则。初始化操作的放置原则数据声明、非DOM依赖的逻辑计算属性、watch配置、工具类初始化放setup顶层DOM操作、窗口事件绑定、图表初始化放onMounted需要在挂载前对props/state做最终调整的逻辑放onBeforeMount异步获取数据推荐watchEffect而不是只依赖onMounted资源清理的强制规范所有window/document级别的事件监听必须在onBeforeUnmount里移除setInterval/setTimeout必须持有句柄并在onBeforeUnmount里clearIntersectionObserver、ResizeObserver、MutationObserver等观察器实例一律配对disconnect第三方库实例图表实例、地图实例如果有销毁方法在onBeforeUnmount里调用状态同步的判断依据如果逻辑是在数据变化后、渲染前执行比如保留旧UI状态用onBeforeUpdate如果逻辑是在渲染完成后执行比如基于新DOM结构的操作用watch flush: post更好如果逻辑不依赖DOM纯数据层面联动优先用watchEffect/watch我把这三点展开说一下细节。窗口事件监听是重灾区很多人记得绑不记得解。好的习惯是绑定和解绑写在一个逻辑块里这样代码审查的时候一眼就能看出配对关系。第三方库实例的销毁稍微麻烦一点因为某些库没有明确的destroy方法或者销毁了还会残留全局状态建议封装一层工具模块统一管理生命周期。团队协作提醒建议在项目的代码规范文档里明确规定凡在onMounted中注册的资源必须在onBeforeUnmount中释放。代码review时把这个作为必查项可以拦下大多数内存泄漏隐患。6. 一个完整案例从需求到生命周期的落地方案我拿一个实际做过的需求来讲透生命周期怎么排布。某个商品管理后台页面页面结构是顶部筛选条件中间是商品表格选中某个商品后侧边弹出详情面板详情面板里可以切换Tab查看图文详情和规格参数。这个页面的组件结构是GoodsList页面组件包含FilterBar和GoodsTable子组件详情面板DetailDrawer组件通过v-model控制显示内部三个Tab分别是ProductOverview、ProductSpecs、ProductImages三个子组件。整个需求里的生命周期安排是这样的FilterBar组件在setup里声明筛选表单的响应式状态onMounted里根据路由参数比如进入页面时自动带上某个预设分类初始化表单的值然后通过emit把筛选条件告诉父页面。GoodsTable组件onMounted里发首次请求拿第一页数据。父组件监听FilterBar的search事件事件触发后更新表格的查询参数。G服务器用watch监听查询参数变化在参数变化后重新请求表格数据。注意这里是watch不是onUpdated——因为这里只需要在数据变化时触发副作用不关心DOM渲染后的状态。DetailDrawer组件通过props接收来自父组件的selectedGoods对象内部用watch监听该对象来初始化详情区域的状态用onBeforeUpdate记录上一次选中的商品ID当商品切换时做动画过渡。三个Tab子组件各自在onMounted时请求自己的详情数据如果做了KeepAlive缓存则在onActivated时根据当前商品ID判断是否需要重新拉取——如果ID变了就清掉旧数据重新请求。页面卸载时GoodsTable组件在onBeforeUnmount里取消掉没完成的请求通过AbortController散掉分页定时器DetailDrawer组件解绑侧边栏的键盘关闭事件监听用户按Esc关闭面板的需求。你看整个需求里几乎每个生命周期钩子都用上了而且每个钩子所做的活都是它最擅长的那一类挂载时完成初始请求、更新前记录旧状态、更新后根据新DOM做视觉反馈、卸载时做资源清理、激活时做缓存数据刷新。这才是生命周期在真实业务里的正确打开方式——不是一个一个硬套而是出现什么问题就找对应的生命周期阶段去精准响应。我在实际项目中还有个坚持了很久的习惯在需求开发前先在脑内过一遍这个组件的数据从哪里来、什么时候变、变完之后要影响什么、销毁前要归还什么。想完这四个问题生命周期该用在哪些地方基本就自动浮现了。这个思考顺序比死记钩子清单有用得多。最后说一个容易被忽视的小技巧在组合式函数composable里同样可以使用onMounted、onBeforeUnmount这些钩子。这意味着你可以把初始化某套逻辑 清理对应资源封装成一个独立的useXxx函数在不同组件里复用。比如封装一个useWindowSize内部注册resize监听返回响应式的宽高值组件里用了就自动监听组件卸载就自动清理import { ref, onMounted, onBeforeUnmount } from vue export function useWindowSize() { const width ref(window.innerWidth) const height ref(window.innerHeight) function updateSize() { width.value window.innerWidth height.value window.innerHeight } onMounted(() { window.addEventListener(resize, updateSize) }) onBeforeUnmount(() { window.removeEventListener(resize, updateSize) }) return { width, height } }这种资源获取与释放封装在组合式函数里的模式才是Vue3组合式API在设计生命周期能力时真正想推的东西。生命周期钩子不再仅仅是组件配置项里的几个补丁点而是成为构建可复用的状态逻辑时顺手可用的底层能力。把这个思维转变过来你对Vue3生命周期的理解就上了一个台阶。
返回列表