ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Vue 2中created和mounted的区别:时机、DOM与最佳实践

Vue 2中created和mounted的区别:时机、DOM与最佳实践

今天这道题问的是Vue 2里created和mounted的区别,这几乎是每次前端面试都会碰到的常规题。很多同学都背过答案:created早,mounted晚,created拿不到DOM,mounted可以。但一问到“为什么”,就卡壳了。还有不少初学朋友会把mounted拼成mouted,先记住正确拼写,m-o-u-n-t-e-d。这篇文章不打算只给结论,我会从生命周期链路、源码执行顺序、实际项目场景、面试回答四个角度把这俩钩子彻底拆开,看完你不仅能答上这一题,还能连带着把nextTick、render函数、异步请求时机都理解透。

1. 先搞清楚Vue 2实例从生到死的完整链路

1.1 生命周期钩子到底在管什么

Vue 2里每个组件实例都有一个从创建到销毁的过程,这个过程里Vue会在特定时间点自动调用一些函数,这些函数就叫生命周期钩子。你可以把Vue实例想象成一台从“下单”到“报废”的机器:先拿到原料(data),再加工(template编译),然后组装成型(DOM挂载),最后运行(用户交互),直到退役(销毁)。生命周期钩子就是这台机器在不同节点自动触发的“质检回调”,方便我们在合适的时间点做该做的事。

Vue 2默认的生命周期顺序是:beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed。这只是主链路,再加上activated、deactivated、errorCaptured等,但核心就是前面这八个。很多人在项目里只用到created和mounted,对其他的了解不深,这其实会给排查问题带来隐患。比如数据变了但视图不更新,大概率就是没理解updated和渲染机制的关系。

要真正搞懂created和mounted的区别,第一步是把它们放进整条链路里看,而不是孤立地比对。created位于“实例创建后、模板渲染前”的位置,mounted位于“首次渲染完成、DOM真实挂载后”的位置。两者中间还隔着一个beforeMount和一个关键动作——render函数的执行。这个间隔很短,但对结果的影响是质变的。

1.2 created和mounted在链路中的具体位置

先看Vue 2源码里实例初始化的简化流程。在new Vue()之后,核心会执行这么几步:

  1. 初始化事件、生命周期相关属性(initLifecycle、initEvents、initRender)
  2. 调用beforeCreate钩子
  3. 处理注入、状态初始化,包括props、methods、data、computed、watch(initState)
  4. 调用created钩子
  5. 如果有el或执行了$mount,进入模板编译和渲染阶段
  6. 调用beforeMount钩子
  7. 创建组件对应的Watcher,执行render函数生成VNode,再通过patch转换为真实DOM并插入页面
  8. 调用mounted钩子

所以created调用的时候,data和methods已经都能用了,但页面上的DOM还没有生成。mounted调用的时候,DOM已经完成首次挂载,可以直接拿this.$el去操作。中间那一步render函数的执行,决定了能不能访问到DOM。

这里有个关键点:created到mounted之间不是一条直线,它中间走的是“模板编译 → 生成VNode → 生成真实DOM”的过程。如果模板特别庞大,这个间隔会更明显,但在前端感知上仍然是微秒到毫秒级。所以不要认为“created在mounted之前几毫秒”,它们之间隔了整整一个渲染周期。

2. created和mounted的核心差异:时机、DOM、异步请求

2.1 触发时机不同:一个是数据完成,一个是渲染完成

created触发时,Vue实例里的数据已经被挂载到this上了,this.xxx能读到值,this.method也能调用。但此时this.$el还是undefined,因为整个模板还没有开始解析。mounted触发时,this.$el已经指向真实的DOM节点,组件已经渲染在页面上了。

用一个生活化类比:created相当于“菜谱和食材都准备好了”,mounted相当于“菜已经端上桌了”。如果你在created阶段就想夹菜吃,那肯定夹不到,因为锅都还没点着火。

这两者触发时机上的差异,最直接的体现是:在created里访问this.$el返回undefined,在mounted里访问this.$el返回真实的DOM节点。这是判断当前处于哪个阶段的最快方法。

2.2 DOM可用性差异:document.getElementById到底能不能拿到节点

我们经常在项目里用document.getElementById('xxx')或querySelector去拿节点。在created钩子里,这些操作的结果一定是null,因为DOM还没有生成。在mounted里,只要模板里存在对应id的元素,就能拿到。

我自己踩过的坑是,曾经在created里调用一个依赖DOM尺寸的图表初始化函数,结果getElementById返回null,图表直接报错。后来才意识到,created阶段连this.$el都是undefined,更别说内部的子节点了。

那有没有办法在created之后、mounted之前拿到DOM?有,使用this.$nextTick的回调也不行,因为$nextTick在created里调用时,DOM其实还没开始渲染。只有在beforeMount到mounted之间的一些同步render流程里,或者通过vm.$mount之后手动操作,才能更早拿到DOM。不过这些场景比较边缘,正常业务中不需要纠结。

2.3 异步请求放哪:axios请求到底该放created还是mounted

这是面试里最常见的一个追问点。先说结论:大多数情况下,异步请求放在created里完全没问题,而且会比mounted更早发起,能更早拿到数据。Vue官方文档在生命周期示例中也是把请求放在created里。但是有一种情况要特别注意:如果请求的回调里需要操作DOM(比如设置滚动条位置、计算元素宽度),那么回调执行时DOM可能还没渲染完,此时放在mounted会更稳。

很多人说“请求必须放mounted”,理由是mounted之后DOM才存在,回调里能操作DOM。这个说法只对了一半。实际上,异步请求是宏任务或微任务,回调执行时间不确定。如果你在created里发请求,等回调回来时大概率DOM已经挂载完成,因为网络耗时本身就比DOM渲染长得多。只有当接口走缓存、返回极快时,才可能在DOM渲染完成前触发回调。

所以我的建议是:依赖DOM操作的初始化场景放mounted;纯数据请求,用来填充页面数据的,放created。如果实在不确定,可以在回调里加this.$nextTick来兜底。下面我用一个表格对比:

对比维度createdmounted
触发时机实例创建、data/methods初始化后首次DOM渲染挂载完成后
this.$elundefined真实DOM节点
能否访问data能能
能否访问DOM不能能
适合做的事数据初始化、事件监听、异步请求DOM操作、插件初始化、复杂图表渲染
同一次渲染中触发次数仅一次仅一次

3. 这两个钩子的源码级拆解

3.1 从new Vue到$mount到底经历了什么

想彻底理解区别,必须看源码。Vue 2的入口new Vue会调用_init方法,这个方法内部按顺序执行了initLifecycle、initEvents、initRender、callHook(vm, 'beforeCreate')、initInjections、initState、initProvide、callHook(vm, 'created')等。这里注意,beforeCreate的时候,initState还没执行,所以data和methods都访问不到;而created执行完initState之后,数据都已经绑定好并能访问了。

之后如果配置里有el,会自动执行$mount。$mount函数在完整版(包含编译器)和运行时版(只有运行时)里的实现略有不同,但核心都是执行mountComponent。mountComponent里会先执行callHook(vm, 'beforeMount'),然后创建一个Watcher实例,这个Watcher会在组件初始化时执行一次render函数。render函数返回一个VNode树,再经过patch操作把VNode变成真实DOM,并赋值给vm.$el。做完这些之后,callHook(vm, 'mounted')才被调用。

所以created和mounted中间至少隔了:模板编译(如果需要)、render函数执行、patch过程。这个过程中任何一步出错,mounted都不会执行,但created是已经执行完的。这是一条很重要但容易被忽略的经验:created里报错,不影响mounted之前的部分已经完成的工作,但会导致后续流程中断。

3.2 render函数执行和挂载前后状态变化

render函数是Vue 2编译模板的最终产物,它返回VNode。在render执行前,vm.$el还指向挂载前的外部节点(如果在new Vue时指定了el,则指向该元素,但内容还没有被Vue替换);在render执行后、patch完成后,vm.$el被替换为新建的真实DOM节点。mounted钩子就是在patch完成之后触发。

这也是为什么mounted里能拿到this.$el的根节点。而且mounted触发时,子组件的mounted会先于父组件的mounted触发。子组件先挂载完成,父组件再挂载完成。面试中如果被问到“父子组件生命周期顺序”,这其实是送分题:父创建created → 子created → 子mounted → 父mounted。原因就是父组件patch过程中会遇到子组件,子组件会先各自走完自己的生命周期。

另一个容易忽略的点是,mounted只在首次渲染时被调用一次,并不会在每次数据更新时重新触发。数据更新触发的是beforeUpdate和updated。所以不要在mounted里绑定一些会随数据变化而重复执行的逻辑,否则只会在初始化时跑一次,后续更新你得靠watcher或computed。

3.3 为什么mounted里能访问到真实DOM

结合上面的流程,本质原因是:mounted被调用时,vm.$el已经完成赋值指向真实DOM。这个真实DOM是通过render函数生成的VNode树,经过patch算法,与旧VNode对比后插入到DOM树中的。

具体到patch过程,Vue 2是跨平台的,但最终都会调用平台特有的createElement(Web端是document.createElement)来创建节点。这就说明mounted里拿到的DOM是完全真实的浏览器DOM节点,所以你可以直接修改style、调用focus()、设置scrollTop,也可以把它传给第三方库使用。

还有一点值得提:Vue的存储格式是VNode,它只是一个纯JavaScript对象,描述节点的tag、props、children等,并不是真实DOM。在created时,你连VNode都还没有生成,因为render函数尚未执行。到了mounted时,VNode已经完成了到真实DOM的转换,并且真实DOM已经挂载到了document上。这个“从VNode到真实DOM”的瞬间,就是created和mounted的“分界线”。

4. 实际项目中的应用场景与最佳实践

4.1 数据初始化应该在created中做的事

适合放在created里的场景大多是和数据状态相关的。比如:

  • 初始化非DOM相关的数据,比如根据路由参数设置表单默认值
  • 注册全局事件监听(注意要在beforeDestroy里移除)
  • 发起异步请求获取列表数据,拿到后直接更新data,再由响应式系统驱动视图更新
  • 设置定时器等不依赖DOM的逻辑

有一个很典型的例子:如果在created里给window添加了resize监听,回调里只会修改data而不直接操作DOM,那就可以放心放在created。因为回调执行时,this上已经有完整的data和methods了。

还要注意,created里不能确定组件是否真的会被插入视图。比如在动态组件或v-if控制的组件里,created可能执行了但组件最终没被挂载(比如v-if是false,根本不会走到mounted)。所以一些必须依赖真实DOM存在的逻辑,绝不能在created里做。

4.2 DOM操作和第三方库初始化应该在mounted中做

最常见的例子就是初始化图表库:某个组件里有一个<div ref="chart"></div>,mounted里用new Chart(this.$refs.chart, options)来初始化。此时$refs里的元素一定存在,可以安全操作。

还有一类是第三方插件,比如富文本编辑器、地图组件、拖拽排序库,它们的初始化几乎都要传一个真实的DOM容器。放在mounted是最稳妥的。如果放在created,必然报“Cannot read property of null”。

另外,如果你想在组件加载后自动聚焦某个输入框,只能在mounted里this.$refs.input.focus(),因为在created里拿不到这个ref。这里有一个小细节:$refs是在整个组件patch完成后才填充的,所以mounted里访问$refs是安全的,但要注意v-if控制的子元素,如果条件为false,那么该元素不会被渲染,$refs里也不会有对应引用。

4.3 常见错误:在created中操作DOM导致的坑与破解方法

我先说一个真实踩坑经历:一个列表页,需要根据滚动位置来判断是否加载更多。我在created里给window添加了scroll监听,然后在回调里使用了document.querySelector('.list')的offsetHeight。结果首屏加载时,列表高度计算为0,导致没有翻页请求。原因就是created阶段DOM不存在,虽然监听回调运行时DOM已经在了,但首次计算在created之后很快发生,DOM还没完全layout,所以取到0。

像我这种情况,正确的做法是:把对DOM尺寸的读取放到mounted里,或者在created里只做事件注册,回调中所有DOM读取用this.$nextTick包裹。但$nextTick也不是万能的,如果DOM结构复杂,渲染质量不稳定,最保险的还是把初始化逻辑整体放到mounted。

还有一个常见的坑:在created里调用了this.$refs.xxx来访问组件内部方法,结果$refs是空的。记住,$refs只有在渲染完成后才会被填充,created阶段甚至beforeMount阶段都不完整。如果你确实需要在created之后操作子组件,可以用this.$nextTick(() => this.$refs.xxx.someMethod()),但这样绕了一圈,不如直接放mounted直白。

4.4 关于$nextTick的补充和实操建议

很多人会把$nextTick和mounted混在一起。$nextTick是延迟回调到下一次DOM更新循环之后执行,通常用于数据变化后要获取更新后的DOM。在created里,$nextTick的回调会在本次数据更新后的DOM刷新时执行,但初始化时created里用$nextTick,它会在首次渲染完成后执行,类似mounted的时机。

但两者还是有区别:$nextTick可以多次使用,回调数组里所有回调都会在DOM更新后执行;mounted只触发一次。另外,$nextTick在updated之后也可以用来拿到最新DOM。所以,如果你在created里突然发现代码需要读DOM,不一定要迁移到mounted,写this.$nextTick(() => { ... })也能达到类似mounted的效果,不过语义上不如直接放在mounted清晰。

我个人的习惯是:能用mounted解决的,不依赖$nextTick在created里兜底。原因很简单,mounted本身就是为“渲染后需要执行的逻辑”设计的,你非要在created里用$nextTick绕一圈,以后维护代码的人容易产生误解。当然,如果从“更早发起请求”的角度,created加$nextTick加载数据可行,但我更推荐数据放created,DOM操作放mounted,职责分离清晰。

5. 面试和多选题中的高赞回答思路

5.1 一句话概括加延伸:怎么把这道题答得漂亮

面试官问“created和mounted的区别”,不要只回答“created早,mounted晚”,那样太单薄。高分的回答应该分三层:

第一层:各自触发时机不同。created在实例初始化完成后、模板渲染前触发,此时data和methods可用,但DOM未生成。mounted在DOM首次渲染并挂载完成后触发,此时可通过this.$el访问真实DOM。

第二层:能力差异。created里不能操作DOM,mounted里能操作DOM。mounted里能拿到$refs,created里不行。

第三层:适用场景。数据初始化、异步请求放created;DOM初始化、第三方插件初始化放mounted。如果面试官继续追问,你可以提父子组件的生命周期顺序:父组件的created先于子组件的created,子组件的mounted先于父组件的mounted。

这样回答,既覆盖了基础问题,又展现了你对生命周期机制的深入理解,还能延伸出更多细节,面试官会认为你不是单纯背答案。

5.2 如何回答“哪个生命周期适合发请求”

这道题几乎是固定追问。我的标准回答是:

“单纯发请求并更新data,放在created更合适,因为可以更早获取数据,减少白屏时间。但如果请求回调里需要操作DOM(比如拿到数据后计算图表宽度并渲染),那放在mounted会更安全,或者放在mounted后再发请求。实际上,现代前端项目大多用路由懒加载和异步组件,组件实例化前已经做了很多优化,请求放created还是mounted对性能影响微乎其微。关键是不要让回调在DOM挂载前执行,导致无法操作DOM。”

再往深一点说,你可以提到在created里发起请求时,如果组件被销毁(比如路由切换),请求回调可能会在组件销毁后才返回。这时候在created里做Promise的catch或then里访问this时,组件实例已经销毁,虽然Vue不会报错,但会存在不必要的内存占用和潜在问题。推荐在beforeDestroy里取消订阅或忽略回调。这部分属于延伸,如果面试官有兴致,可以聊很久。

5.3 写在最后:一个我常用的调试小技巧

平时我在开发时,会在created和mounted里分别打印this.$el和this.$refs,快速观察它们的值。如果输出undefined或空对象,说明过程仍在渲染前;如果输出真实DOM或已填充的refs,说明已经挂载完成。这是调试生命周期问题的最快方法。

另外,如果你发现组件里created在跑,但mounted一直没触发,大概率是渲染出错,打开控制台先看有没有红色报错,比如模板里的某个变量不存在或某个组件未被正确注册。mounted不执行不代表整个组件没创建,只是渲染流程中断了。这时候优先排查render函数和模板语法。这个排查经验我遇到过很多次,希望能帮你省点debug时间。

最后再分享一个操作习惯:如果页面同时需要“发数据请求”和“操作DOM”,我会把请求放在created,然后在数据回调里用this.$nextTick等待DOM渲染完成,再操作DOM。但如果逻辑本身不复杂,直接都放mounted更省心。项目没有唯一方案,关键是理解每个钩子的能力边界,再根据业务场景做取舍。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进