Vue2里的mounted钩子到底怎么用?常见问题一次说清
很多刚开始学Vue2的同学,一到mounted钩子这块就犯懵——它啥时候执行?能做哪些事?和created有啥区别?碰到异步请求、DOM操作出问题咋整?今天咱就把mounted的常见疑问掰碎了讲,从基础到实战坑点全覆盖,帮你把这个生命周期钩子吃透~
mounted是Vue2生命周期里的哪个阶段?
Vue2的组件从“出生”到“渲染到页面”要经历一系列生命周期阶段,mounted 属于“挂载阶段”的最后一步,咱拆解下整个流程:
- 先经历
beforeCreate(实例刚创建,数据和事件都没初始化)、created(数据观测、事件配置完,但DOM还没影); - 接着进入挂载阶段:
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.data、this.methods、this.$refs 这些,但有个隐藏坑:如果在mounted里定义普通函数,this可能丢失!
mounted() {
function handle() {
this.count++ // 这里的this不是组件实例,而是window(非严格模式下)
}
handle() // 报错:Cannot read property 'count' of undefined
}
解决方法有俩:
-
用箭头函数:箭头函数不改变this指向,
this还是组件实例:mounted() { const handle = () => { this.count++ // 正确 } handle() } -
把this存起来:
mounted() { const self = this function handle() { self.count++ // 用self代替this } handle() }
不过Vue的生命周期钩子(像mounted、created这些)内部的 this 已经被绑定为组件实例,所以直接写函数调用一般没问题,但如果在钩子内部定义嵌套函数,就得注意this指向~
多个组件嵌套时,mounted执行顺序是怎样的?
这个问题搞懂了,能避免很多“父组件等子组件DOM”的坑,Vue2里,父组件和子组件的mounted执行顺序是:先子组件mounted,再父组件mounted,完整顺序是这样的:
- 父组件:
beforeCreate→created→beforeMount - 子组件:
beforeCreate→created→beforeMount→mounted - 父组件:
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没找到($refs 或 getElementById 返回空),或者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前端网发表,如需转载,请注明页面地址。
code前端网



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