Vue3 组件生命周期详解
核心概念理解
什么是生命周期?
生命周期是指组件从创建 到销毁 的整个过程。在这个过程中,Vue 会在特定的时间点自动调用一些函数,这些函数就是生命周期钩子。
为什么要学习生命周期?
- 在合适的时机执行特定操作
- 优化性能和资源管理
- 理解组件的运行机制
- 调试和排查问题
Vue3 生命周期钩子
1. 创建阶段
vue
<template>
<div class="lifecycle-demo">
<h2>生命周期演示</h2>
<p>计数器: {{ count }}</p>
<button @click="count++">增加</button>
<button @click="destroyComponent">销毁组件</button>
<div class="log-section">
<h3>生命周期日志:</h3>
<ul class="log-list">
<li v-for="(log, index) in logs" :key="index">
{{ log }}
</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'
const count = ref(0)
const logs = ref([])
const showComponent = ref(true)
// 记录日志的函数
const addLog = (message) => {
logs.value.push(`${message} - ${new Date().toLocaleTimeString()}`)
console.log(message)
}
// 1. setup() - 组件创建时(在 <script setup> 中就是整个 setup 函数)
addLog('🔧 setup: 组件开始创建')
// 2. onBeforeMount - 挂载之前
onBeforeMount(() => {
addLog('🟡 onBeforeMount: 组件即将挂载到 DOM')
console.log('DOM 还未创建,但数据已经准备好')
})
// 3. onMounted - 挂载之后
onMounted(() => {
addLog('🟢 onMounted: 组件已挂载到 DOM')
console.log('DOM 已创建,可以访问 DOM 元素')
// 这里可以进行 DOM 操作、发起网络请求等
fetchData()
})
// 4. onBeforeUpdate - 更新之前
onBeforeUpdate(() => {
addLog('🟠 onBeforeUpdate: 组件即将更新')
console.log('数据已更新,但 DOM 还未更新')
})
// 5. onUpdated - 更新之后
onUpdated(() => {
addLog('🔵 onUpdated: 组件已更新')
console.log('DOM 已更新完成')
})
// 6. onBeforeUnmount - 卸载之前
onBeforeUnmount(() => {
addLog('🟣 onBeforeUnmount: 组件即将卸载')
console.log('组件即将被销毁,但还存在')
})
// 7. onUnmounted - 卸载之后
onUnmounted(() => {
addLog('🔴 onUnmounted: 组件已卸载')
console.log('组件已被销毁,清理工作')
// 清理定时器、事件监听器等
cleanup()
})
// 模拟异步数据获取
const fetchData = () => {
console.log('发起网络请求...')
// 模拟 API 调用
setTimeout(() => {
console.log('数据获取完成')
}, 1000)
}
// 清理工作
const cleanup = () => {
console.log('执行清理工作')
// 清理定时器、取消网络请求、移除事件监听器等
}
// 销毁组件
const destroyComponent = () => {
showComponent.value = false
}
</script>
<style>
.lifecycle-demo {
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}
button {
padding: 8px 16px;
margin: 5px;
border: none;
border-radius: 4px;
background-color: #007bff;
color: white;
cursor: pointer;
font-size: 14px;
}
button:hover {
background-color: #0056b3;
}
.log-section {
margin-top: 30px;
padding: 20px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #dee2e6;
}
.log-section h3 {
margin-top: 0;
color: #495057;
}
.log-list {
max-height: 300px;
overflow-y: auto;
background-color: #f8f9fa;
border-radius: 4px;
padding: 10px;
margin: 0;
font-size: 14px;
}
.log-list li {
padding: 5px 0;
border-bottom: 1px solid #eee;
}
.log-list li:last-child {
border-bottom: none;
}
</style>
详细生命周期阶段
1. 创建阶段 (Creation)
vue
<template>
<div class="creation-stage">
<h2>创建阶段演示</h2>
<p>组件状态: {{ componentState }}</p>
<div class="data-display">
<p>数据初始化: {{ initializedData }}</p>
<p>计算属性: {{ computedValue }}</p>
</div>
</div>
</template>
<script setup>
import { ref, computed, onBeforeMount, onMounted } from 'vue'
const componentState = ref('setup 阶段')
const initializedData = ref('未初始化')
const logs = ref([])
// setup 阶段 - 组件创建时
console.log('🔧 setup 阶段: 组件开始创建')
componentState.value = 'setup 阶段'
// 初始化数据
const initializeData = () => {
console.log('正在初始化数据...')
initializedData.value = '数据已初始化'
logs.value.push('数据初始化完成')
}
// 计算属性
const computedValue = computed(() => {
console.log('计算属性被访问')
return `计算结果: ${initializedData.value}`
})
// onBeforeMount - 挂载之前
onBeforeMount(() => {
console.log('🟡 onBeforeMount: 组件即将挂载')
componentState.value = '即将挂载'
initializeData()
// 此时可以访问响应式数据,但不能访问 DOM
console.log('准备挂载的数据:', initializedData.value)
})
// onMounted - 挂载之后
onMounted(() => {
console.log('🟢 onMounted: 组件已挂载')
componentState.value = '已挂载'
// 此时可以访问 DOM 元素
console.log('DOM 已创建,可以进行 DOM 操作')
// 常见用途:
// 1. 发起网络请求
// 2. 访问 DOM 元素
// 3. 启动定时器
// 4. 添加事件监听器
})
</script>
<style>
.creation-stage {
padding: 20px;
background-color: #e3f2fd;
border-radius: 8px;
margin: 20px 0;
}
.data-display {
margin-top: 20px;
padding: 15px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #bbdefb;
}
.data-display p {
margin: 10px 0;
}
</style>
2. 更新阶段 (Update)
vue
<template>
<div class="update-stage">
<h2>更新阶段演示</h2>
<div class="controls">
<button @click="updateData">更新数据</button>
<button @click="triggerReactiveUpdate">触发响应式更新</button>
<input v-model="inputValue" placeholder="输入文字触发更新" class="update-input">
</div>
<div class="data-section">
<h3>当前数据:</h3>
<p>计数器: {{ counter }}</p>
<p>输入值: {{ inputValue }}</p>
<p>计算值: {{ doubleCounter }}</p>
</div>
<div class="log-section">
<h3>更新日志:</h3>
<ul class="log-list">
<li v-for="(log, index) in updateLogs" :key="index">
{{ log }}
</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref, computed, onBeforeUpdate, onUpdated, watch } from 'vue'
const counter = ref(0)
const inputValue = ref('')
const updateLogs = ref([])
// 计算属性
const doubleCounter = computed(() => {
console.log('计算属性重新计算')
return counter.value * 2
})
// 记录更新日志
const addUpdateLog = (message) => {
updateLogs.value.push(`${message} - ${new Date().toLocaleTimeString()}`)
console.log(message)
}
// onBeforeUpdate - 更新之前
onBeforeUpdate(() => {
addUpdateLog('🟠 onBeforeUpdate: 数据即将更新')
console.log('响应式数据已变化,但 DOM 还未更新')
console.log('当前计数器值:', counter.value)
})
// onUpdated - 更新之后
onUpdated(() => {
addUpdateLog('🔵 onUpdated: DOM 已更新')
console.log('DOM 更新完成')
console.log('更新后的计数器值:', counter.value)
})
// 更新数据
const updateData = () => {
counter.value++
}
const triggerReactiveUpdate = () => {
// 触发多次更新来观察生命周期
counter.value += 1
setTimeout(() => {
counter.value += 1
}, 100)
}
// 监听数据变化
watch(counter, (newVal, oldVal) => {
console.log(`计数器从 ${oldVal} 变为 ${newVal}`)
})
watch(inputValue, (newVal) => {
console.log('输入值变化:', newVal)
})
</script>
<style>
.update-stage {
padding: 20px;
background-color: #f3e5f5;
border-radius: 8px;
margin: 20px 0;
}
.controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 20px;
align-items: center;
}
.update-input {
padding: 8px 12px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 14px;
}
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
background-color: #7e57c2;
color: white;
cursor: pointer;
font-size: 14px;
}
button:hover {
background-color: #5e35b1;
}
.data-section {
margin: 20px 0;
padding: 15px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #ce93d8;
}
.data-section h3 {
margin-top: 0;
color: #495057;
}
.log-section {
padding: 20px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #ce93d8;
}
.log-list {
max-height: 200px;
overflow-y: auto;
background-color: #f8f9fa;
border-radius: 4px;
padding: 10px;
margin: 0;
font-size: 14px;
}
</style>
3. 销毁阶段 (Destruction)
vue
<template>
<div class="destruction-stage">
<h2>销毁阶段演示</h2>
<div class="component-controls">
<button @click="toggleComponent" class="toggle-btn">
{{ showChild ? '销毁子组件' : '创建子组件' }}
</button>
</div>
<!-- 条件渲染子组件 -->
<div v-if="showChild" class="child-container">
<LifecycleChild
@child-mounted="handleChildMounted"
@child-unmounted="handleChildUnmounted"
/>
</div>
<div class="resource-section">
<h3>资源管理演示</h3>
<div class="resource-controls">
<button @click="startTimer">启动定时器</button>
<button @click="addEventListeners">添加事件监听器</button>
<button @click="createInterval">创建间隔任务</button>
</div>
<div class="resource-status">
<p>定时器运行: {{ timerRunning ? '是' : '否' }}</p>
<p>事件监听器添加: {{ eventListenersAdded ? '是' : '否' }}</p>
<p>间隔任务运行: {{ intervalRunning ? '是' : '否' }}</p>
</div>
</div>
<div class="log-section">
<h3>销毁日志:</h3>
<ul class="log-list">
<li v-for="(log, index) in destructionLogs" :key="index">
{{ log }}
</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref, onBeforeUnmount, onUnmounted } from 'vue'
import LifecycleChild from './LifecycleChild.vue'
const showChild = ref(true)
const destructionLogs = ref([])
const timerRunning = ref(false)
const eventListenersAdded = ref(false)
const intervalRunning = ref(false)
let timerId = null
let intervalId = null
// 记录销毁日志
const addDestructionLog = (message) => {
destructionLogs.value.push(`${message} - ${new Date().toLocaleTimeString()}`)
console.log(message)
}
// 切换子组件显示
const toggleComponent = () => {
showChild.value = !showChild.value
}
// 处理子组件事件
const handleChildMounted = () => {
addDestructionLog('👶 子组件已挂载')
}
const handleChildUnmounted = () => {
addDestructionLog('👶 子组件已卸载')
}
// 资源管理演示
const startTimer = () => {
if (timerId) {
clearTimeout(timerId)
}
timerRunning.value = true
timerId = setTimeout(() => {
addDestructionLog('⏰ 定时器执行完成')
timerRunning.value = false
}, 3000)
addDestructionLog('⏰ 定时器已启动 (3秒后执行)')
}
const addEventListeners = () => {
if (!eventListenersAdded.value) {
// 添加事件监听器
const handleClick = () => {
addDestructionLog('🖱️ 文档点击事件触发')
}
document.addEventListener('click', handleClick)
eventListenersAdded.value = true
addDestructionLog('🖱️ 文档点击事件监听器已添加')
// 保存事件处理函数引用,用于清理
window._handleClick = handleClick
}
}
const createInterval = () => {
if (intervalId) {
clearInterval(intervalId)
}
intervalRunning.value = true
intervalId = setInterval(() => {
addDestructionLog('⏱️ 间隔任务执行')
}, 2000)
addDestructionLog('⏱️ 间隔任务已创建 (每2秒执行)')
}
// onBeforeUnmount - 卸载之前
onBeforeUnmount(() => {
addDestructionLog('🟣 onBeforeUnmount: 组件即将卸载')
console.log('组件即将被销毁,准备清理资源')
})
// onUnmounted - 卸载之后
onUnmounted(() => {
addDestructionLog('🔴 onUnmounted: 组件已卸载')
console.log('组件已被销毁,执行清理工作')
// 清理所有资源
cleanupResources()
})
// 清理资源函数
const cleanupResources = () => {
// 清理定时器
if (timerId) {
clearTimeout(timerId)
timerId = null
timerRunning.value = false
addDestructionLog('🧹 定时器已清理')
}
// 清理间隔任务
if (intervalId) {
clearInterval(intervalId)
intervalId = null
intervalRunning.value = false
addDestructionLog('🧹 间隔任务已清理')
}
// 移除事件监听器
if (eventListenersAdded.value && window._handleClick) {
document.removeEventListener('click', window._handleClick)
eventListenersAdded.value = false
addDestructionLog('🧹 事件监听器已移除')
delete window._handleClick
}
}
</script>
<style>
.destruction-stage {
padding: 20px;
background-color: #ffebee;
border-radius: 8px;
margin: 20px 0;
}
.component-controls {
margin-bottom: 20px;
}
.toggle-btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
background-color: #f44336;
color: white;
cursor: pointer;
font-size: 16px;
}
.toggle-btn:hover {
background-color: #d32f2f;
}
.child-container {
margin: 20px 0;
padding: 20px;
background-color: #ffcdd2;
border-radius: 8px;
border: 2px dashed #f44336;
}
.resource-section {
margin: 20px 0;
padding: 20px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #ffcdd2;
}
.resource-section h3 {
margin-top: 0;
color: #495057;
}
.resource-controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 15px;
}
.resource-controls button {
padding: 8px 16px;
border: none;
border-radius: 4px;
background-color: #e91e63;
color: white;
cursor: pointer;
font-size: 14px;
}
.resource-controls button:hover {
background-color: #c2185b;
}
.resource-status p {
margin: 5px 0;
color: #e91e63;
}
.log-section {
padding: 20px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #ffcdd2;
}
.log-list {
max-height: 300px;
overflow-y: auto;
background-color: #f8f9fa;
border-radius: 4px;
padding: 10px;
margin: 0;
font-size: 14px;
}
</style>
vue
<!-- LifecycleChild.vue -->
<template>
<div class="lifecycle-child">
<h3>子组件</h3>
<p>我是生命周期演示的子组件</p>
<p>创建时间: {{ creationTime }}</p>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const creationTime = ref(new Date().toLocaleTimeString())
// 组件挂载时通知父组件
onMounted(() => {
console.log('👶 子组件已挂载')
emit('childMounted')
})
// 组件卸载时通知父组件
onUnmounted(() => {
console.log('👶 子组件已卸载')
emit('childUnmounted')
})
const emit = defineEmits(['childMounted', 'childUnmounted'])
</script>
<style scoped>
.lifecycle-child {
padding: 20px;
background-color: #fff3e0;
border-radius: 8px;
border: 2px solid #ff9800;
text-align: center;
}
.lifecycle-child h3 {
margin-top: 0;
color: #e65100;
}
</style>
实际应用示例
1. 数据获取和网络请求
vue
<template>
<div class="data-fetching-demo">
<h2>数据获取生命周期示例</h2>
<div class="controls">
<button @click="refreshData" :disabled="loading" class="refresh-btn">
{{ loading ? '加载中...' : '刷新数据' }}
</button>
<button @click="clearData" class="clear-btn">清空数据</button>
</div>
<div class="status-section">
<p>加载状态: {{ loadingStatus }}</p>
<p>数据状态: {{ dataStatus }}</p>
<p>错误信息: {{ errorMessage || '无' }}</p>
</div>
<div v-if="loading" class="loading-section">
<div class="spinner"></div>
<p>正在加载数据...</p>
</div>
<div v-else-if="userData" class="data-section">
<h3>用户数据:</h3>
<div class="user-card">
<div class="user-avatar">{{ userData.name.charAt(0) }}</div>
<div class="user-info">
<p><strong>姓名:</strong> {{ userData.name }}</p>
<p><strong>邮箱:</strong> {{ userData.email }}</p>
<p><strong>年龄:</strong> {{ userData.age }}</p>
<p><strong>城市:</strong> {{ userData.city }}</p>
</div>
</div>
</div>
<div v-else-if="errorMessage" class="error-section">
<p>❌ {{ errorMessage }}</p>
<button @click="retryFetch" class="retry-btn">重试</button>
</div>
<div class="log-section">
<h3>请求日志:</h3>
<ul class="log-list">
<li v-for="(log, index) in fetchLogs" :key="index">
{{ log }}
</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
const userData = ref(null)
const loading = ref(false)
const loadingStatus = ref('空闲')
const dataStatus = ref('无数据')
const errorMessage = ref('')
const fetchLogs = ref([])
let abortController = null
// 记录请求日志
const addFetchLog = (message) => {
fetchLogs.value.push(`${message} - ${new Date().toLocaleTimeString()}`)
console.log(message)
}
// 模拟 API 请求
const fetchUserData = async () => {
loading.value = true
loadingStatus.value = '加载中'
errorMessage.value = ''
dataStatus.value = '请求中'
addFetchLog('🌐 开始获取用户数据')
// 创建 AbortController 用于取消请求
abortController = new AbortController()
try {
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 2000))
// 模拟随机成功/失败
if (Math.random() > 0.3) {
// 模拟成功响应
const mockData = {
name: '张三',
email: 'zhangsan@example.com',
age: 25,
city: '北京'
}
userData.value = mockData
dataStatus.value = '数据已加载'
addFetchLog('✅ 用户数据获取成功')
} else {
// 模拟失败响应
throw new Error('网络连接失败,请稍后重试')
}
} catch (error) {
if (error.name !== 'AbortError') {
errorMessage.value = error.message
dataStatus.value = '加载失败'
addFetchLog(`❌ 请求失败: ${error.message}`)
}
} finally {
loading.value = false
loadingStatus.value = '空闲'
}
}
// 刷新数据
const refreshData = () => {
// 取消之前的请求
if (abortController) {
abortController.abort()
}
fetchUserData()
}
// 重试获取
const retryFetch = () => {
refreshData()
}
// 清空数据
const clearData = () => {
userData.value = null
dataStatus.value = '数据已清空'
addFetchLog('🧹 数据已清空')
}
// onMounted - 组件挂载后获取数据
onMounted(() => {
addFetchLog('🟢 onMounted: 组件已挂载,开始获取数据')
fetchUserData()
})
// onBeforeUnmount - 组件卸载前清理资源
onBeforeUnmount(() => {
addFetchLog('🟣 onBeforeUnmount: 组件即将卸载,清理资源')
// 取消未完成的请求
if (abortController) {
abortController.abort()
addFetchLog('🧹 未完成的请求已取消')
}
})
</script>
<style>
.data-fetching-demo {
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #e8f5e9;
border-radius: 8px;
}
.controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 20px;
}
.refresh-btn, .clear-btn, .retry-btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: all 0.2s ease;
}
.refresh-btn {
background-color: #4caf50;
color: white;
}
.refresh-btn:hover:not(:disabled) {
background-color: #45a049;
}
.refresh-btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.clear-btn, .retry-btn {
background-color: #ff9800;
color: white;
}
.clear-btn:hover, .retry-btn:hover {
background-color: #e68900;
}
.status-section {
margin: 20px 0;
padding: 15px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #c8e6c9;
}
.status-section p {
margin: 8px 0;
color: #2e7d32;
}
.loading-section {
text-align: center;
padding: 40px 20px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #c8e6c9;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #4caf50;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.data-section {
margin: 20px 0;
}
.data-section h3 {
color: #495057;
margin-bottom: 15px;
}
.user-card {
display: flex;
align-items: center;
padding: 20px;
background-color: #fff;
border-radius: 8px;
border: 1px solid #c8e6c9;
}
.user-avatar {
font-size: 48px;
margin-right: 20px;
}
.user-info p {
margin: 8px 0;
color: #495057;
}
.error-section {
padding: 30px 20px;
text-align: center;
background-color: #ffebee;
border-radius: 4px;
border: 1px solid #ffcdd2;
color: #c62828;
}
.log-section {
margin-top: 30px;
padding: 20px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #c8e6c9;
}
.log-section h3 {
margin-top: 0;
color: #495057;
}
.log-list {
max-height: 200px;
overflow-y: auto;
background-color: #f8f9fa;
border-radius: 4px;
padding: 10px;
margin: 0;
font-size: 14px;
}
</style>
2. 定时器和轮询任务
vue
<template>
<div class="timer-demo">
<h2>定时器和轮询任务</h2>
<div class="timer-controls">
<button @click="startTimer" :disabled="isTimerRunning" class="start-btn">
开始定时器
</button>
<button @click="stopTimer" :disabled="!isTimerRunning" class="stop-btn">
停止定时器
</button>
<button @click="startPolling" :disabled="isPolling" class="poll-start-btn">
开始轮询
</button>
<button @click="stopPolling" :disabled="!isPolling" class="poll-stop-btn">
停止轮询
</button>
</div>
<div class="status-section">
<div class="status-item">
<span>定时器状态:</span>
<span :class="['status', isTimerRunning ? 'running' : 'stopped']">
{{ isTimerRunning ? '运行中' : '已停止' }}
</span>
</div>
<div class="status-item">
<span>轮询状态:</span>
<span :class="['status', isPolling ? 'running' : 'stopped']">
{{ isPolling ? '运行中' : '已停止' }}
</span>
</div>
<div class="status-item">
<span>定时器计数:</span>
<span class="count">{{ timerCount }}</span>
</div>
<div class="status-item">
<span>轮询计数:</span>
<span class="count">{{ pollingCount }}</span>
</div>
</div>
<div class="clock-section">
<h3>实时时钟</h3>
<div class="clock-display">
{{ currentTime }}
</div>
</div>
<div class="log-section">
<h3>定时器日志:</h3>
<ul class="log-list">
<li v-for="(log, index) in timerLogs" :key="index">
{{ log }}
</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
const isTimerRunning = ref(false)
const isPolling = ref(false)
const timerCount = ref(0)
const pollingCount = ref(0)
const currentTime = ref(new Date().toLocaleTimeString())
const timerLogs = ref([])
let timerId = null
let pollingInterval = null
let clockInterval = null
// 记录定时器日志
const addTimerLog = (message) => {
timerLogs.value.push(`${message} - ${new Date().toLocaleTimeString()}`)
console.log(message)
}
// 定时器任务
const startTimer = () => {
if (isTimerRunning.value) return
isTimerRunning.value = true
timerCount.value = 0
timerId = setTimeout(() => {
addTimerLog('⏰ 3秒定时器执行')
isTimerRunning.value = false
}, 3000)
addTimerLog('⏰ 定时器已启动 (3秒后执行)')
}
const stopTimer = () => {
if (timerId) {
clearTimeout(timerId)
timerId = null
isTimerRunning.value = false
addTimerLog('🧹 定时器已停止')
}
}
// 轮询任务
const startPolling = () => {
if (isPolling.value) return
isPolling.value = true
pollingCount.value = 0
pollingInterval = setInterval(() => {
pollingCount.value++
addTimerLog(`⏱️ 轮询任务执行 #${pollingCount.value}`)
}, 2000)
addTimerLog('⏱️ 轮询已启动 (每2秒执行)')
}
const stopPolling = () => {
if (pollingInterval) {
clearInterval(pollingInterval)
pollingInterval = null
isPolling.value = false
addTimerLog('🧹 轮询已停止')
}
}
// 实时时钟
const updateClock = () => {
currentTime.value = new Date().toLocaleTimeString()
}
// onMounted - 组件挂载后启动时钟
onMounted(() => {
addTimerLog('🟢 onMounted: 组件已挂载')
// 启动实时时钟
clockInterval = setInterval(updateClock, 1000)
addTimerLog('🕒 实时时钟已启动')
// 可以在这里启动默认的定时器或轮询
// startTimer()
// startPolling()
})
// onBeforeUnmount - 组件卸载前清理所有定时器
onBeforeUnmount(() => {
addTimerLog('🟣 onBeforeUnmount: 组件即将卸载')
// 清理所有定时器和间隔任务
stopTimer()
stopPolling()
if (clockInterval) {
clearInterval(clockInterval)
clockInterval = null
addTimerLog('🧹 实时时钟已停止')
}
addTimerLog('🧹 所有定时器资源已清理')
})
</script>
<style>
.timer-demo {
max-width: 700px;
margin: 0 auto;
padding: 20px;
background-color: #fff3e0;
border-radius: 8px;
}
.timer-controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 30px;
}
.start-btn, .stop-btn, .poll-start-btn, .poll-stop-btn {
padding: 10px 16px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.start-btn {
background-color: #4caf50;
color: white;
}
.start-btn:hover:not(:disabled) {
background-color: #45a049;
}
.stop-btn {
background-color: #f44336;
color: white;
}
.stop-btn:hover:not(:disabled) {
background-color: #da190b;
}
.poll-start-btn {
background-color: #2196f3;
color: white;
}
.poll-start-btn:hover:not(:disabled) {
background-color: #0b7dda;
}
.poll-stop-btn {
background-color: #ff9800;
color: white;
}
.poll-stop-btn:hover:not(:disabled) {
background-color: #e68900;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.status-section {
margin: 25px 0;
padding: 20px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #ffe0b2;
}
.status-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.status-item:last-child {
border-bottom: none;
}
.status {
padding: 4px 12px;
border-radius: 12px;
font-size: 14px;
font-weight: bold;
}
.status.running {
background-color: #c8e6c9;
color: #2e7d32;
}
.status.stopped {
background-color: #ffcdd2;
color: #c62828;
}
.count {
font-weight: bold;
color: #ff9800;
}
.clock-section {
text-align: center;
margin: 30px 0;
}
.clock-section h3 {
color: #495057;
margin-bottom: 15px;
}
.clock-display {
font-size: 36px;
font-weight: bold;
font-family: 'Courier New', monospace;
color: #e65100;
background-color: #fff;
padding: 20px;
border-radius: 8px;
border: 2px solid #ff9800;
}
.log-section {
padding: 20px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #ffe0b2;
}
.log-section h3 {
margin-top: 0;
color: #495057;
}
.log-list {
max-height: 300px;
overflow-y: auto;
background-color: #f8f9fa;
border-radius: 4px;
padding: 10px;
margin: 0;
font-size: 14px;
}
</style>
3. 事件监听器管理
vue
<template>
<div class="event-demo">
<h2>事件监听器管理</h2>
<div class="event-controls">
<button @click="addEventListeners" :disabled="listenersAdded" class="add-btn">
添加事件监听器
</button>
<button @click="removeEventListeners" :disabled="!listenersAdded" class="remove-btn">
移除事件监听器
</button>
<button @click="toggleWindowEvents" class="toggle-btn">
{{ windowEventsEnabled ? '禁用窗口事件' : '启用窗口事件' }}
</button>
</div>
<div class="status-section">
<div class="status-grid">
<div class="status-item">
<span>监听器状态:</span>
<span :class="['status', listenersAdded ? 'active' : 'inactive']">
{{ listenersAdded ? '已添加' : '未添加' }}
</span>
</div>
<div class="status-item">
<span>窗口事件:</span>
<span :class="['status', windowEventsEnabled ? 'active' : 'inactive']">
{{ windowEventsEnabled ? '已启用' : '已禁用' }}
</span>
</div>
<div class="status-item">
<span>点击计数:</span>
<span class="count">{{ clickCount }}</span>
</div>
<div class="status-item">
<span>键盘计数:</span>
<span class="count">{{ keyCount }}</span>
</div>
</div>
</div>
<div class="interaction-area">
<h3>交互区域</h3>
<div
class="click-area"
@click="handleAreaClick"
:style="{ backgroundColor: areaColor }"
>
<p>点击这个区域</p>
<p>当前颜色: {{ areaColor }}</p>
</div>
<div class="keyboard-area">
<p>在页面任意位置按键盘:</p>
<p>最后按键: {{ lastKey }}</p>
<p>按 Ctrl+C 复制文本</p>
</div>
</div>
<div class="log-section">
<h3>事件日志:</h3>
<ul class="log-list">
<li v-for="(log, index) in eventLogs" :key="index">
{{ log }}
</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
const listenersAdded = ref(false)
const windowEventsEnabled = ref(false)
const clickCount = ref(0)
const keyCount = ref(0)
const lastKey = ref('')
const areaColor = ref('#e3f2fd')
const eventLogs = ref([])
// 事件处理函数(需要保存引用以便移除)
const handleClick = (event) => {
clickCount.value++
addEventLog(`🖱️ 页面点击事件: (${event.clientX}, ${event.clientY})`)
}
const handleKeydown = (event) => {
keyCount.value++
lastKey.value = event.key
addEventLog(`⌨️ 键盘按下: ${event.key} (${event.code})`)
// 特殊按键处理
if (event.ctrlKey && event.key === 'c') {
addEventLog('📋 Ctrl+C 被按下')
}
}
const handleResize = () => {
addEventLog(`📐 窗口大小改变: ${window.innerWidth} x ${window.innerHeight}`)
}
const handleScroll = () => {
addEventLog(`📜 页面滚动: ${window.scrollY}px`)
}
const handleMouseMove = (event) => {
// 限制日志频率
if (eventLogs.value.length % 10 === 0) {
addEventLog(`🖱️ 鼠标移动: (${event.clientX}, ${event.clientY})`)
}
}
// 记录事件日志
const addEventLog = (message) => {
eventLogs.value.push(`${message} - ${new Date().toLocaleTimeString()}`)
console.log(message)
// 限制日志数量
if (eventLogs.value.length > 100) {
eventLogs.value.shift()
}
}
// 添加事件监听器
const addEventListeners = () => {
if (listenersAdded.value) return
// 添加各种事件监听器
document.addEventListener('click', handleClick)
document.addEventListener('keydown', handleKeydown)
window.addEventListener('resize', handleResize)
window.addEventListener('scroll', handleScroll)
document.addEventListener('mousemove', handleMouseMove)
listenersAdded.value = true
addEventLog('👂 事件监听器已添加')
}
// 移除事件监听器
const removeEventListeners = () => {
if (!listenersAdded.value) return
// 移除事件监听器
document.removeEventListener('click', handleClick)
document.removeEventListener('keydown', handleKeydown)
window.removeEventListener('resize', handleResize)
window.removeEventListener('scroll', handleScroll)
document.removeEventListener('mousemove', handleMouseMove)
listenersAdded.value = false
addEventLog('🧹 事件监听器已移除')
}
// 切换窗口事件
const toggleWindowEvents = () => {
windowEventsEnabled.value = !windowEventsEnabled.value
if (windowEventsEnabled.value) {
window.addEventListener('resize', handleResize)
window.addEventListener('scroll', handleScroll)
addEventLog('👂 窗口事件监听器已启用')
} else {
window.removeEventListener('resize', handleResize)
window.removeEventListener('scroll', handleScroll)
addEventLog('🧹 窗口事件监听器已禁用')
}
}
// 区域点击处理
const handleAreaClick = () => {
// 改变区域颜色
const colors = ['#e3f2fd', '#f3e5f5', '#e8f5e9', '#fff3e0', '#fce4ec']
const currentIndex = colors.indexOf(areaColor.value)
const nextIndex = (currentIndex + 1) % colors.length
areaColor.value = colors[nextIndex]
addEventLog('🎨 区域点击,颜色已改变')
}
// onMounted - 组件挂载后添加默认监听器
onMounted(() => {
addEventLog('🟢 onMounted: 组件已挂载')
// 可以在这里添加默认的事件监听器
// addEventListeners()
})
// onBeforeUnmount - 组件卸载前移除所有监听器
onBeforeUnmount(() => {
addEventLog('🟣 onBeforeUnmount: 组件即将卸载')
// 确保移除所有事件监听器
removeEventListeners()
toggleWindowEvents()
addEventLog('🧹 所有事件监听器已清理')
})
</script>
<style>
.event-demo {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f3e5f5;
border-radius: 8px;
}
.event-controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 30px;
}
.add-btn, .remove-btn, .toggle-btn {
padding: 10px 16px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.add-btn {
background-color: #9c27b0;
color: white;
}
.add-btn:hover:not(:disabled) {
background-color: #7b1fa2;
}
.remove-btn {
background-color: #f44336;
color: white;
}
.remove-btn:hover:not(:disabled) {
background-color: #da190b;
}
.toggle-btn {
background-color: #ff9800;
color: white;
}
.toggle-btn:hover {
background-color: #e68900;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.status-section {
margin: 25px 0;
}
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.status-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #ce93d8;
}
.status {
padding: 4px 12px;
border-radius: 12px;
font-size: 14px;
font-weight: bold;
}
.status.active {
background-color: #e8f5e9;
color: #2e7d32;
}
.status.inactive {
background-color: #ffcdd2;
color: #c62828;
}
.count {
font-weight: bold;
color: #9c27b0;
}
.interaction-area {
margin: 30px 0;
}
.interaction-area h3 {
color: #495057;
margin-bottom: 20px;
}
.click-area {
padding: 40px 20px;
text-align: center;
border-radius: 8px;
margin-bottom: 30px;
cursor: pointer;
transition: all 0.3s ease;
border: 2px dashed #9c27b0;
}
.click-area:hover {
transform: scale(1.02);
}
.click-area p {
margin: 10px 0;
color: #495057;
font-weight: bold;
}
.keyboard-area {
padding: 20px;
background-color: #fff;
border-radius: 8px;
border: 1px solid #ce93d8;
text-align: center;
}
.keyboard-area p {
margin: 10px 0;
color: #495057;
}
.log-section {
padding: 20px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #ce93d8;
}
.log-section h3 {
margin-top: 0;
color: #495057;
}
.log-list {
max-height: 300px;
overflow-y: auto;
background-color: #f8f9fa;
border-radius: 4px;
padding: 10px;
margin: 0;
font-size: 14px;
}
</style>
生命周期钩子对比表
Vue2 vs Vue3
Vue2 选项式 API | Vue3 组合式 API | 说明 |
---|---|---|
beforeCreate |
setup() |
组件创建前 |
created |
setup() |
组件创建后 |
beforeMount |
onBeforeMount |
挂载前 |
mounted |
onMounted |
挂载后 |
beforeUpdate |
onBeforeUpdate |
更新前 |
updated |
onUpdated |
更新后 |
beforeDestroy |
onBeforeUnmount |
销毁前 |
destroyed |
onUnmounted |
销毁后 |
Vue3 新增的生命周期钩子
vue
<template>
<div class="new-lifecycle-demo">
<h2>Vue3 新增生命周期钩子</h2>
<div class="render-demo">
<h3>渲染跟踪演示</h3>
<button @click="updateData">更新数据</button>
<p>计数器: {{ counter }}</p>
<p>双倍: {{ doubleCounter }}</p>
</div>
<div class="error-demo">
<h3>错误处理演示</h3>
<button @click="triggerError">触发错误</button>
<button @click="clearError">清除错误</button>
<p v-if="error" class="error-message">错误: {{ error.message }}</p>
</div>
</div>
</template>
<script setup>
import { ref, computed, onErrorCaptured, onRenderTracked, onRenderTriggered } from 'vue'
const counter = ref(0)
const error = ref(null)
// 计算属性
const doubleCounter = computed(() => counter.value * 2)
// 更新数据
const updateData = () => {
counter.value++
}
// 触发错误
const triggerError = () => {
throw new Error('这是一个测试错误')
}
// 清除错误
const clearError = () => {
error.value = null
}
// onRenderTracked - 响应式依赖被追踪时触发
onRenderTracked((event) => {
console.log('🔍 onRenderTracked:', event)
// event 包含 type, key, target, effect 等信息
})
// onRenderTriggered - 响应式依赖被触发时触发
onRenderTriggered((event) => {
console.log('⚡ onRenderTriggered:', event)
// 当响应式数据变化导致重新渲染时触发
})
// onErrorCaptured - 捕获子组件错误
onErrorCaptured((err, instance, info) => {
console.log('💥 onErrorCaptured:', err, instance, info)
error.value = err
return false // 阻止错误继续传播
})
</script>
<style>
.new-lifecycle-demo {
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #e1f5fe;
border-radius: 8px;
}
.render-demo, .error-demo {
margin: 25px 0;
padding: 20px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #b3e5fc;
}
.render-demo h3, .error-demo h3 {
margin-top: 0;
color: #495057;
}
button {
padding: 8px 16px;
margin: 5px;
border: none;
border-radius: 4px;
background-color: #0277bd;
color: white;
cursor: pointer;
font-size: 14px;
}
button:hover {
background-color: #01579b;
}
.error-message {
color: #c62828;
background-color: #ffebee;
padding: 10px;
border-radius: 4px;
margin-top: 15px;
}
</style>
总结
生命周期钩子使用场景
钩子 | 使用场景 |
---|---|
onMounted |
发起网络请求、访问 DOM、启动定时器 |
onBeforeUpdate |
在 DOM 更新前获取当前状态 |
onUpdated |
DOM 更新后执行操作(谨慎使用) |
onBeforeUnmount |
清理定时器、事件监听器、取消网络请求 |
onUnmounted |
最后的清理工作 |
最佳实践
-
资源管理:
- 在
onBeforeUnmount
中清理定时器和事件监听器 - 使用
AbortController
取消网络请求
- 在
-
性能优化:
- 避免在
onUpdated
中修改响应式数据 - 合理使用防抖和节流
- 避免在
-
错误处理:
- 使用
onErrorCaptured
捕获子组件错误 - 提供友好的错误提示
- 使用
记忆口诀:
- 创建阶段:setup → onBeforeMount → onMounted
- 更新阶段:onBeforeUpdate → onUpdated
- 销毁阶段:onBeforeUnmount → onUnmounted
- 挂载后:可以访问 DOM 和发起请求
- 销毁前:记得清理资源和定时器
这样就能很好地掌握 Vue3 组件的生命周期了!