Code前端首页关于Code前端联系我们

Vue2里的mount到底是干啥的?怎么用?要注意啥?

terry 2个月前 (06-09) 阅读数 93 #Vue
文章标签 Vue2;mount

说起Vue2里的mount,不少刚开始学的同学都会犯懵:它到底是生命周期里的“挂载阶段”,还是手动调用的$mount方法?和页面渲染、DOM操作有啥关系?为啥有时候mount完了拿不到DOM?今天咱们把mount相关的知识点拆碎了讲,从概念到用法,再到避坑和优化,一次性理清楚~

mount在Vue2生命周期里扮演啥角色?

Vue实例从“诞生”到“显示在页面”,要经历生命周期钩子的串联,mount阶段就是把“虚拟的Vue实例”和“真实的DOM元素”绑定起来的核心环节,流程分两步:

第一步是beforeMount钩子:此时Vue已经完成数据观测、事件初始化,但模板还没编译成渲染函数,页面上的$el还是“占位符”(比如new Vue时指定的el对应的DOM元素,此时还没被Vue渲染后的内容替换),举个例子,你在beforeMount里打印this.$el.innerHTML,会发现内容还是页面上原本写的静态HTML,没被Vue处理过。

第二步是mount过程+mounted钩子:Vue会先把模板(不管是template选项、el对应的HTML,还是render函数)编译成渲染函数,再通过渲染函数生成虚拟DOM,最后把虚拟DOM转换成真实DOM,替换掉之前的$el占位符,等这一系列操作完成,就会触发mounted钩子——这时候操作DOM(比如用refs拿元素、修改样式)才真正“生效”,因为DOM已经实实在在挂到页面上了。

另外要注意:如果new Vue时写了el: '#app',Vue会自动调用$mount方法完成挂载;如果没写el,就需要手动调用vm.$mount('#app')来触发挂载,这也是“mount既代表生命周期阶段,又代表$mount方法”的原因~

手动调用$mount和自动挂载有啥不同?

先看自动挂载:写new Vue({ el: '#app', ... })时,Vue会自动找到页面上的#app元素,把实例挂载上去,这种方式适合“页面初始化就渲染”的场景,比如项目入口文件里的根实例。

再看手动调用$mount:场景更灵活,比如做组件库、动态创建弹窗组件时,你可能需要“先创建Vue实例,再决定啥时候挂到页面”,举个代码例子:

// 先创建实例,此时不自动挂载
const vm = new Vue({
  template: '<div>{{ msg }}</div>',
  data() { return { msg: '手动挂载测试' }}
})
// 手动选择挂载时机,比如用户点击按钮后
button.addEventListener('click', () => {
  vm.$mount('#app') // 挂载到#app元素
  // 或者挂载到文档碎片,之后自己插入页面
  // const div = document.createElement('div')
  // vm.$mount(div)
  // document.body.appendChild(div)
})

另外要注意Vue版本差异:Vue有“完整版(带编译器)”和“运行时版”,完整版能把template字符串编译成渲染函数,所以手动挂载时用template没问题;但运行时版没有编译器,必须用render函数,否则会报错,比如运行时版里写:

new Vue({
  // 运行时版这样写会报错!因为没编译器处理template
  template: '<div>{{ msg }}</div>',
  data() { return { msg: 'hi' }}
}).$mount('#app')

这时候得改成render函数:

new Vue({
  render: h => h('div', {}, this.msg),
  data() { return { msg: 'hi' }}
}).$mount('#app')

mount过程中,DOM是怎么“从无到有”的?

很多同学好奇:写个<template><div>{{ msg }}</div></template>,怎么就变成页面上的真实div了?这背后是“模板编译→渲染函数→虚拟DOM→真实DOM”的流程:

  1. 模板编译:Vue先把template字符串(或el对应的HTML)转换成抽象语法树(AST),比如<div>{{ msg }}</div>会被解析成描述标签、属性、插值的AST节点,接着Vue会优化AST,标记出“静态节点”(比如纯文本、不随数据变化的元素),这些节点后续渲染时可以跳过更新,提升性能,最后把AST转换成渲染函数(render function),比如上面的例子会生成类似h('div', {}, this.msg)的函数(hcreateElement的别名)。

  2. 生成虚拟DOM:调用渲染函数,生成虚拟DOM树(本质是JS对象,描述DOM的结构、属性、事件等),虚拟DOM的好处是“轻量”,操作JS对象比直接操作真实DOM快得多。

  3. 挂载真实DOM:Vue会对比虚拟DOM和页面上的$el占位符,通过patch过程把虚拟DOM转换成真实DOM,替换掉原来的$el,完成挂载,这一步完成后,mounted钩子就会触发,页面上就能看到渲染后的内容啦~

为啥mount后有时候拿不到DOM?常见坑怎么避?

遇到“mount完了,DOM还是空的/拿不到refs”,大概率是时机或作用域没搞对,常见场景和解决方法:

场景1:在beforeMount里操作DOM

beforeMount阶段,模板还没编译成渲染函数,$el还是页面上的原始元素,没被Vue替换。

new Vue({
  el: '#app',
  beforeMount() {
    console.log(this.$el.innerHTML) // 打印的是页面上#app原本的内容,不是Vue渲染后的
  }
})

解决:要操作DOM,至少等mounted钩子,因为mounted时DOM已经替换完成。

场景2:mounted里改数据,DOM没及时更新

Vue的DOM更新是异步的,比如mounted里修改数据,Vue会等所有同步代码执行完,再统一更新DOM,如果这时候立刻操作DOM,拿到的还是旧内容,举个例子:

mounted() {
  this.msg = '新内容'
  console.log(this.$el.innerHTML) // 可能还是旧内容,因为DOM更新还没执行
}

解决:用this.$nextTick,等DOM更新后再操作:

mounted() {
  this.msg = '新内容'
  this.$nextTick(() => {
    console.log(this.$el.innerHTML) // 此时能拿到更新后的内容
  })
}

场景3:组件嵌套时,父组件mounted里访问子组件DOM

父组件和子组件的生命周期顺序是:父beforeMount → 子beforeMount → 子mounted → 父mounted,所以父组件mounted时,子组件(同步加载的)已经完成挂载,理论上可以访问子组件的$elrefs;但如果子组件是异步组件(比如用import()加载),父组件mounted时子组件可能还没加载完成,这时候访问就会空。

解决:如果是普通子组件,父mounted里可以安全访问;如果是异步子组件,要么用v-if控制加载状态(等子组件加载完再渲染父组件部分DOM),要么在子组件的mounted里给父组件发通知,再执行DOM操作。

组件化开发中,mount有啥特殊点?

在组件化开发里,每个组件都是独立的Vue实例,所以mount的逻辑会嵌套执行

  1. 父组件与子组件的mount顺序:父组件执行beforeMount → 找到子组件标签,创建子组件实例 → 子组件执行beforeMount → 子组件mounted → 父组件mounted,所以父组件mounted时,所有同步加载的子组件已经完成挂载,这时候父组件里操作子组件的DOM是安全的。

  2. 异步组件的mount:如果子组件是异步加载的(比如const Child = () => import('./Child.vue')),父组件的mounted会先执行,等异步组件加载完成后,才会执行子组件的beforeMountmounted,这时候父组件里如果想访问异步子组件的DOM,必须等子组件加载+挂载完成,可以用<Child v-if="isLoaded" />控制加载状态,或者在异步组件的mounted里通知父组件。

  3. 递归组件的mount:如果组件自己调用自己(比如树形组件),mount过程会递归执行,直到所有层级的组件都挂载完成,这时候要注意内存溢出问题,避免无限递归。

和mount有关的性能优化技巧

想要页面渲染更快、减少mount时的性能开销,可以从这几个角度下手:

技巧1:减少mounted里的同步 heavy 操作

mounted是DOM挂载完成后触发,如果你在这钩子函数里写大量同步逻辑(比如循环计算、DOM频繁操作),会阻塞页面渲染,让用户觉得“页面卡”。解决:把同步逻辑拆成异步(比如用setTimeoutPromise),或者移到created钩子(created时数据已初始化,但DOM还没挂载,适合做数据请求、逻辑计算)。

技巧2:用keep-alive缓存组件,减少重复mount

<keep-alive>是Vue的内置组件,能缓存组件实例,避免组件反复mountunmount,比如Tab切换组件,切换时被隐藏的组件不会销毁,下次切换回来时直接从缓存取,不用重新mount,用法:

<keep-alive>
  <component :is="activeComponent"></component>
</keep-alive>

技巧3:优化模板,减少编译开销

Vue编译模板时,会遍历所有节点,如果模板里有大量静态节点(比如纯展示的图片、文字,不随数据变化),可以用v-once标记,让Vue编译时优化这部分,后续更新时跳过。

<div v-once>{{ 静态文本 }}</div>
<img v-once src="logo.png" />

技巧4:用函数式组件替代普通组件

函数式组件没有自己的实例(少了data、生命周期钩子等),所以mount过程更轻量,适合纯展示、无状态的组件(比如列表项、图标组件),定义方式:

Vue.component('FunctionalComp', {
  functional: true,
  render(h, context) {
    return h('div', context.props.msg)
  }
})

Vue2的mount是“把虚拟实例落地成真实DOM”的核心环节,既涉及生命周期的关键步骤,又有$mount方法的灵活用法,理解它的流程、避坑点和优化技巧,能帮你更丝滑地写Vue代码~下次再遇到“mount后DOM没更新”“手动挂载怎么用”这类问题,就知道从哪下手啦~

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门