挂载阶段是 Vue 实例生命周期的第二个阶段。在这个阶段,Vue 将模板编译成渲染函数,生成虚拟 DOM,并最终渲染成真实 DOM。
挂载阶段包含两个钩子:beforeMount 和 mounted。
beforeMount 在挂载开始之前调用,此时模板已经编译完成,但还没有渲染到页面。
created
│
▼
编译模板为渲染函数
│
▼
beforeMount ← 模板已编译,DOM 未渲染
│
▼
创建虚拟 DOM
│
▼
渲染真实 DOM
│
▼
mounted
<div id="app">
<p>{{ message }}</p>
</div>
<script>
new Vue({
el: '#app',
data: {
message: 'Hello Vue'
},
beforeMount() {
console.log('beforeMount')
console.log(this.$el.textContent)
console.log(this.$el.outerHTML)
}
})
</script>
输出结果:
beforeMount
{{ message }}
<div id="app"><p>{{ message }}</p></div>
此时 $el 的内容还是模板字符串,没有被渲染。
beforeMount 使用场景很少,偶尔用于:
1. 最后修改模板
beforeMount() {
this.$options.template = this.$options.template.replace(
/{{\s*(\w+)\s*}}/g,
'[[ $1 ]]'
)
}
2. 服务端渲染判断
beforeMount() {
if (typeof window !== 'undefined') {
this.initClientOnly()
}
}
mounted 在实例挂载完成后调用,此时 DOM 已经渲染完成。
beforeMount
│
▼
创建虚拟 DOM
│
▼
渲染真实 DOM
│
▼
mounted ← DOM 已渲染完成
<div id="app">
<p ref="message">{{ message }}</p>
</div>
<script>
new Vue({
el: '#app',
data: {
message: 'Hello Vue'
},
mounted() {
console.log('mounted')
console.log(this.$el.textContent)
console.log(this.$refs.message.textContent)
}
})
</script>
输出结果:
mounted
Hello Vue
Hello Vue
mounted 是非常常用的钩子:
1. 操作 DOM
mounted() {
const canvas = this.$refs.canvas
const ctx = canvas.getContext('2d')
ctx.fillStyle = 'red'
ctx.fillRect(0, 0, 100, 100)
}
2. 初始化第三方库
mounted() {
this.editor = new CodeMirror(this.$refs.editor, {
mode: 'javascript',
theme: 'monokai'
})
},
beforeDestroy() {
this.editor.toTextArea()
}
3. 添加事件监听
mounted() {
window.addEventListener('resize', this.handleResize)
document.addEventListener('click', this.handleClick)
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize)
document.removeEventListener('click', this.handleClick)
}
4. 初始化图表
mounted() {
this.chart = new Chart(this.$refs.chart, {
type: 'bar',
data: this.chartData,
options: this.chartOptions
})
},
watch: {
chartData(newData) {
this.chart.data = newData
this.chart.update()
}
},
beforeDestroy() {
this.chart.destroy()
}
Vue 的 DOM 更新是异步的。在 mounted 中,如果需要等待 DOM 更新完成,可以使用 $nextTick。
mounted() {
this.message = 'Updated'
console.log(this.$el.textContent)
this.$nextTick(() => {
console.log(this.$el.textContent)
})
}
mounted() {
this.$nextTick(() => {
this.initPlugin()
})
}
mounted() {
this.$nextTick(() => {
const rect = this.$refs.element.getBoundingClientRect()
this.width = rect.width
this.height = rect.height
})
}
理解父子组件的挂载顺序非常重要:
父 beforeCreate
父 created
父 beforeMount
子 beforeCreate
子 created
子 beforeMount
子 mounted
父 mounted
Vue.component('child', {
template: '<div>Child</div>',
beforeCreate() { console.log('子 beforeCreate') },
created() { console.log('子 created') },
beforeMount() { console.log('子 beforeMount') },
mounted() { console.log('子 mounted') }
})
new Vue({
template: '<div><child></child></div>',
beforeCreate() { console.log('父 beforeCreate') },
created() { console.log('父 created') },
beforeMount() { console.log('父 beforeMount') },
mounted() { console.log('父 mounted') }
}).$mount()
输出结果:
父 beforeCreate
父 created
父 beforeMount
子 beforeCreate
子 created
子 beforeMount
子 mounted
父 mounted
如果需要在所有子组件挂载完成后执行操作:
mounted() {
this.$nextTick(() => {
console.log('所有子组件已挂载')
this.doSomethingWithChildren()
})
}
Vue.component('bar-chart', {
template: '<div ref="chart" style="width: 100%; height: 300px;"></div>',
props: {
data: {
type: Array,
required: true
}
},
data() {
return {
chart: null
}
},
mounted() {
this.initChart()
window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize)
if (this.chart) {
this.chart.dispose()
}
},
watch: {
data: {
deep: true,
handler() {
this.updateChart()
}
}
},
methods: {
initChart() {
this.chart = echarts.init(this.$refs.chart)
this.updateChart()
},
updateChart() {
this.chart.setOption({
xAxis: {
type: 'category',
data: this.data.map(item => item.name)
},
yAxis: {
type: 'value'
},
series: [{
type: 'bar',
data: this.data.map(item => item.value)
}]
})
},
handleResize() {
this.chart.resize()
}
}
})
Vue.component('datepicker', {
template: '<input ref="input" type="text">',
props: {
value: String,
options: {
type: Object,
default: () => ({})
}
},
mounted() {
$(this.$refs.input).datepicker({
...this.options,
defaultDate: this.value,
onSelect: (dateText) => {
this.$emit('input', dateText)
}
})
},
beforeDestroy() {
$(this.$refs.input).datepicker('destroy')
},
watch: {
value(newVal) {
$(this.$refs.input).datepicker('setDate', newVal)
}
}
})
Vue.component('scroll-spy', {
template: '<div><slot></slot></div>',
data() {
return {
activeSection: null
}
},
mounted() {
this.sections = this.$el.querySelectorAll('section')
window.addEventListener('scroll', this.handleScroll)
this.handleScroll()
},
beforeDestroy() {
window.removeEventListener('scroll', this.handleScroll)
},
methods: {
handleScroll() {
const scrollPos = window.scrollY + 100
this.sections.forEach((section, index) => {
const top = section.offsetTop
const bottom = top + section.offsetHeight
if (scrollPos >= top && scrollPos < bottom) {
this.activeSection = index
this.$emit('change', index)
}
})
}
}
})
在服务端渲染时:
export default {
mounted() {
console.log('只在客户端执行')
}
}
1. DOM 操作放在 mounted
mounted() {
this.initDOM()
}
2. 及时清理资源
mounted() {
this.timer = setInterval(this.poll, 5000)
},
beforeDestroy() {
clearInterval(this.timer)
}
3. 使用 $nextTick 等待更新
mounted() {
this.$nextTick(() => {
this.measureElement()
})
}
4. 避免在 mounted 中直接修改数据导致重渲染
mounted() {
this.someData = 'new value'
}