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前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。