深入理解 Vue 生命周期:created 与 mounted 的核心差异与实战指南
掌握生命周期钩子,是 Vue 开发从入门到精通的关键一步。今天我们来深度剖析两个最容易混淆的钩子:
created和mounted。
一、生命周期全景图:先看森林,再见树木
在深入细节之前,让我们先回顾 Vue 实例的完整生命周期:
graph TD
A[new Vue()] --> B[Init Events & Lifecycle]
B --> C[beforeCreate]
C --> D[Init Injections & Reactivity]
D --> E[created]
E --> F[Compile Template]
F --> G[beforeMount]
G --> H[Create vm.$el]
H --> I[mounted]
I --> J[Data Changes]
J --> K[beforeUpdate]
K --> L[Virtual DOM Re-render]
L --> M[updated]
M --> N[beforeDestroy]
N --> O[Teardown]
O --> P[destroyed]
理解这张图,你就掌握了 Vue 组件从出生到消亡的完整轨迹。而今天的主角------created 和 mounted,正是这个旅程中两个关键的里程碑。
二、核心对比:created vs mounted
让我们通过一个表格直观对比:
| 特性 | created | mounted |
|---|---|---|
| 执行时机 | 数据观测/方法/计算属性初始化后,模板编译前 | 模板编译完成,DOM 挂载到页面后 |
| DOM 可访问性 | ❌ 无法访问 DOM | ✅ 可以访问 DOM |
| $el 状态 | undefined |
已挂载的 DOM 元素 |
| 主要用途 | 数据初始化、API 调用、事件监听 | DOM 操作、第三方库初始化 |
| SSR 支持 | ✅ 在服务端和客户端都会执行 | ❌ 仅在客户端执行 |
三、实战代码解析:从理论到实践
场景 1:API 数据获取的正确姿势
javascript
export default {
data() {
return {
userData: null,
loading: true
}
},
async created() {
// ✅ 最佳实践:在 created 中发起数据请求
// 此时数据观测已就绪,可以设置响应式数据
try {
this.userData = await fetchUserData()
} catch (error) {
console.error('数据获取失败:', error)
} finally {
this.loading = false
}
// ❌ 这里访问 DOM 会失败
// console.log(this.$el) // undefined
},
mounted() {
// ✅ DOM 已就绪,可以执行依赖 DOM 的操作
const userCard = document.getElementById('user-card')
if (userCard) {
// 使用第三方图表库渲染数据
this.renderChart(userCard, this.userData)
}
// ✅ 初始化需要 DOM 的第三方插件
this.initCarousel('.carousel-container')
}
}
关键洞察:数据获取应尽早开始(created),DOM 相关操作必须等待 mounted。
场景 2:计算属性与 DOM 的微妙关系
vue
<template>
<div ref="container">
<p>容器宽度: {{ containerWidth }}px</p>
<div class="content">
<!-- 动态内容 -->
</div>
</div>
</template>
<script>
export default {
data() {
return {
items: []
}
},
computed: {
// ❌ 错误示例:在 created 阶段访问 $refs
containerWidth() {
// created 阶段:this.$refs.container 是 undefined
// mounted 阶段:可以正常访问
return this.$refs.container?.offsetWidth || 0
}
},
created() {
// ✅ 安全操作:初始化数据
this.items = this.generateItems()
// ⚠️ 注意:computed 属性在此阶段可能基于错误的前提计算
console.log('created 阶段宽度:', this.containerWidth) // 0
},
mounted() {
console.log('mounted 阶段宽度:', this.containerWidth) // 实际宽度
// ✅ 正确的 DOM 相关初始化
this.observeResize()
},
methods: {
observeResize() {
// 使用 ResizeObserver 监听容器大小变化
const observer = new ResizeObserver(entries => {
this.handleResize(entries[0].contentRect.width)
})
observer.observe(this.$refs.container)
}
}
}
</script>
四、性能优化:理解渲染流程避免常见陷阱
1. 避免在 created 中执行阻塞操作
javascript
export default {
created() {
// ⚠️ 潜在的渲染阻塞
this.processLargeData(this.rawData) // 如果处理时间过长,会延迟首次渲染
// ✅ 优化方案:使用 Web Worker 或分块处理
this.asyncProcessData()
},
async asyncProcessData() {
// 使用 requestIdleCallback 避免阻塞主线程
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
this.processInBackground()
})
} else {
// 回退方案:setTimeout 让出主线程
setTimeout(() => this.processInBackground(), 0)
}
}
}
2. 理解异步更新队列
javascript
export default {
mounted() {
// 情景 1:直接修改数据
this.someData = 'new value'
console.log(this.$el.textContent) // ❌ 可能还是旧值
// 情景 2:使用 $nextTick
this.someData = 'new value'
this.$nextTick(() => {
console.log(this.$el.textContent) // ✅ 更新后的值
})
// 情景 3:多个数据变更
this.data1 = 'new1'
this.data2 = 'new2'
this.data3 = 'new3'
// Vue 会批量处理,只触发一次更新
this.$nextTick(() => {
// 所有变更都已反映到 DOM
})
}
}
五、高级应用:SSR 场景下的特殊考量
javascript
export default {
// created 在服务端和客户端都会执行
async created() {
// 服务端渲染时,无法访问 window、document 等浏览器 API
if (process.client) {
// 客户端特定逻辑
this.screenWidth = window.innerWidth
}
// 数据预取(Universal)
await this.fetchUniversalData()
},
// mounted 只在客户端执行
mounted() {
// 安全的浏览器 API 使用
this.initializeBrowserOnlyLibrary()
// 处理客户端 hydration
this.handleHydrationEffects()
},
// 兼容 SSR 的数据获取模式
async fetchUniversalData() {
// 避免重复获取数据
if (this.$ssrContext && this.$ssrContext.data) {
// 服务端已获取数据
Object.assign(this, this.$ssrContext.data)
} else {
// 客户端获取数据
const data = await this.$axios.get('/api/data')
Object.assign(this, data)
}
}
}
六、实战技巧:常见问题与解决方案
Q1:应该在哪个钩子初始化第三方库?
javascript
export default {
mounted() {
// ✅ 大多数 UI 库需要 DOM 存在
this.$nextTick(() => {
// 确保 DOM 完全渲染
this.initSelect2('#my-select')
this.initDatepicker('.date-input')
})
},
beforeDestroy() {
// 记得清理,防止内存泄漏
this.destroySelect2()
this.destroyDatepicker()
}
}
Q2:如何处理动态组件?
vue
<template>
<component :is="currentComponent" ref="dynamicComponent" />
</template>
<script>
export default {
data() {
return {
currentComponent: 'ComponentA'
}
},
watch: {
currentComponent(newVal, oldVal) {
// 组件切换时,新的 mounted 会在下次更新后执行
this.$nextTick(() => {
console.log('新组件已挂载:', this.$refs.dynamicComponent)
})
}
},
mounted() {
// 初次挂载
this.initializeCurrentComponent()
}
}
</script>
七、最佳实践总结
- 数据初始化 → 优先选择
created - DOM 操作 → 必须使用
mounted(配合$nextTick确保渲染完成) - 第三方库初始化 →
mounted+beforeDestroy清理 - 性能敏感操作 → 考虑使用
requestIdleCallback或 Web Worker - SSR 应用 → 注意浏览器 API 的兼容性检查
写在最后
理解 created 和 mounted 的区别,本质上是理解 Vue 的渲染流程。记住这个核心原则:
created 是关于数据的准备,mounted 是关于视图的准备。
随着 Vue 3 Composition API 的普及,生命周期有了新的使用方式,但底层原理依然相通。掌握这些基础知识,能帮助你在各种场景下做出更合适的架构决策。