废话不多说直接进入正题。我记得第一次被面试官问到“说一下Vue的生命周期”的时候我背得滚瓜烂熟beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed一共八个。但对方紧接着追问了一句“那如果我想在页面加载完后获取某个元素的高度应该放在哪个钩子里为什么”我当场就有点卡壳因为我知道答案大概是mounted但“为什么”这三个字把我和真正理解生命周期的人区分开了。后来自己也带过新人、复盘过很多次面试才发现生命周期这玩意儿不只是面试里的“八股文高频考点”它直接决定了你写的代码在什么时机执行、能不能拿到你想拿的数据、会不会出现“数据变了但DOM没刷新”这类莫名其妙的问题。这篇是“八股文通关指南”系列的第一篇我打算把Vue生命周期从宏观流程到微观源码、从Vue 2到Vue 3、从正常流程到边界场景一次性拆透。1. 先把生命周期在整个Vue知识体系里的位置钉死1.1 生命周期到底是什么很多人把“生命周期”等同于那八个钩子函数的名字这是最常见的误解。生命周期本身是一个过程是Vue实例从创建、初始化数据、编译模板、挂载DOM、更新数据、到最终销毁的完整时间轴。钩子函数只是这个时间轴上的一些“哨位”Vue在特定时机帮你调用你预先定义好的函数让你有机会在这个时刻插入自己的逻辑。用开一家奶茶店来类比开店前你要租店面、办执照、买设备、招员工这是创建阶段正式营业后每天进货、做奶茶、卖奶茶这是运行阶段最后租约到期、清算设备、关门这是销毁阶段。每一个阶段里都有一些“关键检查点”比如开业前必须检查设备能不能通电打烊后必须盘点库存。Vue的钩子函数就相当于这些关键检查点你不一定在每个检查点都要做事但你必须知道哪些事只能在这个检查点做。1.2 八个钩子的全景对照表从Vue 2到Vue 3钩子名称发生了部分变化用一张表看最清晰时机阶段Vue 2钩子Vue 3选项式Vue 3组合式触发时的状态创建前beforeCreatebeforeCreatesetup()data/methods等尚未初始化无法访问创建后createdcreatedsetup()data已可访问但DOM未挂载挂载前beforeMountbeforeMountonBeforeMount模板已编译但尚未插入页面挂载后mountedmountedonMountedDOM已挂载完成可访问$el更新前beforeUpdatebeforeUpdateonBeforeUpdate数据已变化DOM尚未同步更新后updatedupdatedonUpdatedDOM已同步更新完成卸载前beforeDestroybeforeUnmountonBeforeUnmount实例即将销毁资源仍可用卸载后destroyedunmountedonUnmounted实例已销毁所有指令解绑Vue 3组合式API里beforeCreate和created被setup()取代了后面我会专门讲这个这里先记住结论Vue 3中如果在setup()里写代码它的执行时机比beforeCreate还早所以不需要再关注那两个钩子。1.3 面试回答的推荐结构如果面试官让你介绍一下生命周期别一上来就背八个名字。建议按这个结构回答第一层一句话定义生命周期讲清楚它是一个时间轴钩子是时间轴上的回调。 第二层按创建、挂载、更新、卸载四个阶段逐个说每个钩子讲清楚触发时机和能访问什么。 第三层挑一两个关键钩子展开比如created和mounted的区别、父子组件钩子的执行顺序。 第四层如果面试官还愿意听可以聊Vue 3的变化和keep-alive带来的额外钩子。这个结构既展示了你对基础概念的掌握又能把话题引向更深的维度。2. 从new Vue()到页面呈现挂载流程源码级拆解2.1 beforeCreate和created之间Vue到底做了哪些事很多开发者知道created里可以访问data但不知道“为什么可以”。差距就在源码理解上。new Vue()执行后Vue会调用_init方法里面按顺序做了这些事initLifecycle初始化实例属性比如$parent、$root、$children、$refs等此时这些属性还是空的。initEvents初始化事件监听机制处理父组件传入的自定义事件。initRender初始化$slots、$scopedSlots、$createElement等方法。到这里beforeCreate钩子被触发。所以在这个钩子里你访问不到data、methods、computed、watch因为它们还没初始化。接下来依次执行initInjections初始化依赖注入、initState这是重头戏初始化props、methods、data、computed、watch、initProvide初始化依赖提供。到这一步created钩子触发。此时data已经变成响应式的methods里的方法也可以访问了但实例还没有挂载到DOM上$el属性还是空的。这解释了项目里最常见的报错在created里访问this.$el得到undefined或者在beforeCreate里调用this.methodName()直接报错“is not a function”。2.2 模板编译el、template、render的优先级从created到beforeMount之间Vue要处理一件核心事情把模板编译成render函数。这里涉及到一个经典面试题如果同时存在el、template和render选项哪个优先源码里的判断逻辑是这样的如果传入了render函数直接使用它不再做模板编译。如果没有render看有没有template选项有的话把template编译成render函数。如果既没有render也没有template则把el对应的外部HTML即#app里的内容当作模板进行编译。所以优先级是render template el。这也是为什么很多组件库和工程化项目里顶层组件用的是render: h h(App)这种写法因为render函数性能最好跳过了模板编译的步骤。运行时版本vue.runtime.js甚至都不包含模板编译器只支持render函数写法。2.3 mounted到底什么时候触发很多人的理解是错的compile完成后Vue调用mountComponent函数这就是挂载流程的起点。在这个函数里先创建了更新用的Watcher然后在_update和_render的配合下首次执行渲染。beforeMount钩子就在创建这个Watcher之前触发。首次渲染时_render调用render函数生成虚拟DOMVNode_update把VNode通过patch算法转换成真实DOM并插入到文档流中。到这一步mounted钩子才会触发。但要注意一个细节mounted不代表页面上的图片、字体、异步组件已经全部加载完成。它只代表组件的DOM结构已经挂载到文档里了。如果你要在mounted里测量某个元素渲染后的真实尺寸比如offsetHeight、scrollWidth大多数情况下可以拿到值但如果元素里包含异步加载的资源可能需要配合nextTick甚至setTimeout才能拿到准确结果。这是我实际开发里踩过坑的地方后面会专门说。2.4 用调试工具验证整个流程理论说再多不如自己跑一遍。在Vue 2项目里新建一个组件分别在beforeCreate、created、beforeMount、mounted里打印this.$el和this.someDatabeforeCreate() { console.log(beforeCreate:, this.$el, this.someData) // undefined, undefined }, created() { console.log(created:, this.$el, this.someData) // undefined, hello }, beforeMount() { console.log(beforeMount:, this.$el) // 还是undefinedVue2中 }, mounted() { console.log(mounted:, this.$el) // div idapp.../div }配合Vue DevTools的性能分析面板你能看到每个钩子的调用时间点。这个实验强烈建议自己做一遍亲手看到结果比背十遍文档都管用。3. 每个钩子的正确打开方式该做什么千万别做什么3.1 beforeCreate和created业务逻辑的分水岭beforeCreate在面试里经常被问“能不能访问data”能设计出来的场景是在初始化data之前做一些全局性的配置比如给Vue实例挂载自定义属性或者读取URL参数做灰度判断这些动作不依赖data。但实际项目中我在beforeCreate里写过最多的逻辑是拦截错误——比如全局的错误上报器要在data初始化前准备好这样后续初始化过程中报错也能被捕获。created是业务代码的第一个黄金入口。这时候能访问data和methods适合做初始化非响应式数据比如从localStorage里读取用户信息调用接口获取初始数据大多数公司的代码都是这么写的设置定时器、注册全局事件监听器等但有一个经典争议请求到底放created还是mounted我个人的习惯是如果请求结果只用来赋值给data放created即可因为data已经可用请求早发早回来如果请求结果要操作DOM或者依赖DOM元素的存在放mounted。不过更严谨的说法是在服务端渲染场景下beforeCreate和created是唯二会在服务端执行的钩子所以不能在这两个钩子里访问window、document对象也不建议把请求放在这两个钩子里否则可能导致服务端和客户端重复请求。在纯前端项目里这条约束没这么严格。3.2 beforeMount和mountedDOM可用的临界点beforeMount最实用的场景是在不触发额外更新的前提下修改数据。因为此时render函数即将执行修改data会直接体现在这次渲染中不会触发一次“数据变化→重新渲染”的额外循环。比如你在beforeMount里动态修改主题色、语言包之类的都是可行的。mounted是前端生命周期里最常用的钩子。这里的DOM已经可用你可以通过this.$refs.xxx拿到子组件实例或DOM元素初始化echarts、地图、Swiper等需要依赖真实DOM的第三方库绑定window的resize、scroll事件监听器手动调用子组件的方法需要注意如果父组件里有一段类似v-ifrenderChild的逻辑mounted只保证当前组件的DOM挂载了不保证子组件的DOM也一定挂载了。这个时序问题在父子组件章节里详细讲。3.3 beforeUpdate和updated批量更新的边界beforeUpdate在数据变化后、DOM重新渲染前触发。在这个钩子里可以获取更新前的DOM状态比如记录用户滚动位置、记录旧值等。此时修改数据不会造成循环更新因为本次更新尚未渲染完成。updated是双刃剑。它在DOM重新渲染完成后触发可以读取更新后的DOM比如根据新列表重新计算唯一标识。但它有一个著名的坑不要在updated里直接修改data。因为updated触发的条件就是数据变化导致视图更新你改一下data又会触发一次更新形成无限循环。如果真的要在更新后做数据处理正确的做法是用watch配合nextTick或者干脆只在watch里处理逻辑。我举一个真实踩过的坑电商项目里有一个购物车页面需求是当总价变化时让一个按钮显示“价格变动”的动画。我当时在updated里对比新旧总价然后直接改了data里的showAnimation标记结果页面卡死控制台疯狂报“Maximum call stack size exceeded”。后来改成watchwatch: { totalPrice(newVal, oldVal) { if (newVal ! oldVal) { this.showAnimation true this.$nextTick(() { // 动画结束后把 showAnimation 重置为 false }) } } }3.4 beforeUnmount和unmounted资源清理清单Vue 2里叫beforeDestroy和destroyedVue 3选项式API里改成了beforeUnmount和unmounted含义不变但命名更准确了。beforeUnmount触发时组件还在正常工作data、methods、computed都还能访问DOM也还挂在页面上。这是清理资源的最后时间窗口。我一般会在这里清除setInterval、setTimeout定时器移除window上的事件监听removeEventListener销毁echarts实例、地图实例、Swiper实例取消未完成的异步请求使用AbortController或请求库的取消机制解绑第三方插件自定义的事件监听unmounted触发时组件实例已经被销毁$el已经不存在了。这里能做的清理已经不多通常用来做最终的日志上报或埋点。不清理资源的后果最典型的就是内存泄漏。比如你在mounted里绑定了一个window.addEventListener(scroll, handler)但在销毁时没有移除那么组件销毁后它引用的DOM和状态依然被handler闭包捕获导致内存无法释放。页面不断切换路由内存就不断增长最后浏览器直接白屏。4. 父子组件的生命周期执行顺序面试官最爱让你现场推演4.1 挂载、更新、卸载三张顺序表如果面试只问单个组件的生命周期那太没意思了绝大多数面试官会更进一步问父子组件的生命周期顺序。这个顺序不是背出来的是从Vue的调度机制里推导出来的。先看结论挂载阶段 父组件 beforeCreate → 父组件 created → 父组件 beforeMount → 子组件 beforeCreate → 子组件 created → 子组件 beforeMount → 子组件 mounted → 父组件 mounted更新阶段父组件数据变化引起子组件更新 父组件 beforeUpdate → 子组件 beforeUpdate → 子组件 updated → 父组件 updated更新阶段子组件自身数据变化 子组件 beforeUpdate → 子组件 updated卸载阶段 父组件 beforeUnmount → 子组件 beforeUnmount → 子组件 unmounted → 父组件 unmounted4.2 为什么是这个顺序深度优先和插入顺序要理解这个顺序先理解Vue的挂载机制父组件的beforeMount触发后Vue进入patch阶段这个阶段会深度遍历子组件创建子组件实例、初始化子组件数据、完成子组件挂载等所有子组件都挂载完成父组件的挂载才算结束。可以把父组件想象成一本目录子组件是目录下的章节。翻开目录父组件beforeMount之后你得一页一页写完所有章节子组件们依次走完自己的生命周期最后一章写完整本书才能算出版父组件mounted。这是一个典型的深度优先遍历过程。更新阶段遵循同样的逻辑父组件数据变化Vue开始更新但真正把DOM同步到页面上的过程是从叶子节点开始的所以子组件的beforeUpdate和updated夹在父组件的beforeUpdate和updated之间。4.3 一个实际项目里的例子假设父组件有一个用户列表点击“加载更多”按钮后通过接口获取下一页数据数据传给子组件List渲染。这个过程里父组件的beforeUpdate先触发父组件把新数据传给了子组件子组件接收props变化后进入更新流程子组件beforeUpdate→ 子组件updated等子组件重新渲染完父组件的updated才触发。如果你在父组件updated里用this.$refs.list.xxx去获取子组件更新后的内容一定得放在this.$nextTick里否则拿到的是旧值。反过来如果你在子组件updated里想去修改父组件的状态也会导致不必要的父子联动更新。这种时序理解在实际项目里的价值是它能帮你解释为什么某些代码在特定钩子位置执行会得到预期之外的结果。5. 那些让你的生命周期“不按套路出牌”的边界情况5.1 v-if和v-show对生命周期的“改命”效果很多初学者分不清v-if和v-show对生命周期的影响。结论是v-if为false的组件根本不会创建实例它的beforeCreate、created、beforeMount、mounted一个都不会触发。当v-if切换为true时组件会完整的走一遍创建到挂载流程。v-show只是通过CSS的display控制显示隐藏组件从一开始就创建并挂载了后续切换时不会触发任何创建或销毁钩子。这意味着如果你有一个需要轮询接口的组件用v-if控制显示隐藏每次显示都会重新创建组件、重新发起请求用v-show则不会。反过来如果你想通过“销毁重建”来重置组件内部状态就必须要用v-if。这在实际开发里是很有用的技巧比如搜索页的筛选条件重置用一个v-if包一层组件条件变化时把组件销毁重建内部表单状态自然清零。5.2 keep-alive带来的额外钩子keep-alive是内置组件用于缓存组件实例。被它包裹的组件生命周期会多出两个activated和deactivated。首次进入时组件会照常走完创建到挂载流程然后在mounted之后触发activated。切换到其他路由时组件不会销毁而是触发deactivated此时组件的beforeUnmount和unmounted不会执行。再次切换回来时不会重新触发created和mounted直接触发activated。这对业务的影响很大。最常见的需求是列表页滚动位置保持、离开页面前保存表单草稿。如果你在mounted里绑定了一个事件监听组件被keep-alive缓存后从其他页面回来时mounted不会重新执行你必须把逻辑放在activated里。同理清理逻辑也不要在beforeUnmount里做而应该在deactivated里做。5.3 路由切换时的组件复用created可能不触发如果路由配置是这样的/user/:id从/user/1跳到/user/2组件实例会被复用。此时beforeCreate、created、mounted都不会重新执行只有beforeUpdate和updated会触发。所以依赖路由参数初始化的数据必须用watch监听$route的变化来处理。这也是一个经典的面试题变体。5.4 errorCaptured捕获子组件错误的特别钩子Vue 2.5开始提供了errorCaptured钩子它可以捕获后代组件包括所有子孙级别的错误然后决定是否继续向上传播。这个钩子常被用在业务的基础组件层用来做局部错误边界。我做中后台项目时把每个业务模块的入口组件都加了一个errorCaptured子组件里一旦有渲染错误模块入口会捕获到错误并渲染一个友好提示避免整个页面白屏。5.5 SSR环境下的生命周期限制服务端渲染时只有beforeCreate和created会在服务端执行其余钩子都是在浏览器端执行。所以在created里只要访问了window、documentSSR项目就会直接报错。这也是我前面强调“请求尽量别放beforeCreate/created”的实际原因之一——如果前后端同构接口放在这两个钩子里会执行两次一次在服务端、一次在客户端既浪费资源还可能出现数据不一致。6. Vue 3组合式API生命周期写法变了时机没变6.1 setup替代beforeCreate和createdVue 3组合式API把beforeCreate和created合并进了setup()。setup()的执行时机比beforeCreate还要早它是在props解析完之后、组件实例创建之前执行的。所以你在setup()里可以通过props参数拿到父组件传入的数据但不能访问this因为组件实例还没完全创建好。这意味着Vue 2里写在created里的逻辑在Vue 3组合式API里直接写进setup()就行。注意区别setup()是在组件创建前执行的所以不适合在里面访问this.$refs、this.$el等实例属性。6.2 钩子的注册方式Vue 3组合式API中生命周期钩子都以on开头使用时需要显式导入import { onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onBeforeUnmount } from vue export default { setup() { onBeforeMount(() { // 相当于 Vue2 beforeMount }) onMounted(() { // 相当于 Vue2 mounted }) onBeforeUpdate(() { // 相当于 Vue2 beforeUpdate }) onUpdated(() { // 相当于 Vue2 updated }) onBeforeUnmount(() { // 相当于 Vue2 beforeDestroy }) onUnmounted(() { // 相当于 Vue2 destroyed }) } }一个比较实用的特性是组合式函数里也可以注册生命周期钩子。比如我封装一个“监听元素尺寸变化”的useResizeObserver组合式函数可以在函数内部注册onUnmounted来自动清理监听function useResizeObserver(targetRef, callback) { const observer new ResizeObserver(callback) onMounted(() { observer.observe(targetRef.value) }) onUnmounted(() { observer.disconnect() }) }这样业务组件里就不用再关心清理逻辑了生命周期职责被天然封装到了复用单元里。这是组合式API在生命周期管理上一个很大的优势。6.3 为什么setup里拿不到this这是个高频追问点。setup发生在组件实例创建之前此时data、computed、methods都还没有初始化所以this指向的是undefined在普通函数里会报错在箭头函数里会指向外层作用域表现更诡异。Vue 3之所以这么设计一是为了逻辑复用更清晰二是因为组件实例的创建顺序变了——先通过setup拿到所有逻辑定义再一次性建立响应式系统。所以不要再有“setup里用this拿到data”的想法了。6.4 组合式API下的执行顺序在Vue 3组合式API里如果setup()内部有多个生命周期注册执行顺序按注册的先后顺序排列。如果父组件和子组件都用了组合式API父子组件之间的顺序和Vue 2保持一致父组件 setup → 子组件 setup → 子组件 onMounted → 父组件 onMounted这种顺序和选项式API的嵌套渲染逻辑完全一致不会因为写法变化而改变。6.5 新老写法迁移的一个关键提醒在Vue 3选项式API里你依然可以写mounted、created这些选项,它们和组合式API里的onMounted可以共存。执行顺序是选项式钩子先执行组合式API注册的钩子后执行。迁移项目时如果老代码和新代码在同一组件里都注册了mounted先跑老代码再跑新代码这个顺序是有保证的但为了避免混乱强烈建议迁移过程中统一一种写法别在一棵树上吊两个秋千。回到开头那个面试题。如果现在有人问我获取元素高度应该放哪个钩子我会说推荐放在mounted里配合nextTick。因为mounted确保了DOM结构已经在文档中nextTick确保了同步渲染任务已经完成此时元素尺寸基本稳定。如果元素内部依赖异步加载的图片或字体再用ResizeObserver监听尺寸变化或者配合loading状态在图片完全加载后再测量。这才是真正把生命周期用明白的状态。最后分享一个日常开发的小习惯每写一个组件我都习惯把生命周期钩子按“创建、挂载、更新、卸载”的顺序在代码里从上到下排列然后先写这个组件在各个阶段“必须做的事”再回头补业务逻辑。这样一来代码可读性高自己也清楚这个组件的整个生命周期里到底干了哪些活儿。下次遇到那些“页面刷新后数据千奇百怪”的bug别急着怀疑后端先把生命周期钩子打开看一眼往往答案就在你写代码的那一刻已经注定了。