生命周期钩子
每个 Vue 组件实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。
生命周期图示
创建阶段:
beforeCreate → 实例初始化之后,数据观测和事件配置之前
created → 实例创建完成,数据观测、属性和方法的运算已完成
挂载阶段:
beforeMount → 挂载开始之前,render 函数首次被调用
mounted → 实例被挂载后调用,DOM 已经创建完成
更新阶段:
beforeUpdate → 数据更新时调用,发生在虚拟 DOM 重新渲染之前
updated → 数据更新导致的虚拟 DOM 重新渲染后调用
卸载阶段:
beforeUnmount → 卸载组件实例之前调用
unmounted → 卸载组件实例后调用
组合式 API 中的生命周期钩子
在组合式 API 中,生命周期钩子需要在 setup()
函数中调用:
vue
<template>
<div class="lifecycle-demo">
<h2>生命周期演示</h2>
<div class="demo-section">
<h3>组件状态</h3>
<p>计数器: {{ count }}</p>
<p>消息: {{ message }}</p>
<button @click="increment">增加计数</button>
<button @click="updateMessage">更新消息</button>
</div>
<div class="demo-section">
<h3>生命周期日志</h3>
<div class="log-container">
<div
v-for="(log, index) in lifecycleLogs"
:key="index"
class="log-entry"
>
<span class="log-time">{{ log.time }}</span>
<span class="log-hook">{{ log.hook }}</span>
<span class="log-message">{{ log.message }}</span>
</div>
</div>
<button @click="clearLogs" class="clear-btn">清空日志</button>
</div>
<div class="demo-section">
<h3>子组件控制</h3>
<button @click="toggleChild" class="toggle-btn">
{{ showChild ? '隐藏' : '显示' }}子组件
</button>
<ChildComponent v-if="showChild" :count="count" @log="addLog" />
</div>
</div>
</template>
<script setup>
import {
ref,
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
import ChildComponent from './ChildComponent.vue'
const count = ref(0)
const message = ref('Hello Vue!')
const showChild = ref(true)
const lifecycleLogs = ref([])
function addLog(hook, message) {
lifecycleLogs.value.push({
time: new Date().toLocaleTimeString(),
hook,
message
})
}
function increment() {
count.value++
}
function updateMessage() {
message.value = `更新时间: ${new Date().toLocaleTimeString()}`
}
function toggleChild() {
showChild.value = !showChild.value
}
function clearLogs() {
lifecycleLogs.value = []
}
// 生命周期钩子
onBeforeMount(() => {
addLog('onBeforeMount', '组件挂载前 - DOM 还未创建')
console.log('onBeforeMount: 组件即将挂载')
})
onMounted(() => {
addLog('onMounted', '组件已挂载 - 可以访问 DOM')
console.log('onMounted: 组件已挂载,DOM 可用')
// 在这里可以进行 DOM 操作、发起网络请求等
// 例如:获取元素引用、初始化第三方库等
})
onBeforeUpdate(() => {
addLog('onBeforeUpdate', '组件更新前 - 数据已变化,DOM 未更新')
console.log('onBeforeUpdate: 组件即将更新')
})
onUpdated(() => {
addLog('onUpdated', '组件已更新 - DOM 已重新渲染')
console.log('onUpdated: 组件已更新,DOM 已同步')
// 注意:避免在此钩子中修改组件状态,可能导致无限循环
})
onBeforeUnmount(() => {
addLog('onBeforeUnmount', '组件卸载前 - 清理工作的最佳时机')
console.log('onBeforeUnmount: 组件即将卸载')
// 清理工作:移除事件监听器、取消定时器、清理订阅等
})
onUnmounted(() => {
addLog('onUnmounted', '组件已卸载 - 所有清理工作完成')
console.log('onUnmounted: 组件已卸载')
})
</script>
<style scoped>
.lifecycle-demo {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.demo-section {
margin-bottom: 25px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.demo-section h3 {
margin: 0 0 15px 0;
color: #333;
}
.demo-section button {
padding: 10px 20px;
margin-right: 10px;
background-color: #007bff;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s;
}
.demo-section button:hover {
background-color: #0056b3;
transform: translateY(-1px);
}
.toggle-btn {
background-color: #28a745 !important;
}
.toggle-btn:hover {
background-color: #1e7e34 !important;
}
.clear-btn {
background-color: #dc3545 !important;
margin-top: 15px;
}
.clear-btn:hover {
background-color: #c82333 !important;
}
.log-container {
max-height: 300px;
overflow-y: auto;
background-color: white;
border-radius: 6px;
padding: 15px;
margin-bottom: 15px;
}
.log-entry {
display: flex;
padding: 8px;
margin-bottom: 5px;
border-bottom: 1px solid #f0f0f0;
font-family: monospace;
font-size: 14px;
}
.log-time {
color: #6c757d;
margin-right: 15px;
min-width: 90px;
}
.log-hook {
color: #007bff;
font-weight: bold;
margin-right: 15px;
min-width: 120px;
}
.log-message {
color: #333;
flex: 1;
}
@media (max-width: 768px) {
.demo-section button {
display: block;
width: 100%;
margin: 5px 0;
}
.log-entry {
flex-direction: column;
gap: 5px;
}
.log-time,
.log-hook {
min-width: auto;
}
}
</style>
实际应用示例
数据获取和清理
vue
<template>
<div class="data-fetch-demo">
<h2>数据获取示例</h2>
<div class="demo-section">
<h3>用户列表</h3>
<div v-if="loading" class="loading">
<div class="spinner"></div>
<p>加载中...</p>
</div>
<div v-else-if="error" class="error">
<p>❌ 加载失败: {{ error }}</p>
<button @click="fetchUsers" class="retry-btn">重试</button>
</div>
<div v-else class="user-list">
<div
v-for="user in users"
:key="user.id"
class="user-card"
>
<div class="user-avatar">
{{ user.name.charAt(0) }}
</div>
<div class="user-info">
<h4>{{ user.name }}</h4>
<p>{{ user.email }}</p>
<p>{{ user.phone }}</p>
</div>
</div>
</div>
<button @click="refreshUsers" class="refresh-btn">
🔄 刷新数据
</button>
</div>
<div class="demo-section">
<h3>实时时间</h3>
<div class="time-display">
<p class="current-time">{{ currentTime }}</p>
<p class="uptime">运行时间: {{ uptime }}秒</p>
</div>
</div>
<div class="demo-section">
<h3>窗口大小监听</h3>
<div class="window-info">
<p>窗口宽度: {{ windowWidth }}px</p>
<p>窗口高度: {{ windowHeight }}px</p>
<p>设备类型: {{ deviceType }}</p>
</div>
</div>
</div>
</template>
<script setup>
import {
ref,
computed,
onMounted,
onBeforeUnmount
} from 'vue'
const users = ref([])
const loading = ref(false)
const error = ref('')
const currentTime = ref('')
const uptime = ref(0)
const windowWidth = ref(0)
const windowHeight = ref(0)
let timeInterval = null
let uptimeInterval = null
const deviceType = computed(() => {
if (windowWidth.value < 768) return '移动设备'
if (windowWidth.value < 1024) return '平板设备'
return '桌面设备'
})
// 模拟 API 调用
function fetchUsers() {
loading.value = true
error.value = ''
// 模拟网络延迟
setTimeout(() => {
try {
// 模拟随机失败
if (Math.random() < 0.2) {
throw new Error('网络连接失败')
}
// 模拟用户数据
users.value = [
{
id: 1,
name: '张三',
email: 'zhangsan@example.com',
phone: '138-0000-0001'
},
{
id: 2,
name: '李四',
email: 'lisi@example.com',
phone: '138-0000-0002'
},
{
id: 3,
name: '王五',
email: 'wangwu@example.com',
phone: '138-0000-0003'
},
{
id: 4,
name: '赵六',
email: 'zhaoliu@example.com',
phone: '138-0000-0004'
}
]
loading.value = false
} catch (err) {
error.value = err.message
loading.value = false
}
}, 1000 + Math.random() * 2000) // 1-3秒随机延迟
}
function refreshUsers() {
fetchUsers()
}
function updateTime() {
currentTime.value = new Date().toLocaleTimeString()
}
function updateWindowSize() {
windowWidth.value = window.innerWidth
windowHeight.value = window.innerHeight
}
// 组件挂载后的初始化
onMounted(() => {
console.log('组件已挂载,开始初始化...')
// 1. 获取初始数据
fetchUsers()
// 2. 设置定时器更新时间
updateTime()
timeInterval = setInterval(updateTime, 1000)
// 3. 设置运行时间计数器
uptimeInterval = setInterval(() => {
uptime.value++
}, 1000)
// 4. 监听窗口大小变化
updateWindowSize()
window.addEventListener('resize', updateWindowSize)
console.log('初始化完成')
})
// 组件卸载前的清理
onBeforeUnmount(() => {
console.log('组件即将卸载,开始清理...')
// 1. 清理定时器
if (timeInterval) {
clearInterval(timeInterval)
timeInterval = null
}
if (uptimeInterval) {
clearInterval(uptimeInterval)
uptimeInterval = null
}
// 2. 移除事件监听器
window.removeEventListener('resize', updateWindowSize)
console.log('清理完成')
})
</script>
<style scoped>
.data-fetch-demo {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.demo-section {
margin-bottom: 25px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.demo-section h3 {
margin: 0 0 15px 0;
color: #333;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error {
text-align: center;
padding: 20px;
color: #dc3545;
}
.retry-btn {
padding: 10px 20px;
background-color: #dc3545;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
margin-top: 10px;
}
.user-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.user-card {
display: flex;
align-items: center;
padding: 15px;
background-color: white;
border-radius: 8px;
border: 1px solid #dee2e6;
transition: all 0.3s;
}
.user-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.user-avatar {
width: 50px;
height: 50px;
background-color: #007bff;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: bold;
margin-right: 15px;
}
.user-info h4 {
margin: 0 0 5px 0;
color: #333;
}
.user-info p {
margin: 3px 0;
color: #666;
font-size: 14px;
}
.refresh-btn {
padding: 10px 20px;
background-color: #28a745;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
}
.refresh-btn:hover {
background-color: #1e7e34;
}
.time-display {
text-align: center;
padding: 20px;
background-color: white;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.current-time {
font-size: 32px;
font-weight: bold;
color: #007bff;
margin: 0 0 10px 0;
font-family: monospace;
}
.uptime {
font-size: 18px;
color: #666;
margin: 0;
}
.window-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
text-align: center;
}
.window-info p {
padding: 15px;
background-color: white;
border-radius: 6px;
border: 1px solid #dee2e6;
margin: 0;
font-size: 16px;
font-weight: 500;
}
@media (max-width: 768px) {
.user-list {
grid-template-columns: 1fr;
}
.current-time {
font-size: 24px;
}
.window-info {
grid-template-columns: 1fr;
}
}
</style>
第三方库集成
vue
<template>
<div class="chart-demo">
<h2>图表集成示例</h2>
<div class="demo-section">
<h3>销售数据图表</h3>
<div class="chart-controls">
<button @click="updateData" class="update-btn">
🔄 更新数据
</button>
<button @click="changeChartType" class="type-btn">
📊 切换图表类型
</button>
</div>
<!-- 图表容器 -->
<div ref="chartContainer" class="chart-container">
<div class="chart-placeholder">
📈 图表将在这里显示
<p>(这是一个模拟的图表容器)</p>
</div>
</div>
<div class="chart-info">
<p>图表类型: {{ chartType }}</p>
<p>数据点数量: {{ chartData.length }}</p>
<p>最后更新: {{ lastUpdate }}</p>
</div>
</div>
<div class="demo-section">
<h3>地图组件</h3>
<div ref="mapContainer" class="map-container">
<div class="map-placeholder">
🗺️ 地图将在这里显示
<p>(这是一个模拟的地图容器)</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import {
ref,
onMounted,
onBeforeUnmount,
nextTick
} from 'vue'
const chartContainer = ref(null)
const mapContainer = ref(null)
const chartType = ref('line')
const chartData = ref([])
const lastUpdate = ref('')
// 模拟图表实例
let chartInstance = null
let mapInstance = null
// 模拟第三方图表库的初始化
function initChart() {
if (!chartContainer.value) return
console.log('初始化图表...')
// 模拟图表库初始化
chartInstance = {
container: chartContainer.value,
type: chartType.value,
data: chartData.value,
// 模拟图表方法
updateData(newData) {
this.data = newData
console.log('图表数据已更新:', newData)
},
changeType(newType) {
this.type = newType
console.log('图表类型已切换:', newType)
},
destroy() {
console.log('图表已销毁')
}
}
// 生成初始数据
generateChartData()
console.log('图表初始化完成')
}
// 模拟地图库的初始化
function initMap() {
if (!mapContainer.value) return
console.log('初始化地图...')
// 模拟地图库初始化
mapInstance = {
container: mapContainer.value,
center: [116.404, 39.915], // 北京坐标
zoom: 10,
// 模拟地图方法
setCenter(coords) {
this.center = coords
console.log('地图中心已设置:', coords)
},
setZoom(level) {
this.zoom = level
console.log('地图缩放级别已设置:', level)
},
destroy() {
console.log('地图已销毁')
}
}
console.log('地图初始化完成')
}
// 生成模拟数据
function generateChartData() {
const data = []
for (let i = 0; i < 12; i++) {
data.push({
month: `${i + 1}月`,
value: Math.floor(Math.random() * 1000) + 100
})
}
chartData.value = data
lastUpdate.value = new Date().toLocaleTimeString()
}
// 更新图表数据
function updateData() {
generateChartData()
if (chartInstance) {
chartInstance.updateData(chartData.value)
}
}
// 切换图表类型
function changeChartType() {
const types = ['line', 'bar', 'pie', 'area']
const currentIndex = types.indexOf(chartType.value)
const nextIndex = (currentIndex + 1) % types.length
chartType.value = types[nextIndex]
if (chartInstance) {
chartInstance.changeType(chartType.value)
}
}
// 组件挂载后初始化第三方库
onMounted(async () => {
console.log('组件已挂载,开始初始化第三方库...')
// 等待 DOM 更新完成
await nextTick()
// 初始化图表
initChart()
// 初始化地图
initMap()
console.log('所有第三方库初始化完成')
})
// 组件卸载前清理第三方库
onBeforeUnmount(() => {
console.log('组件即将卸载,开始清理第三方库...')
// 销毁图表实例
if (chartInstance) {
chartInstance.destroy()
chartInstance = null
}
// 销毁地图实例
if (mapInstance) {
mapInstance.destroy()
mapInstance = null
}
console.log('第三方库清理完成')
})
</script>
<style scoped>
.chart-demo {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.demo-section {
margin-bottom: 25px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #dee2e6;
}
.demo-section h3 {
margin: 0 0 15px 0;
color: #333;
}
.chart-controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.update-btn,
.type-btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s;
}
.update-btn {
background-color: #28a745;
color: white;
}
.update-btn:hover {
background-color: #1e7e34;
}
.type-btn {
background-color: #007bff;
color: white;
}
.type-btn:hover {
background-color: #0056b3;
}
.chart-container,
.map-container {
width: 100%;
height: 300px;
background-color: white;
border: 2px dashed #dee2e6;
border-radius: 8px;
margin-bottom: 15px;
}
.chart-placeholder,
.map-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #6c757d;
font-size: 24px;
}
.chart-placeholder p,
.map-placeholder p {
margin: 10px 0 0 0;
font-size: 16px;
}
.chart-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.chart-info p {
padding: 10px 15px;
background-color: white;
border-radius: 6px;
border: 1px solid #dee2e6;
margin: 0;
text-align: center;
font-weight: 500;
}
@media (max-width: 768px) {
.chart-controls {
flex-direction: column;
}
.chart-info {
grid-template-columns: 1fr;
}
}
</style>
生命周期钩子的最佳实践
1. 数据获取时机
javascript
// ✅ 推荐:在 onMounted 中获取数据
onMounted(async () => {
try {
const data = await fetchUserData()
users.value = data
} catch (error) {
console.error('获取用户数据失败:', error)
}
})
// ❌ 不推荐:在 setup 中直接获取数据
// 这会在服务端渲染时执行,可能导致问题
2. 清理资源
javascript
// ✅ 推荐:在 onBeforeUnmount 中清理资源
let timer = null
onMounted(() => {
timer = setInterval(() => {
// 定时任务
}, 1000)
})
onBeforeUnmount(() => {
if (timer) {
clearInterval(timer)
timer = null
}
})
3. DOM 操作
javascript
// ✅ 推荐:在 onMounted 中进行 DOM 操作
const elementRef = ref(null)
onMounted(() => {
// 此时 DOM 已经渲染完成
if (elementRef.value) {
elementRef.value.focus()
}
})
4. 避免在 onUpdated 中修改状态
javascript
// ❌ 危险:可能导致无限循环
onUpdated(() => {
count.value++ // 这会触发新的更新
})
// ✅ 推荐:只进行读取操作或 DOM 操作
onUpdated(() => {
console.log('组件已更新')
// 或者进行 DOM 测量等操作
})
选项式 API 对比
选项式 API | 组合式 API | 说明 |
---|---|---|
beforeCreate | setup() | 组件实例创建前 |
created | setup() | 组件实例创建后 |
beforeMount | onBeforeMount | 挂载前 |
mounted | onMounted | 挂载后 |
beforeUpdate | onBeforeUpdate | 更新前 |
updated | onUpdated | 更新后 |
beforeUnmount | onBeforeUnmount | 卸载前 |
unmounted | onUnmounted | 卸载后 |