先说明一个容易看走眼的细节:这个标题里的“mouted”其实是个笔误,正确拼写是 mounted。不过没关系,面试题本身的价值在于它考察的绝不是单词拼写,而是你对 Vue2 生命周期的理解深度。
我面试过不少人,这道题只要一抛出来,十个人里有八个能答出“created 能访问 data,mounted 能访问 DOM”这种标准答案。再多问一句“为什么 mounted 才能拿到 DOM?这中间 Vue 到底做了什么?”多数人就卡壳了。这篇文章我会尽量把底层机制、源码位置、实操场景和面试话术一次讲透,也顺便聊聊我在真实项目和面试中积累的一些经验和坑。
1. 先审题:这道题到底在问什么
1.1 题目里的两个小坑
第一个坑就是拼写,mouted 显然是 mounted 的笔误,面试笔试题里偶尔会出现这种“人为埋坑”,考察候选人是不是一眼扫过、完全不核对英文。我自己的建议是,哪怕心里知道考官想考的是 mounted,回答时也最好礼貌指出来:“题目里写的 mouted,我理解应该是 mounted。”这不会显得较真,反而让人觉得你细心。
第二个坑藏在题目本身的结构里。它问的是“区别”,但很多人的回答只是把两个钩子的特点分别列出来,像背菜单一样:“created 是实例创建后调用,mounted 是挂载后调用。”这种回答给面试官的感觉是:你背过这个知识点,但未必理解它。真正的区别需要落到“时机、可访问资源、渲染状态、执行场景”四个维度上,后续我会逐个展开。
1.2 前置知识:Vue2 生命周期钩子全家桶
先快速过一遍 Vue2 的生命周期钩子,这样才能把 created 和 mounted 放到完整坐标系里理解。Vue2 一共暴露了 8 个常规钩子:beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed。如果页面里用了 keep-alive,还会多出 activated 和 deactivated 两个缓存相关钩子。
简单记一个口诀:创建 → 挂载 → 更新 → 销毁。created 处于“创建”阶段,mounted 处于“挂载”阶段,这两个阶段之间隔着一个 beforeMount,以及一次完整的模板编译和真实 DOM 渲染过程。很多人只背了钩子名,却不知道中间跨了一次“虚拟 DOM 转真实 DOM”的过程,这正是面试官往深处挖的关键点。
1.3 created 和 mounted 在生命周期中的位置
用一个常见场景描述:当某个组件被创建时,Vue 会先做一些初始化工作,包括响应式数据、计算属性、方法和事件监听。这些初始化完成后,created 被调用。此时组件实例已经有了 data、props、methods,可以直接用,但页面上的真实 DOM 还不存在,this.$el 还是 undefined。
之后 Vue 会继续做模板的编译和渲染,把模板编译成渲染函数,再通过渲染函数生成虚拟 DOM,最终挂载到真实 DOM 上。这一整套流程完成后,mounted 才会被调用。所以从时间轴上来看,created 总是先于 mounted,顺序大概是:beforeCreate → created → beforeMount → mounted。
2. 源码视角:一个“在内存里”,一个“在页面上”
2.1 触发时机到底差在哪
要说清楚这个问题,必须看 Vue2 源码里的初始化流程。在源码的_init方法中,Vue 实例创建后会依次执行initLifecycle、initEvents、initRender,随后调用callHook(vm, 'beforeCreate'),然后执行initState,也就是把 props、methods、data、computed、watch 全部初始化完毕,接着调用callHook(vm, 'created')。
也就是说,created 触发时,实例已经完成了数据相关的初始化。但注意,此时组件还没有进入渲染阶段,模板还没有被编译成渲染函数,更没有生成虚拟 DOM 和真实 DOM。如果你想在 created 里操作 DOM 元素,能拿到的只有空。
而 mounted 的触发位置在mountComponent函数中。Vue 会先创建vm.$el,然后执行updateComponent,它内部会调用渲染函数生成虚拟 DOM,通过patch方法把虚拟 DOM 应用到真实 DOM。整个补丁过程结束后,才调用callHook(vm, 'mounted')。简单的理解就是:mounted 触发时,真实 DOM 已经挂载到页面上了,你不仅能拿到 this.$el,还能通过 this.$el.querySelector 找到里面的子元素。
2.2 从源码位置看两者本质差异
我用一段简化过的源码逻辑来展示这个差异,虽然不算完整源码,但核心位置是准确的:
function initMixin(Vue) { Vue.prototype._init = function (options) { initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') // 数据尚未初始化 initState(vm) // 初始化 props / methods / data / computed / watch callHook(vm, 'created') // 数据已可用,但实例尚未挂载 if (vm.$options.el) { vm.$mount(vm.$options.el) // 进入挂载流程 } } } function mountComponent(vm, el) { vm.$el = el updateComponent = function () { // 渲染函数生成 vnode,patch 到真实 DOM vm._update(vm._render(), hydrating) } // 调用 updateComponent 完成首次渲染 callHook(vm, 'beforeMount') // 挂载前 updateComponent() // 首次渲染 callHook(vm, 'mounted') // 真实 DOM 挂载完成 }可以看出来,created 和 mounted 中间隔了 initState 后续的 $mount 流程,而 $mount 里最关键的动作是_render和_update。虚拟 DOM 到真实 DOM 的 patch 过程就发生在_update里,所以 mounted 能拿到真实节点,这一点从源码上就非常明确。
2.3 几个核心维度对比表
直接给出一张对比表,这是我自己整理和讲课时常用的版本,信息密度比较高,适合收藏或直接背:
| 对比维度 | created | mounted |
|---|---|---|
| 触发时机 | 实例初始化完成,data、props、methods 已可用 | 模板渲染完成,真实 DOM 已挂载到页面 |
| this.$el | undefined,拿不到挂载节点 | 已指向组件的根 DOM 元素 |
| 能否访问 data | 可以,且能对 data 做初始化赋值 | 可以 |
| 能否操作真实 DOM | 不能,DOM 尚未生成 | 可以,子节点也能查询和操作 |
| 模板渲染状态 | 尚未开始渲染 | 已完成首次渲染 |
| 适合做的事情 | 初始化数据、发起请求、事件总线注册 | DOM 测量、图表初始化、第三方库绑定、依赖子组件 ref |
| 执行次数 | 服务器端和客户端各自执行一次(SSR 场景) | 仅客户端执行,SSR 中不执行 |
| 与 nextTick 配合 | 可以配合 nextTick 绕过 DOM 限制 | 本身已处于 DOM 就绪状态 |
2.4 为什么会有这些差异:渲染流水线
很多人不理解,为什么 Vue 不把 created 延后到 DOM 生成后再调用,或者把 mounted 提前到渲染前调用。原因是 Vue2 的渲染流程本身是一条单向流水线:初始化数据 → 编译模板 → 生成虚拟 DOM → 真实 DOM 挂载。每个阶段都有明确的职责,如果提前或延后,会造成职责混乱。
举个例子,created 里如果没有完成模板编译,此时拿到的 data 数值和模板中动态绑定的数值没法统一;如果 mounted 延后到所有数据都渲染完成再调用,那开发者想测量首屏渲染性能就找不到时机。Vue 这样设计,本质上是把“业务逻辑初始化”和“视图挂载”两个关注点彻底拆开了。知道这一点,你在面试里就能答出设计意图,而不只是在背钩子顺序。
3. 实操现场:这些坑我都帮你踩过了
3.1 坑1:在 created 里死活拿不到 DOM
这是新手最容易踩的坑。某次我在一个项目里看到一位同事写的代码,在 created 里直接做this.$refs.xxx.focus(),结果页面一刷新就报错,提示无法读取 undefined 的属性。原因就是 created 阶段还没有渲染出真实 DOM,$refs 里什么都挂不到。
解决办法有两个。第一个是把这个操作搬到 mounted 里,因为挂载后 DOM 一定存在;第二个是保留在 created,但用 nextTick 包裹:
created() { this.$nextTick(() => { this.$refs.input && this.$refs.input.focus() }) }用 nextTick 的原理也简单:它会等这次组件渲染完成后执行回调,相当于在 mounted 之后运行。两者的效果接近,但 mounted 更直观。我的建议是,如果你的逻辑强依赖 DOM,就别懒,直接放到 mounted,语义最清晰。
3.2 坑2:created 里读取元素尺寸全是 0
同样的道理,在 created 里读取offsetWidth、scrollHeight或者调用getBoundingClientRect(),拿到的往往都是 0 或者初始值,因为元素还没被渲染到浏览器里,浏览器根本不知道它的布局信息。
我印象很深的一次是做一个列表懒加载,想提前判断当前容器高度以决定加载条数。当时把测量逻辑放在了 created 里,结果页面一直只加载第一页数据。排查到最后才发现是时机问题——测量发生在渲染之前,容器高度是 0,条件判断失准。后来把测量挪到 mounted,再用window.requestAnimationFrame包一层,确保布局稳定后测量,问题立刻消失。
注意,mounted 也不代表布局一定稳定。如果页面里还有图片、异步字体、动态样式,建议配合this.$nextTick或 window load 事件做二次测量,避免拿到中间态数据。
3.3 坑3:数据请求到底放 created 还是 mounted
这个问题几乎是这道面试题的延伸翻版。我见过不少团队规范里写“数据请求放 created”,理由是“越早请求越快拿到数据,减少白屏时间”。也见过另一派认为“mounted 更稳妥”。从工程实践来看,大多数场景下请求放 created 问题不大,因为请求是异步的,响应回来后渲染阶段大概率已经完成。
举个对比例子:
// 放 created created() { this.fetchList() } // 放 mounted mounted() { this.fetchList() }两者的差别更多体现在时序和严谨性上。放 created,成功后如果立即操作依赖 DOM 的变量,可能组件还没挂载完毕;放 mounted,写代码时不用担心组件未挂载。从性能角度讲,两者对首屏的影响差异微乎其微,因为网络请求耗时远超挂载耗时。所以我个人更推荐“数据初始化放 created,DOM 操作放 mounted”。但如果你在团队里,最好跟随团队规范和既有代码风格,这本身也是回答面试题时能体现工程意识的一点。
3.4 坑4:挂载后要初始化第三方插件怎么办
凡是涉及 echarts、地图、富文本编辑器这类依赖真实 DOM 的第三方插件,必须放在 mounted 里初始化。原因不需要多解释,它们需要拿到一个确定的容器节点,才能执行测量和渲染。
我早期在一个管理后台里写过图表组件,一开始图省事把 echarts 初始化放 created,然后通过this.$refs.chart拿节点,结果每次都是 undefined。后来才理解,ref 的作用时间点是在实例挂载完成后,created 阶段 ref 根本不存在。改成 mounted 后一切正常,所以后来我在团队内部定了条规范:凡是在组件里使用第三方库,初始化统一写 mounted,并配合if (!this.$refs.container) return做防御式判断,避免复用组件时出现异常。
3.5 created + nextTick 的特殊玩法
有些场景里,created 阶段需要先处理数据,数据结果又会触发 DOM 更新,此时可以在 created 里用 nextTick 等 DOM 更新。这种做法在表单校验类组件里很常见,比如需要在初始值变化后立刻更新校验状态。
但要注意,nextTick 在 created 里使用时,它等待的其实是当前渲染任务的完成,不一定等于“整个页面所有子组件都挂载完”。如果组件树特别深,你可能需要多层 nextTick 才能稳定拿到最终 DOM。所以我的经验是:created + nextTick 适合解决单个组件自己的 DOM 问题,不适合处理跨组件、跨层级的 DOM 依赖。
4. 组件化场景:父子和兄弟之间的执行细节
4.1 父子组件生命周期执行顺序
面试官问完刚才那些基础问题后,大概率会追加一道:父子组件的 created 和 mounted 执行顺序是什么?这个知识点如果只背结论,很容易忘。我帮你拆一下。
父组件开始创建时,会先走自己的 beforeCreate、created,此时子组件还没有实例化。接着父组件进入 beforeMount,开始渲染子组件对应的 vnode,此时子组件才开始走自身的初始化流程:beforeCreate → created → beforeMount → mounted。等所有子组件挂载完毕,父组件才真正完成挂载,触发自己的 mounted。
所以经典答案是:父 created → 子 created → 子 mounted → 父 mounted。简化理解成“父先创建,子先挂载”。我面试时特别喜欢用这个问题来考察候选人有没有组件化思维,因为很多人背了顺序却不知道为什么是父 mounted 最后执行——其实是因为父组件的挂载动作依赖所有子组件的渲染完成,就像一个项目要等所有模块交付了才能整体验收。
4.2 mounted 也有“不执行”的时候
如果页面被 keep-alive 包裹,组件首次进入时 created 和 mounted 都会执行,但再次从缓存中激活时,只有 activated,不会重新执行 created 和 mounted。这个细节很多项目里会被忽略,进而引发 bug。
比如一个列表页被 keep-alive 缓存,你在 mounted 里绑定了滚动监听,等用户离开再返回时,监听不会重复绑定,看起来一切正常。但如果你把一些每次进入页面都必须刷新的数据放在 created 里,就会发现数据不更新,因为 created 只执行一次。正确做法是数据刷新放 activated,DOM 初始化放 mounted。这类问题在真实项目中很常见,我在答疑群里看到过至少五次类似求助。
4.3 created 之后、mounted 之前发生了什么
这段过程是面试加分项,也是实际排查问题的重要依据。从 created 到 mounted 之间,Vue 做的主要是:把模板编译成渲染函数,生成虚拟 DOM,然后将虚拟 DOM patch 到真实 DOM 上。如果你用了运行时构建版本且没有编译器,这段逻辑会被前置到构建阶段,但结果一致。
理解这个过程,你就能解释很多现象:为什么 created 里改变 data,页面不会立即重新渲染?因为此时还没有渲染 watcher,数据变化只会被记录,真正的 DOM 更新要等挂载流程触发。为什么 mounted 之后修改 data,页面更新会走 beforeUpdate 和 updated?因为此时渲染 watcher 已经就绪,数据变化会触发重新渲染。
5. 面试答题模板:怎么答出三个层次
5.1 及格层:一句话各说各的
如果你基础一般,至少要能说出:created 时 Vue 实例已经初始化完成,能拿到 data,但拿不到 DOM;mounted 时组件已经被挂载到真实 DOM,能操作节点。这个回答不会得高分,但至少证明你了解基本生命周期概念,不至于被一票否决。
我见过不少候选人连这一层都说不完整,比如把 created 说成“刚开始创建,还没初始化数据”,这就不对了。created 的触发点恰恰是数据初始化完成之后,这也是它名字叫 created 而不是 beforeCreate 的原因。
5.2 良好层:补充时机、场景和模板渲染
在及格层基础上,加上“模板编译”“真实 DOM”“虚拟 DOM patch”这几个词,并给出使用场景的区分。可以这样说:created 在初始化响应式数据后触发,适合做数据初始化、接口请求和事件注册;mounted 在首次渲染完成、真实 DOM 挂载后触发,适合做 DOM 操作、图表初始化和第三方库绑定。
如果能补充“created 阶段 this.$el 还是 undefined,mounted 阶段 this.$el 已经存在”,面试官就会认为你不是死记硬背,而是真的写过代码。这个层次已经超过 70% 的候选人。
5.3 加分层:从源码和工程角度作答
最高级的回答是把它聊成一次架构评审。可以这样组织:先说 created 的触发时机位于 initState 之后、$mount 之前;再说 mounted 触发于 mountComponent 流程的 updateComponent 之后;随后补充渲染 watcher 机制,说明为什么 created 里修改数据不会立即更新到 DOM,而 mounted 之后数据更新会走新的渲染周期。
如果面试官继续追问,你还可以借机提到 SSR 和缓存组件场景,指出 mounted 在服务端不会执行,created 会执行一次,所以服务端数据预取通常放 created;而 keep-alive 缓存场景下 mounted 只执行首次,激活回调需要靠 activated 配合。这一通组合拳下来,面试官会明显感受到你不仅有实战经验,而且有系统化思考能力。
5.4 现场追问演练两则
为了让你面试时更从容,我模拟两段追问场景。
追问一:如果在 created 里请求数据,数据回来时组件已经销毁了,怎么办?回答思路:mounted 阶段可以通过在 created 里记录实例标志位,在请求回调里判断this._isDestroyed,或者用取消令牌避免多次请求;Vue2 没有内置请求取消机制,但可以通过组件销毁钩子标记一个 disposed 状态,回调里判断后不再操作 DOM 和 data。
追问二:created 里能拿到 ref 吗?回答思路:不能。ref 是在渲染完成后才注册到 this.$refs 上的,created 阶段组件尚未渲染,所以拿不到任何 ref。如果一定要在 created 里使用,可以借助 nextTick 延后执行。追问核心是想确认你懂 ref 的注册时机,而不是真的要求你在 created 里强行访问。
5.5 千万别犯的几个错误
第一,不要把 beforeCreate 和 created 混为一谈,beforeCreate 阶段 data 和 methods 都还没初始化,连 this.data 访问都会报 undefined;第二,不要声称 created 阶段页面 DOM 已渲染,这是最典型的硬伤;第三,不要忘记频繁触发 mounted 造成重复初始化,比如用 v-if 来控制组件显示隐藏时,组件每次重建都会重新走一遍生命周期,导致 mounted 重复执行。
我遇到过一位同事,用 v-if 控制图表组件切换,每次切换组件就重建,mounted 里 echarts 不断初始化,最后页面卡顿严重,内存直线上升。后来我把图表初始化抽到子组件内部,并且用 key 保持实例唯一,才解决问题。这个问题本质上不是生命周期钩子选错,而是对“组件销毁重建会重跑生命周期”这个机制理解不够。
6. 由这道题牵出来的延伸知识点
6.1 beforeCreate 和 created 也是常考点
面试官既然问到了 created,顺手就会问 beforeCreate 和它的区别。这个也简单:beforeCreate 阶段 Vue 实例刚被创建,data、methods、props 都没初始化,所以不适合做任何依赖 this 数据的事情。created 阶段这些事情就绪了,可以安全读取和操作。
实际项目里 beforeCreate 用得非常少,少数场景是希望在实例初始化前注入一些全局配置。如果是面试,把 beforeCreate 比喻成“刚出生的婴儿,还没学会说话走路”,created 比喻成“已经会读会写,但还没出门见人”,这种类比比死记硬背强得多。
6.2 beforeMount 和 mounted 别再分不清
beforeMount 发生在模板编译完成之后、首次 DOM 挂载之前。这个阶段虚拟 DOM 已经生成,但尚未 patch 到真实 DOM 上,所以 this.$el 还是旧节点或空节点,页面也看不到新内容。mounted 之前的所有准备工作,包括真实 DOM 创建和插入,都在 beforeMount 到 mounted 之间完成。
很多人把 beforeMount 误认为“DOM 快好了”,其实它只是“马上开始创建 DOM”。如果你要在挂载前做最后一次数据修改,然后直接进入首屏渲染,beforeMount 是个合适的位置,但操作 DOM 仍然不行。
6.3 项目里生命周期钩子怎么选型
我整理了一张选型表,方便你在日常开发中对号入座:
| 需求 | 推荐钩子 | 原因 |
|---|---|---|
| 初始化 data 或从本地存储读取状态 | created | 数据已就绪,时机早,不依赖 DOM |
| 发起数据请求 | created 或 mounted 均可 | 差异小,按团队规范统一 |
| 读取/操作真实 DOM | mounted | DOM 已挂载,最省心 |
| 测量元素尺寸、绑定滚动事件 | mounted | 依赖真实布局,有些需配合 nextTick |
| 初始化 echarts、地图、富文本 | mounted | 第三方库需要真实容器节点 |
| 缓存页面的数据刷新 | activated | keep-alive 缓存后 created 只执行一次 |
| 服务端渲染场景的数据预取 | created | mounted 在服务端不执行 |
6.4 从 Vue2 到 Vue3:setup 替代了谁
如果你对技术趋势敏感,面试官可能还会补一句:Vue3 里 created 和 mounted 有什么区别?Vue3 组合式 API 中,setup 的执行时机相当于 beforeCreate 和 created 的合并,且在两者之前执行。而 mounted 对应的钩子变成了 onMounted。所以整个生命周期模型从 Options API 变成了 Hooks 风格,但核心阶段依然存在。
也就是说,即便换到 Vue3,created 和 mounted 背后的“数据初始化阶段”和“DOM 挂载阶段”的区分依然成立,只是表达方式变了。面试时如果能主动点明这一点,会让面试官觉得你的知识是成体系的,不是停留在某个版本的 API 记忆上。
最后说点我个人的体会。这道歌题之所以被反复拿出来当“每日一题”,恰恰因为它不考记忆,而考理解。我见过太多候选人背下了完整的生命周期钩子顺序表,可代码一写就错:created 里找 DOM、mounted 里做太重的初始化、keep-alive 下数据不刷新。说白了,生命周期钩子不是背出来的,是在真实项目里一点点调试出来的。
建议大家遇到子组件、缓存页、服务端渲染这类场景时,稍微停一下,想一想当前这个操作依赖的是“数据已经准备好”还是“DOM 已经落地”,再决定放哪个钩子。养成这种思维习惯之后,你会发现这道题真的可以拿来检验一个人的前端基础是否扎实。