首页 / 资讯中心 / 文章详情

Vue生命周期完全解读:钩子机制、执行顺序与常见坑位

Vue生命周期完全解读:钩子机制、执行顺序与常见坑位 ★ FEATURED ARTICLE
1. 为什么每个Vue开发者都得弄懂生命周期两个真实场景先讲两个我实际遇到过的现场。第一个是刚入行那会儿同事负责的一个后台管理系统列表页每隔几秒就发一次轮询请求。后来产品说“切到别的页面之后网络面板里还在不停地刷这个接口”。查了很久原因很简单组件销毁的时候忘了清定时器。页面是用Vue Router切走的组件实例已经被卸载了但那个setInterval还活着躲在全局作用域里继续发请求。这就是典型的生命周期“善后”没做干净。第二个场景更普遍就是面试。十个Vue面试题九个绕不开生命周期。什么created和mounted哪个先执行、beforeDestroy里该干什么、父子组件的钩子顺序是什么。问法千奇百怪本质都是同一个东西你是否理解Vue从一个组件诞生到销毁中间到底替你做了什么事又留了哪些时机给你插手。老实说“生命周期”这个概念有点虚不像一个按钮、一个接口那么具体。但恰恰是这种“看不见摸不着”的东西决定了你能不能在正确的时机拿到DOM、发请求、初始化图表、清理副作用。不管你用的是Vue 2还是Vue 3是写选项式API还是组合式API是普通页面还是Electron应用只要你在用Vue这套机制就跑不掉。这篇文章不打算把官方文档复述一遍。我会按组件从生到死的完整路径把每个钩子的执行机制、能干什么、不能干什么、容易踩什么坑结合我自己的实际项目经验一次说透。看完之后你再回头看那两个场景应该就能自己找到答案了。2. “把控制权交还给你”的设计逻辑生命周期到底是什么2.1 框架替你做了很多事但不知道你想什么时候做事要理解生命周期先要理解一件事Vue这个框架本身就是一套“替你做事情”的流程。从你写下一个组件对象开始Vue要帮你完成这些工作初始化data里的响应式数据把每个属性变成可追踪的解析模板里的指令和插值表达式编译成渲染函数执行渲染函数生成虚拟DOM再对比新旧差异把变更同步到真实DOM上监听数据变化触发重新渲染组件切换、页面关闭时释放掉相关的引用和监听器。这套流程本身环环相扣是框架自己控制的。但问题是你自己写的业务代码——比如“页面打开后去后端拉一份用户列表”“按钮点击后把表格里的数据存下来”“页面关闭前把草稿同步到本地”——这些动作必须插在框架的执行过程中而且是有先后顺序的。举个最直观的例子created钩子里能访问this.data因为响应式数据已经初始化好了但如果在这个阶段去操作DOM比如document.getElementById(app).innerHTML大概率拿不到你要的元素因为视图还没渲染出来。这就不是框架不为难你而是你插手的时机不对。所以Vue把整个组件从创建到销毁的过程划分成了几个固定阶段在阶段之间留出钩子Hook。每个钩子就是一个回调函数的注册点到了那个时机Vue就会调用你注册的函数。这种设计说到底就是把控制权在恰当的时机交还给开发者。2.2 生命周期不是Vue的专利一次横向类比如果你之前写过Spring或者Servlet再来看Vue的生命周期会很亲切。Spring的Bean也有完整的生命周期实例化、属性赋值、初始化InitializingBean的afterPropertiesSet、自定义init-method、使用中、销毁DisposableBean的destroy、自定义destroy-method。你可以在Bean初始化之后做数据预加载在销毁之前做资源清理思路和Vue的created、beforeUnmount几乎一模一样。Servlet更典型容器加载Servlet时调用init()做初始化请求来了走service()容器卸载时调用destroy()释放资源。一个Java Web开发者一定会被反复叮嘱“数据库连接要在init里建立在destroy里释放资源”因为Servlet实例是单例的、长期存活的不好好管理资源的释放很容易把连接池耗尽。Vue组件和Servlet、Spring Bean在生命周期上确实是同一个思维模型一个对象被框架托管框架在几个关键节点给你“插一手”的机会。想通了这一点你就能建立一种直觉——任何被框架托管的对象都要搞明白它的出生、存活、死亡三个阶段的处理时机。这也是为什么面试官喜欢横向对比这几个生命周期考察的就是你有没有真正理解框架的托管模型而不是死记硬背钩子函数名。2.3 Vue 2与Vue 3的生命周期钩子命名对照在展开细说之前先把两版钩子对应关系摆出来。很多人从Vue 2切到Vue 3之后看到beforeUnmount、unmounted会愣一下因为Vue 2里叫beforeDestroy和destroyed名字对不上号。阶段Vue 2Vue 3选项式APIVue 3组合式API创建前beforeCreatebeforeCreatesetup()期间创建后createdcreatedsetup()期间挂载前beforeMountbeforeMountonBeforeMount挂载后mountedmountedonMounted更新前beforeUpdatebeforeUpdateonBeforeUpdate更新后updatedupdatedonUpdated卸载前beforeDestroybeforeUnmountonBeforeUnmount卸载后destroyedunmountedonUnmounted缓存激活activatedactivatedonActivated缓存失活deactivateddeactivatedonDeactivatedVue 3把destroy改名为unmount其实更准确因为Vue做的本来就不是“销毁实例”而是“把组件从DOM上卸载、断开响应式依赖连接”。这个命名变化你也可以理解成Vue 3在语义上的一次澄清不是对象被销毁而是组件被卸载。3. 从创建到挂载beforeCreate、created、beforeMount、mounted的执行机制3.1beforeCreate与createddata能不能访问的分水岭beforeCreate是组件生命周期里最早被调用的钩子它在实例初始化之初、data和methods都还没准备好的时候触发。export default { beforeCreate() { console.log(beforeCreate, this.$data); // undefined console.log(beforeCreate, this.someMethod); // undefined }, created() { console.log(created, this.$data); // { articleList: [] }能访问了 console.log(created, this.someMethod); // Function能调用了 }, }这两个钩子之间发生了什么Vue在这一段初始化过程里做了几件事初始化事件相关的属性和回调、初始化inject/provide依赖、初始化props、初始化methods、初始化data、初始化computed和watch。等到created执行的时候绝大部分配置项已经就位。但有一点必须说清楚created阶段仍然不能访问DOM。因为模板还没有渲染$el这个属性此时还是undefined。如果你在created里试图去拿某个DOM节点的宽高拿到的只能是null。实际操作中beforeCreate几乎很少用到。它仅有的价值在于一些极早期的逻辑比如设置实例上的一些全局属性、初始化非响应式的变量、或者做依赖注入的准备工作。如果只是普通的初始化业务数据写created就够了没必要提前到beforeCreate。3.2beforeMount与mounted模板编译到真实DOM的关键节点beforeMount在什么时机触发组件的模板已经编译成了渲染函数虚拟DOM也已经生成但还没有插入真实DOM。所以在这个阶段this.$el仍然是undefined页面上一片空白。再往下走Vue会正式执行渲染函数生成虚拟DOM节点随后把它挂载到真实DOM上。这个动作完成后mounted钩子触发。export default { beforeMount() { console.log(beforeMount $el, this.$el); // undefined }, mounted() { console.log(mounted $el, this.$el); // div idapp.../div }, }这里我认为是整个生命周期里最重要的一个分界线beforeMount意味着“将要渲染的虚拟DOM已经就绪但浏览器里还什么都没有”mounted意味着“组件已经真实插入DOM可以测量元素尺寸、可以直接操作DOM、可以初始化那些依赖DOM的第三方库”。以ECharts为例初始化图表必须放在mounted里mounted() { const chartDom this.$refs.chart; this.chart echarts.init(chartDom); this.chart.setOption({ // 图表配置 }); },如果你放在created里this.$refs.chart取到的是undefinedECharts直接报错。类似的还有选择第三方富文本编辑器、初始化地图实例比如在Vue项目里接入腾讯地图、绑定原生DOM事件等都必须等mounted之后再动手。3.3 初始化请求放在created还是mounted一个务实的选择题很多新手纠结一个问题首屏要拉接口数据请求到底放在created还是mounted里答案是如果你不需要依赖DOMcreated里发请求完全没问题而且是更早的时机请求能更早发出理论上首屏数据回来的更快。这里有个背景知识created阶段虽然拿不到DOM但data里的数据已经初始化好methods也能访问完全可以发起Ajax请求然后把返回值赋给data。等到mounted时数据可能已经回来甚至渲染完成了。但是有一个例外场景如果你的项目做了服务端渲染SSR生命周期钩子只在服务端运行客户端不会执行。这时如果你在mounted里发请求服务端预取的数据就用不上如果放在created里服务端会执行到这一步配合asyncData或者类似的机制反而更合适。当然SSR有自己专门的数据预取方案不在本文展开但你要知道这个差异。考虑到大多数中后台项目是纯客户端渲染我的习惯是和DOM无关的初始化数据放created依赖DOM的操作放mounted。这样既不浪费时机也不会踩空。3.4 一个容易忽视的细节beforeMount到mounted之间模板还没“活”过来我见很多新手在beforeMount里试着操作DOM没拿到元素就以为是自己代码写错了其实是根本没理解这个阶段的含义。beforeMount阶段模板编译完成、虚拟DOM已经产出但真实DOM尚未插入文档流。你把this.$el打出来它存在但内容是空的或者说只是模板对应的占位状态。还有一个细节值得注意如果组件的根节点上有v-if之类的条件渲染beforeMount阶段这些条件还没被真正评估到DOM上而到了mounted阶段条件渲染的结果已经体现在DOM中了。从实用角度讲beforeMount这个钩子的使用频率也不高。它比较适合做一些“渲染前最后一次数据修正”之类的操作比如给某个响应式数据设置默认值让模板真正渲染时拿到的就是最终形态。但这种事情放在created里也能做差异并不大。4. 更新机制beforeUpdate与updated以及那个容易让人翻车的死循环4.1 一次数据变更从发布到DOM更新的完整链路组件挂载完成之后就进入运行态。只要响应式数据发生变化Vue就会启动一次更新流程。整个更新的快照是这样的响应式数据被赋值触发了setter拦截Vue把这个变更通知给组件的渲染Watcher标记组件为“需要重新渲染”在真正执行更新之前Vue调用beforeUpdate钩子此时DOM还是旧状态但数据和虚拟DOM即将改变Vue重新执行渲染函数生成新的虚拟DOM对比新旧虚拟DOM计算出差异diff把变更批量应用到真实DOM真实DOM更新完成后调用updated钩子。看代码更直观export default { data() { return { count: 0 }; }, beforeUpdate() { console.log(beforeUpdate DOM内容, document.getElementById(countText).textContent); // 这里打印出来的还是旧值比如 0 }, updated() { console.log(updated DOM内容, document.getElementById(countText).textContent); // 这里能打印出新值比如 1 }, methods: { add() { this.count; }, }, }需要留心的是Vue的DOM更新是异步的、批量执行的。你连续修改count三次Vue不会立刻更新三次DOM而是把这些变更收集起来在同一个事件循环的微任务阶段统一执行一次更新。这也是为什么updated不会像你的赋值语句一样频繁触发——它是在一次真正的DOM更新完成之后被调用的。4.2updated里改数据一个极其经典的死循环入口这个坑我踩过一次之后再也不敢在updated里无脑写逻辑了。假设有这样一个场景组件里有一个totalPrice你希望在价格变化后把数据同步保存到后端于是顺手在updated里写了一段同步逻辑updated() { this.saveTotalPrice(this.totalPrice); // 这里又改了组件里的另一个响应式数据 },如果saveTotalPrice这个方法内部又修改了某个响应式数据比如把lastSavedAt这个值更新成了当前时间戳那么组件会再次触发更新流程updated会被再次调用然后又触发新的变更……形成一个无限循环。轻则控制台刷屏重则页面卡死。正确的姿势是不要依赖updated去改数据。追踪数据变化用watch副作用操作放在watch回调里处理。watch的作用域更小它精确地监听你指定的数据源不会因为无关的数据变化就被触发。如果你的诉求是“数据变化后在真实DOM更新完成之后执行某个DOM操作”那updated和watch的配合方式也有讲究在watch回调里访问DOM拿到的可能还是更新前的DOM状态需要配合this.$nextTick(() {})确保回调在DOM更新之后执行。watch: { count(newVal, oldVal) { this.$nextTick(() { // 此时DOM已经更新完毕 const el document.getElementById(countText); console.log(el.textContent); // 拿到的是新值 }); }, },5. 卸载之前要做的善后beforeUnmount与unmounted以及被低估的副作用管理5.1 一个“幽灵定时器”的完整事故复盘回到开头说的第一个场景。一个列表页在mounted里启动了轮询mounted() { this.timer setInterval(() { this.refreshList(); }, 3000); },然后你的组件被Vue Router切走了页面却还在持续发请求为什么因为你在mounted里把这个定时器挂到了组件实例上但你从来没有在组件卸载时把它清掉。定时器是全局的不会因为组件实例销毁就自动回收。组件虽然从DOM上卸载了定时器的回调还在全局作用域里周期性地执行不断触发被卸载组件里的方法。修复方案很简单在beforeUnmount阶段清理副作用beforeUnmount() { if (this.timer) { clearInterval(this.timer); this.timer null; } },类似的“善后清单”还有这些定时器setInterval、setTimeout该清都清掉原生事件监听比如你用window.addEventListener(resize, this.onResize)注册了回调必须在beforeUnmount里用removeEventListener移除避免回调泄漏到全局第三方实例ECharts实例调用dispose()、地图实例调用destroy()、WebSocket连接调用close()子组件引用的资源如果子组件内部有自己的资源占用也要在自己的卸载钩子里清理各管各的别指望父组件替你收拾。5.2beforeUnmount与unmounted清理资源的最佳时机选择Vue 3的语义是beforeUnmount在组件实例从DOM卸载之前触发此时组件仍然完全可用——数据还在、事件还能触发、DOM还在。如果你需要在卸载前读取一些状态比如表单草稿快照放在这里最合适。unmounted则是在组件实例被卸载、响应式依赖断开之后触发。此时组件内部的数据、事件监听、指令等都已经清理完毕你无法再依赖这个实例去访问它原来维护的状态。所以实践上有一个通用的经验法则资源清理放在beforeUnmount里更保险因为你还有机会读取到组件最后的完整状态unmounted主要用于最后一步的外部兜底清理但能提前做的尽量提前做。再把视野拉宽一点如果你正在开发Electron应用主进程和渲染进程的通信、IPC事件监听、子窗口的引用都要在对应组件的卸载阶段解除否则Electron应用退出时会报内存泄漏。这个思路和普通Web项目一脉相承只是生命周期钩子同样承担了“进程级清理”的职责。5.3 被keep-alive改变的生命周期activated与deactivatedkeep-alive是Vue内置组件用于缓存组件实例让组件在切换时不被销毁从而保留状态。它一旦介入生命周期就会发生一个很多人容易忽略的变化首次进入时会执行mounted但从缓存中再次进入时只会执行activated不再执行created和mounted组件从页面切走时会执行deactivated而不是unmounted。keep-alive router-view v-ifisKeepAlive / /keep-alive这时候你如果在mounted里初始化了某个操作比如轮询列表、订阅数据切换回来你会发现它不会重新执行因为mounted没有被再次触发。正确的做法是把这个初始化和恢复逻辑放在activated里activated() { this.startPolling(); }, deactivated() { this.stopPolling(); this.resetState(); },如果你用Vue Router的meta字段标记哪些页面需要缓存这也是很多中后台项目的标配做法配合activated和deactivated做状态恢复典型的场景就是列表页滚动位置恢复、搜索条件保留、返回详情页后列表不重新拉取。6. 父子组件的生命周期执行顺序面试答案与真实调试方法6.1 一次嵌套渲染的完整钩子执行顺序组件不是孤立存在的。一个页面通常由父组件嵌套多个子组件组成。Vue的挂载过程是递归的父组件找到模板里的子组件占位符然后去创建子组件实例子组件完成挂载之后父组件才继续完成自己的挂载。完整顺序如下父 beforeCreate 父 created 父 beforeMount 子 beforeCreate 子 created 子 beforeMount 子 mounted 父 mounted这个顺序透露了一个关键信息父组件的beforeMount发生在子组件实例创建之前但父组件的mounted要等到所有子组件都挂载完成之后才触发。也就是说子组件先完成挂载父组件才会走到自己的mounted。这里有一个实际应用如果你在父组件的mounted里去访问子组件的DOM或者调用子组件的方法此时子组件的mounted已经执行过了子组件自身的初始化逻辑也已完成你可以安全地通过$refs拿到子组件实例。6.2 更新与销毁阶段的顺序父先知道子先行动数据更新时父子组件的执行顺序是这样的父 beforeUpdate 子 beforeUpdate 子 updated 父 updated销毁时的顺序父 beforeUnmount 子 beforeUnmount 子 unmounted 父 unmounted这两组顺序的核心逻辑是一致的父组件先发出“要更新了/要卸载了”的信号子组件先完成自己的更新或卸载最后父组件才收尾。这很合理因为子组件是父组件的组成部分父组件必须等子组件处理完才能结束自己的生命周期。理解了这一点一些排查经验也能派上用场。比如你在父组件的beforeUnmount里做清理却发现子组件内部的某些资源没有释放干净往往是因为子组件的清理逻辑写在了自己的beforeUnmount里但父组件的beforeUnmount太早执行了以为能替子组件“兜底”实际兜不住。6.3 用Vue Devtools验证钩子触发比看文档快得多生命周期这东西光看文档总觉得抽象我比较推荐直接用Vue Devtools验证。打开Vue Devtools的组件面板选中一个组件你能看到当前组件的完整状态包括data、props、$el等。但你未必知道的是我们可以直接在钩子里打断点或者在钩子里写日志然后配合Devtools的“性能记录”功能观察每次渲染和更新。做法很简单在created、mounted、updated、beforeUnmount里分别打console.log然后在操作页面时打开控制台的时序记录钩子的触发顺序一目了然。配合Devtools的components面板选中组件后点击刷新你能看到$el从“未定义”到“DOM节点”的变化过程。如果你是Vue 2项目需要确认一下当前浏览器里的是不是Vue Devtools 6Vue 3项目则要装Vue Devtools 7。版本不对组件面板是连不上的。这一步很多新手会卡一下先把版本对齐再谈调试。7. Vue 3的Composition API生命周期钩子换了套写法时机不变7.1setup是“beforeCreate created”的合体Vue 3组合式API里setup是所有组合式逻辑的入口它在组件实例创建之初执行执行时机介于beforeCreate和created之间。官方甚至明确说过beforeCreate和created这两个钩子里的代码在组合式API里直接写进setup就可以因为setup本质上就是在实例初始化过程中被调用的。所以如果你在setup里同时注册onMounted它的触发顺序是在setup执行完之后、组件挂载完成后。用代码理解import { onMounted, onBeforeUnmount, ref } from vue; export default { setup() { const count ref(0); console.log(setup 执行此时相当于 created); onMounted(() { console.log(挂载完成DOM可用); }); onBeforeUnmount(() { console.log(卸载前清理侧副作用); }); return { count }; }, };需要注意setup里可以使用this吗不能。在setup执行阶段组件实例还没完全创建完毕this无法指向当前实例。组合式API的设计意图就是让你摆脱对this的依赖一切数据、方法都通过返回值显式暴露给模板。7.2 组合式API的注册函数与选项式钩子的一一对应选项式API组合式API在setup内调用beforeCreate直接在setup顶层编写代码created直接在setup顶层编写代码beforeMountonBeforeMount(callback)mountedonMounted(callback)beforeUpdateonBeforeUpdate(callback)updatedonUpdated(callback)beforeUnmountonBeforeUnmount(callback)unmountedonUnmounted(callback)activatedonActivated(callback)deactivatedonDeactivated(callback)errorCapturedonErrorCaptured(callback)很多人问选项式API里同一个钩子只能定义一个组合式API里能不能注册多个onMounted答案是可以。这也是组合式API的优势之一——你可以在不同的组合式函数useXxx里各自注册自己的onMountedVue会把它们收集起来在挂载完成后按注册顺序依次调用。这样你就不需要把所有初始化逻辑塞进同一个mounted函数里了。// 组合式函数A function useTracking() { onMounted(() console.log(A统计页面PV)); } // 组合式函数B function useChart() { onMounted(() console.log(B初始化图表)); } export default { setup() { useTracking(); useChart(); }, };两个onMounted都会执行顺序是useTracking里的先跑然后useChart里的再跑。这种“按注册顺序依次触发”的机制让组合式函数之间的生命周期管理变得特别干净。7.3 选项式和组合式混用时的执行顺序现在不少老项目是从Vue 2迁移到Vue 3的项目里会有一段过渡期一个组件里既有选项式API的钩子又有组合式API的setup和onXxx函数。那执行顺序怎么算大致是这样setup先于一切选项式钩子执行——它相当于beforeCreate加created的合计等组件挂载完成后先执行setup里注册的onMounted回调再执行选项式的mounted卸载阶段则是setup里的onBeforeUnmount先执行选项式的beforeUnmount后执行。这背后的逻辑链是组成一个组件的所有逻辑包括组合式函数里的逻辑应该比“默认的选项式逻辑”更早介入初始化、更早完成清理。因为组合式函数是组件能力的扩展基础默认选项式逻辑则像是壳子。写一个小示例验证export default { mounted() { console.log(选项式 mounted); }, setup() { onMounted(() { console.log(组合式 onMounted); }); }, };控制台输出顺序永远是先组合式 onMounted后选项式 mounted。8. 两个容易被面试官追问的扩展钩子errorCaptured与renderTracked/renderTriggered8.1errorCaptured子组件出错时父组件的“拦截网”errorCaptured是一个更冷门但很有用的生命周期钩子用于捕获后代组件的错误类似React的ErrorBoundary。export default { errorCaptured(err, instance, info) { console.error(捕获到子组件错误, err, info); // 返回 false 会阻止错误继续向上传播返回 true 或 undefined 则继续传播 return false; }, };这个钩子在生命周期里的位置比较特殊它不是按固定节奏触发的而是依赖“错误发生”这个事件。一旦后代组件在生命周期钩子、事件处理器、模板渲染等环节抛错这个钩子就会被调用。实际项目中我一般会在根组件上挂一个全局的errorCaptured所有子组件的未捕获异常都会先经过这里配合错误上报系统比如Sentry把现场信息收集起来而不是放任错误在控制台里无声无息地消失。这对线上问题的排查价值很大。8.2renderTracked与renderTriggered调试响应式依赖的利器这两个是在Vue 3里新增的调试钩子常被忽略。renderTracked在组件首次渲染时触发会告诉你模板中实际用到了哪些响应式依赖相当于把组件的“依赖清单”打印出来renderTriggered在响应式依赖被修改、计划触发重新渲染时触发通常先于beforeUpdate执行。export default { renderTriggered(event) { console.log(触发重新渲染的依赖, event.key, event.type); }, };调试一些“数据没变但页面一直重新渲染”的性能问题时这个钩子非常有用。你能直接看到是哪个响应式数据被意外修改从而定位到问题源头。它也是抖音上很多Vue性能优化视频里反复推荐的工具。9. 结合动态路由和keep-alive一次真实的中后台页面生命周期画像热词里有“vue动态路由”和“vue路由参数”。我把它们串起来描述一个中后台项目里常见的组合场景。假设你做一个“订单管理”模块路由是根据用户权限动态注册的列表页和详情页都是同一个路由组件但通过路由参数区分不同的订单ID。为了减少重复请求详情页需要被缓存到keep-alive里同时列表页切走再回来时要保留滚动位置和筛选条件。这个场景下生命周期钩子的配合是这样的用户首次进入列表页beforeCreate-created-beforeMount-mounted-activated用户跳转详情页列表页触发deactivated但组件未卸载用户从详情页返回列表页列表页触发activated此时如果你在activated里写了“重新拉列表”的逻辑每次回来都会重新刷新如果想保留筛选条件只拉当前页面需要的新数据那activated里应该做更精细的判断用户最终登出系统整个路由视图被替换对应页面才触发beforeUnmount和unmounted。这套组合用好了体验会非常顺滑。很多人觉得keep-alive难用、状态容易混乱多数情况下是因为没有正确使用activated/deactivated来区分“首次进入”和“重新进入”。如果配合beforeRouteEnter路由守卫还能做更细的控制但那已经超出生命周期本身属于路由层面的知识。我只提醒一句beforeRouteEnter执行在组件实例创建之前它里面拿不到this这是一个很容易踩的坑——不过它不叫生命周期钩子叫导航守卫别混淆。10. 最后分享一个我自己总结的排查清单在实际项目里遇到和生命周期相关的问题我通常会按这个清单过一遍基本能覆盖90%的场景页面初始化数据没渲染出来检查请求是否在created里发得太早数据回来后DOM已更新但没触发updated相关逻辑或者请求在mounted里发但DOM上依赖数据的地方已经渲染了空态。DOM操作无效或拿不到元素确认操作是不是放在了beforeCreate或created里应该移到mounted。组件切换后还在发请求大概率是定时器或事件监听的清理漏了去beforeUnmount里补clearInterval、removeEventListener。页面从缓存恢复后状态不对检查组件是否被keep-alive包裹确认你的初始化逻辑是否错误地写在了mounted里应该按场景拆到activated里。无限循环更新检查updated里是否改了响应式数据换用watch并配合$nextTick处理DOM更新后的操作。子组件初始化了很多遍看看父组件的模板里是不是用了不合理的v-if结构导致子组件在状态切换时反复创建销毁。我自己做前端这些年最大的感受是生命周期不是面试背题用的它是排查问题时的第一反应工具。遇到一个“行为错乱”的组件先别急着改代码打开控制台把各钩子里的日志打印出来看看到底走了哪条路、错在哪一步大多数问题都能定位到具体阶段。
阅读完成 · 觉得有帮助?
咨询建站