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

Vue2里的mounted钩子到底怎么用?常见问题一次说清

terry 2周前 (06-09) 阅读数 50 #Vue
文章标签 Vue2 mounted

很多刚开始学Vue2的同学,一到mounted钩子这块就犯懵——它啥时候执行?能做哪些事?和created有啥区别?碰到异步请求、DOM操作出问题咋整?今天咱就把mounted的常见疑问掰碎了讲,从基础到实战坑点全覆盖,帮你把这个生命周期钩子吃透~

mounted是Vue2生命周期里的哪个阶段?

Vue2的组件从“出生”到“渲染到页面”要经历一系列生命周期阶段,mounted 属于“挂载阶段”的最后一步,咱拆解下整个流程:

  1. 先经历 beforeCreate(实例刚创建,数据和事件都没初始化)、created(数据观测、事件配置完,但DOM还没影);
  2. 接着进入挂载阶段:beforeMount(模板开始编译,但真实DOM还没渲染出来)→ mounted(模板编译成真实DOM,并且已经挂载到页面上了)。

简单说,mounted 触发时,组件的HTML结构已经实实在在出现在页面里,这时候你想操作页面上的按钮、div这些DOM元素,终于能“抓”到它们了~

举个直观例子:组件里有个 <div ref="test">测试文本</div>,如果在 created 里打印 this.$refs.test,结果是 undefined(因为DOM还没渲染);但在 mounted 里打印,就能拿到这个div的DOM节点,甚至能修改它的样式:this.$refs.test.style.color = 'red'

mounted和created有啥核心区别?

这俩钩子经常被搞混,其实核心区别在“DOM是否渲染”“能做的事”上,咱分两点唠:

执行时机不同

created 是“组件实例创建完,数据和事件绑好了,但DOM还没开始渲染”;mounted 是“DOM已经渲染成真实结构,并且挂到页面上了”。

能干的事儿不同

  • created 适合干“不依赖DOM的初始化”:比如发请求拿用户信息(这时候数据响应式已经 ready,拿到数据直接存到 data 里就行,不用管DOM);或者做一些数据格式转换(比如把接口返回的时间戳转成日期格式)。
  • mounted 适合干“依赖DOM的操作”:比如初始化ECharts、Swiper这些第三方插件(因为它们需要一个真实的DOM容器才能初始化);或者给某个DOM元素绑自定义事件(虽然Vue推荐用指令,但第三方生成的DOM就得在这搞);再或者根据某个DOM的尺寸发请求(比如自适应表格高度,得先拿到容器高度再请求对应条数的数据)。

举个经典例子:做图表组件时,ECharts初始化需要传入DOM容器的id,如果在 created 里写 this.chart = echarts.init(this.$refs.chartDom),会直接报错——因为 $refs.chartDom 还没生成呢!必须等到 mounted 里,DOM存在了才能初始化。

mounted里能做哪些常见操作?

只要是“DOM渲染后才能干的事儿”,都适合往 mounted 里塞,咱列几个高频场景:

初始化依赖DOM的第三方库

像ECharts(图表)、Swiper(轮播)、Quill(富文本编辑器)这些插件,都得先有个DOM容器才能初始化,比如Swiper轮播:

mounted() {
  new Swiper(this.$refs.swiperContainer, {
    loop: true,
    autoplay: true
  })
}

这里 $refs.swiperContainer 就是模板里的轮播容器DOM,mounted 保证它已经存在,Swiper才能正常初始化。

手动操作DOM元素

虽然Vue推荐用数据驱动DOM,但偶尔需要手动改DOM(比如第三方库生成的元素加事件),比如给一个按钮加点击事件(虽然Vue里用 @click 更方便,但假设按钮是第三方生成的):

mounted() {
  const btn = document.getElementById('third-party-btn')
  btn.addEventListener('click', () => {
    this.handleClick()
  })
}

基于DOM的异步请求

有些请求需要先知道DOM的状态(比如高度、宽度),比如做“滚动加载”组件,得先拿到容器高度,再请求对应数量的数据:

mounted() {
  const containerHeight = this.$refs.scrollBox.clientHeight
  this.fetchData(containerHeight) // 根据高度请求数据
}

订阅事件或开定时器

组件挂载后,需要定时轮询数据(比如实时消息),或者监听window的滚动事件,但要注意:这些操作必须在组件销毁前清理,否则会内存泄漏!

比如定时器:

mounted() {
  this.timer = setInterval(() => {
    this.fetchNewData()
  }, 5000)
},
beforeDestroy() {
  clearInterval(this.timer) // 组件销毁前清掉定时器
}

再比如监听滚动:

mounted() {
  window.addEventListener('scroll', this.handleScroll)
},
beforeDestroy() {
  window.removeEventListener('scroll', this.handleScroll)
}

mounted里处理异步请求要注意啥?

很多同学在 mounted 里发请求时,容易碰到“数据拿到了但DOM没更新”“props还没传值就发请求”这些坑,咱逐个拆:

数据更新后,DOM会自动更新吗?

Vue是响应式的,所以在 mounted 里发请求,拿到数据赋值给 data 后,DOM会自动更新(因为数据变化触发了重新渲染)。但如果是手动操作DOM(比如自己改 innerHTML,得等数据赋值后再操作——因为Vue更新DOM是异步的。

mounted() {
  axios.get('/api/data').then(res => {
    this.list = res.data // 赋值给data
    // 这时候想手动改列表的DOM样式
    // 直接操作可能拿到旧DOM,因为Vue还没更新DOM
    this.$nextTick(() => {
      const listItems = document.querySelectorAll('.list-item')
      listItems.forEach(item => {
        item.style.color = 'blue'
      })
    })
  })
}

$nextTick 能保证在DOM更新后再执行回调,避免拿到旧DOM。

依赖props的请求,时机咋控制?

如果请求需要父组件传的 props,得注意父组件传值的时机,比如父组件异步拿数据,传给子组件的 id 是异步更新的,子组件 mounted 执行时,props.id 可能还是初始值(比如空),这时候有两种解法:

  • watch 监听props:子组件里写:

    watch: {
      id(newVal) {
        if (newVal) { // 确保id有值
          this.fetchData(newVal)
        }
      }
    }
  • 父组件用 v-if 控制子组件渲染:父组件里等 id 拿到后,再渲染子组件:

    <ChildComponent :id="id" v-if="id" />

    这样子组件 mounted 执行时,props.id 已经有值了,请求能正常发。

mounted里操作DOM为什么偶尔拿不到?

明明 mounted 是DOM挂载后执行,为啥有时候还是拿不到DOM?常见原因有这几个:

v-if / v-show 坑了

v-if 是“条件为false时,DOM直接不渲染”;v-show 是“DOM渲染了,但用CSS隐藏”。mounted 里操作的DOM受 v-if 控制,且条件为false,那DOM根本没生成,自然拿不到。

<div ref="target" v-if="show">...</div>
mounted() {
  console.log(this.$refs.target) // 如果show是false,这里是undefined
}

解决方法:确保 v-if 条件为true时,再操作DOM;或者改用 v-show(但要接受DOM存在但隐藏的开销)。

异步渲染导致DOM更新延迟

Vue更新DOM是异步的(为了性能,把多个数据变化合并成一次DOM更新),如果在 mounted 里连续改数据,然后立刻操作DOM,可能拿到的是旧DOM。

mounted() {
  this.list = [1,2,3] // 改数据,Vue异步更新DOM
  const listDom = document.getElementById('list')
  console.log(listDom.children.length) // 可能还是旧长度(比如0)
  this.$nextTick(() => {
    console.log(listDom.children.length) // 现在是3,正确
  })
}

这时候必须用 $nextTick 等DOM更新后再操作。

子组件和父组件的执行顺序坑

Vue2里,父组件和子组件的生命周期执行顺序是:父 beforeCreate → 父 created → 父 beforeMount → 子 beforeCreate → 子 created → 子 beforeMount → 子 mounted → 父 mounted

所以父组件 mounted 执行时,子组件已经 mounted 了,理论上父组件里的子组件DOM应该存在,但如果父组件里的子组件受 v-if 控制,且条件在父 mounted 后才变为true,那子组件还没渲染,父组件自然拿不到它的DOM。

比如父组件:

mounted() {
  this.showChild = true // 子组件v-if="showChild"
  console.log(this.$refs.childDom) // 可能还是undefined,因为子组件刚渲染,DOM还没更新
  this.$nextTick(() => {
    console.log(this.$refs.childDom) // 现在能拿到
  })
}

这种情况也得用 $nextTick 等子组件DOM渲染完。

mounted里的this指向哪里?

mounted 里的 this 默认指向当前组件实例,所以能直接访问 this.datathis.methodsthis.$refs 这些,但有个隐藏坑如果在mounted里定义普通函数,this可能丢失

mounted() {
  function handle() {
    this.count++ // 这里的this不是组件实例,而是window(非严格模式下)
  }
  handle() // 报错:Cannot read property 'count' of undefined
}

解决方法有俩:

  1. 用箭头函数:箭头函数不改变this指向,this 还是组件实例:

    mounted() {
      const handle = () => {
        this.count++ // 正确
      }
      handle()
    }
  2. 把this存起来

    mounted() {
      const self = this
      function handle() {
        self.count++ // 用self代替this
      }
      handle()
    }

不过Vue的生命周期钩子(像mounted、created这些)内部的 this 已经被绑定为组件实例,所以直接写函数调用一般没问题,但如果在钩子内部定义嵌套函数,就得注意this指向~

多个组件嵌套时,mounted执行顺序是怎样的?

这个问题搞懂了,能避免很多“父组件等子组件DOM”的坑,Vue2里,父组件和子组件的mounted执行顺序是:先子组件mounted,再父组件mounted,完整顺序是这样的:

  1. 父组件:beforeCreatecreatedbeforeMount
  2. 子组件:beforeCreatecreatedbeforeMountmounted
  3. 父组件:mounted

举个例子:父组件叫 Parent,里面包含子组件 Child,那么执行顺序是:

Parent beforeCreate → Parent created → Parent beforeMount → 
Child beforeCreate → Child created → Child beforeMount → Child mounted → 
Parent mounted

父组件的mounted执行时,所有子组件已经mounted了,如果父组件需要等子组件都渲染完再做操作(比如获取子组件的DOM尺寸总和),直接在父组件的mounted里处理就行,因为这时候子组件的DOM已经存在。

mounted里做性能优化要避开哪些坑?

mounted 里的操作如果没做好,很容易让页面变卡或者内存泄漏,这几个坑要避开:

别在mounted里做大量同步DOM操作

比如循环创建几百个DOM节点,同步操作会阻塞浏览器渲染,导致页面卡顿,可以用分片处理(把操作分成小块,用 requestAnimationFrame 分批执行),或者用Vue的列表渲染(让Vue帮你优化DOM操作)。

反例(别这么写):

mounted() {
  const container = this.$refs.container
  for (let i = 0; i < 1000; i++) {
    const div = document.createElement('div')
    div.innerText = `item ${i}`
    container.appendChild(div)
  }
}

正解:用Vue的 v-for 渲染列表,或者分片处理:

mounted() {
  const container = this.$refs.container
  let i = 0
  function addItem() {
    for (let j = 0; j < 100; j++) { // 每次加100个
      const div = document.createElement('div')
      div.innerText = `item ${i++}`
      container.appendChild(div)
    }
    if (i < 1000) {
      requestAnimationFrame(addItem) // 下一帧再执行
    }
  }
  addItem()
}

定时器和事件订阅必须及时销毁

mounted里开的定时器、addEventListener,如果不在组件销毁前清理,组件都销毁了它们还在运行,会导致内存泄漏(浏览器内存越用越多,页面越来越卡)。

正确做法是在 beforeDestroy 里清理:

mounted() {
  this.timer = setInterval(() => { ... }, 1000)
  window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
  clearInterval(this.timer)
  window.removeEventListener('resize', this.handleResize)
}

避免重复请求

如果组件被频繁切换(比如用 keep-alive 缓存),mounted里的请求会重复发,可以结合 activated 钩子(组件被激活时执行),或者加个“是否已请求”的标识:

data() {
  return {
    isFetched: false
  }
},
mounted() {
  this.fetchData()
},
activated() {
  if (!this.isFetched) {
    this.fetchData()
  }
},
methods: {
  fetchData() {
    axios.get('/api/data').then(res => {
      this.data = res.data
      this.isFetched = true
    })
  }
}

mounted里遇到错误咋排查?

碰到 mounted 里的代码报错,别慌,按这步骤查:

先看控制台报错信息

Cannot read property 'xxx' of undefined,大概率是DOM没找到$refsgetElementById 返回空),或者props没传值(比如请求依赖的props是undefined)。

检查执行时机

是不是在 mounted 之前就操作了DOM?比如错误地把DOM操作写到 created 里了。

异步请求的响应处理

请求返回后的数据有没有正确赋值给 data?DOM操作是不是在请求的 then 里(或者用 $nextTick)?比如请求还没返回,就去操作依赖请求结果的DOM,肯定拿不到数据。

检查this指向

内部函数是不是用了普通函数导致 this 丢失?比如在定时器回调里用了普通函数,this 不是组件实例。

举个排查例子:页面上有个按钮,点击后没反应,控制台报 this.handleClick is not a function,一查,原来在 mounted 里的定时器用了普通函数:

mounted() {
  setInterval(function() {
    this.handleClick() //

版权声明

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

发表评论:

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

热门