Vue 3 定时器清理的最佳实践
在 Vue 3 中,清理定时器的最佳位置取决于组件的使用场景和定时器的用途。我将设计一个直观的示例来演示不同生命周期钩子中定时器的清理方式。设计的定时器截图如下

设计思路
- 展示不同生命周期钩子中定时器的创建和清理
- 提供可视化界面展示定时器状态
- 允许用户手动创建和清理定时器
- 演示组件卸载时的自动清理
功能说明
这个示例演示了在 Vue 3 中管理定时器的最佳实践:
-
定时器创建和清理:
- 在
onMounted钩子中创建定时器 - 在
onUnmounted钩子中清理定时器(必须) - 也可在
onBeforeUnmount中清理
- 在
-
生命周期演示:
- 通过切换子组件显示/隐藏来演示组件卸载时的定时器清理
- 在控制台输出生命周期事件
-
手动管理:
- 提供手动创建和清理定时器的功能
- 显示所有活动定时器的状态和进度
-
最佳实践代码示例:
- 展示在 Vue 3 组件中正确管理定时器的代码模式
这个示例强调了在 Vue 3 中,无论定时器是在哪个生命周期创建的,都必须在 onUnmounted 或 onBeforeUnmount 中清理,以防止内存泄漏。
最终实现代码
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue 3 定时器生命周期管理</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
color: #fff;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: rgba(0, 0, 0, 0.7);
border-radius: 15px;
padding: 30px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
header {
text-align: center;
margin-bottom: 30px;
}
h1 {
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}
.subtitle {
font-size: 1.2rem;
opacity: 0.8;
margin-bottom: 20px;
}
.content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
}
@media (max-width: 768px) {
.content {
grid-template-columns: 1fr;
}
}
.card {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
}
.card h2 {
margin-bottom: 15px;
color: #fdbb2d;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
padding-bottom: 10px;
}
.timer-controls {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-bottom: 20px;
}
.control-group {
flex: 1;
min-width: 200px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
}
input, select, button {
width: 100%;
padding: 12px;
border: none;
border-radius: 5px;
font-size: 1rem;
}
input, select {
background: rgba(255, 255, 255, 0.9);
}
button {
background: #4CAF50;
color: white;
cursor: pointer;
transition: all 0.3s;
font-weight: bold;
margin-top: 10px;
}
button:hover {
background: #45a049;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.danger-btn {
background: #f44336;
}
.danger-btn:hover {
background: #d32f2f;
}
.warning-btn {
background: #ff9800;
}
.warning-btn:hover {
background: #f57c00;
}
.timer-list {
margin-top: 20px;
}
.timer-item {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255, 255, 255, 0.1);
padding: 15px;
margin-bottom: 10px;
border-radius: 8px;
transition: all 0.3s;
}
.timer-item:hover {
background: rgba(255, 255, 255, 0.15);
transform: translateX(5px);
}
.timer-info {
flex: 1;
}
.timer-id {
font-weight: bold;
font-size: 1.1rem;
}
.timer-details {
display: flex;
gap: 15px;
margin-top: 5px;
font-size: 0.9rem;
opacity: 0.8;
}
.timer-actions {
display: flex;
gap: 10px;
}
.timer-actions button {
margin: 0;
padding: 8px 15px;
width: auto;
}
.status {
padding: 5px 10px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: bold;
}
.status-active {
background: #4CAF50;
}
.status-cleared {
background: #f44336;
}
.lifecycle-info {
margin-top: 30px;
padding: 20px;
background: rgba(0, 0, 0, 0.3);
border-radius: 10px;
}
.lifecycle-info h3 {
margin-bottom: 15px;
color: #fdbb2d;
}
.lifecycle-info ul {
padding-left: 20px;
margin-bottom: 15px;
}
.lifecycle-info li {
margin-bottom: 8px;
line-height: 1.5;
}
.highlight {
color: #fdbb2d;
font-weight: bold;
}
.component-demo {
margin-top: 30px;
padding: 20px;
border: 2px dashed rgba(255, 255, 255, 0.3);
border-radius: 10px;
}
.toggle-btn {
background: #2196F3;
width: 100%;
}
.toggle-btn:hover {
background: #0b7dda;
}
.stats {
display: flex;
justify-content: space-between;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
.stat-box {
text-align: center;
flex: 1;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
margin-bottom: 5px;
}
.stat-label {
font-size: 0.9rem;
opacity: 0.8;
}
.empty-state {
text-align: center;
padding: 40px;
opacity: 0.7;
}
.pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.progress-bar {
height: 5px;
background: rgba(255, 255, 255, 0.2);
border-radius: 5px;
margin-top: 10px;
overflow: hidden;
}
.progress {
height: 100%;
background: #4CAF50;
width: 0%;
transition: width 0.5s;
}
.code-block {
background: rgba(0, 0, 0, 0.5);
padding: 15px;
border-radius: 5px;
font-family: monospace;
margin: 15px 0;
overflow-x: auto;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<header>
<h1>Vue 3 定时器生命周期管理</h1>
<p class="subtitle">演示在不同生命周期钩子中创建和清理定时器的最佳实践</p>
</header>
<div class="content">
<div>
<div class="card">
<h2>定时器控制面板</h2>
<div class="timer-controls">
<div class="control-group">
<label for="timerType">定时器类型</label>
<select id="timerType" v-model="timerType">
<option value="timeout">setTimeout (一次性)</option>
<option value="interval">setInterval (重复)</option>
</select>
</div>
<div class="control-group">
<label for="timerDuration">持续时间 (毫秒)</label>
<input type="number" id="timerDuration" v-model.number="timerDuration" min="100" max="100000">
</div>
<div class="control-group">
<label for="timerMessage">定时器消息</label>
<input type="text" id="timerMessage" v-model="timerMessage" placeholder="输入定时器执行时显示的消息">
</div>
</div>
<button @click="addTimer" class="pulse">添加定时器</button>
<button @click="clearAllTimers" class="danger-btn">清理所有定时器</button>
</div>
<div class="card">
<h2>活动定时器 ({{ activeTimersCount }})</h2>
<div class="timer-list">
<div v-if="activeTimers.length === 0" class="empty-state">
暂无活动定时器
</div>
<div v-else v-for="timer in activeTimers" :key="timer.id" class="timer-item">
<div class="timer-info">
<div class="timer-id">定时器 #{{ timer.id }}</div>
<div class="timer-details">
<span>类型: {{ timer.type === 'timeout' ? 'setTimeout' : 'setInterval' }}</span>
<span>持续时间: {{ timer.duration }}ms</span>
<span>消息: "{{ timer.message }}"</span>
</div>
<div class="progress-bar">
<div class="progress" :style="{ width: timer.progress + '%' }"></div>
</div>
</div>
<div class="timer-actions">
<span class="status status-active">活动</span>
<button class="danger-btn" @click="clearTimer(timer.id)">清理</button>
</div>
</div>
</div>
</div>
</div>
<div>
<div class="card">
<h2>生命周期演示</h2>
<div class="lifecycle-info">
<h3>Vue 3 定时器清理最佳实践</h3>
<ul>
<li><span class="highlight">onMounted</span> - 在组件挂载后创建定时器</li>
<li><span class="highlight">onUnmounted</span> - 在组件卸载前清理定时器(必须)</li>
<li><span class="highlight">onBeforeUnmount</span> - 在组件卸载前清理定时器的替代方案</li>
<li><span class="highlight">watchEffect</span> - 响应式地创建和清理定时器</li>
<li><span class="highlight">手动清理</span> - 在需要时手动清理特定定时器</li>
</ul>
<div class="code-block">
// 最佳实践示例<br>
import { onMounted, onUnmounted, ref } from 'vue'<br><br>
const timerId = ref(null)<br><br>
onMounted(() => {<br>
// 创建定时器<br>
timerId.value = setInterval(() => {<br>
// 定时器逻辑<br>
}, 1000)<br>
})<br><br>
onUnmounted(() => {<br>
// 清理定时器<br>
if (timerId.value) {<br>
clearInterval(timerId.value)<br>
}<br>
})
</div>
</div>
<div class="component-demo">
<h3>组件卸载演示</h3>
<p>点击按钮切换子组件显示/隐藏,观察控制台输出</p>
<button class="toggle-btn" @click="toggleComponent">
{{ showChildComponent ? '隐藏' : '显示' }}子组件
</button>
<div v-if="showChildComponent">
<child-component></child-component>
</div>
</div>
</div>
<div class="stats">
<div class="stat-box">
<div class="stat-value">{{ activeTimersCount }}</div>
<div class="stat-label">活动定时器</div>
</div>
<div class="stat-box">
<div class="stat-value">{{ clearedTimersCount }}</div>
<div class="stat-label">已清理定时器</div>
</div>
<div class="stat-box">
<div class="stat-value">{{ totalTimersCount }}</div>
<div class="stat-label">总定时器</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const { createApp, ref, onMounted, onUnmounted, computed, onBeforeUnmount } = Vue;
// 子组件演示生命周期
const ChildComponent = {
template: `
<div class="card" style="margin-top: 15px;">
<h3>子组件生命周期演示</h3>
<p>组件状态: <span style="color: #4CAF50;">已挂载</span></p>
<p>定时器ID: {{ timerId || '无' }}</p>
<p>计时: {{ count }} 秒</p>
<button class="warning-btn" @click="stopTimer">停止定时器</button>
</div>
`,
setup() {
const timerId = ref(null);
const count = ref(0);
// 在 onMounted 中创建定时器
onMounted(() => {
console.log('子组件已挂载,创建定时器');
timerId.value = setInterval(() => {
count.value++;
console.log(`子组件定时器执行: ${count.value} 秒`);
}, 1000);
});
// 在 onUnmounted 中清理定时器 (最佳实践)
onUnmounted(() => {
console.log('子组件即将卸载,清理定时器');
if (timerId.value) {
clearInterval(timerId.value);
console.log('定时器已清理');
}
});
// 也可以在 onBeforeUnmount 中清理
onBeforeUnmount(() => {
console.log('onBeforeUnmount: 清理定时器');
});
const stopTimer = () => {
if (timerId.value) {
clearInterval(timerId.value);
timerId.value = null;
console.log('手动停止定时器');
}
};
return {
timerId,
count,
stopTimer
};
}
};
const app = createApp({
components: {
ChildComponent
},
setup() {
// 定时器数据
const timers = ref([]);
const timerIdCounter = ref(1);
const timerType = ref('timeout');
const timerDuration = ref(3000);
const timerMessage = ref('定时器已触发!');
const showChildComponent = ref(false);
// 计算属性
const activeTimers = computed(() =>
timers.value.filter(t => t.status === 'active')
);
const activeTimersCount = computed(() => activeTimers.value.length);
const clearedTimersCount = computed(() =>
timers.value.filter(t => t.status === 'cleared').length
);
const totalTimersCount = computed(() => timers.value.length);
// 添加定时器
const addTimer = () => {
if (timerDuration.value < 100) {
alert('请输入有效的持续时间(至少100毫秒)');
return;
}
const timerId = timerIdCounter.value++;
let timerRef;
// 创建定时器对象
const timerObj = {
id: timerId,
type: timerType.value,
duration: timerDuration.value,
message: timerMessage.value,
status: 'active',
startTime: Date.now(),
progress: 0
};
// 根据类型设置定时器
if (timerType.value === 'timeout') {
timerRef = setTimeout(() => {
handleTimerCompletion(timerId);
console.log(`定时器 #${timerId}: ${timerMessage.value}`);
}, timerDuration.value);
timerObj.ref = timerRef;
} else {
timerRef = setInterval(() => {
console.log(`定时器 #${timerId}: ${timerMessage.value}`);
}, timerDuration.value);
timerObj.ref = timerRef;
}
timers.value.push(timerObj);
updateProgressBars();
};
// 处理定时器完成
const handleTimerCompletion = (timerId) => {
const timer = timers.value.find(t => t.id === timerId);
if (timer) {
timer.status = 'completed';
}
};
// 清理单个定时器
const clearTimer = (timerId) => {
const timer = timers.value.find(t => t.id === timerId);
if (timer && timer.status === 'active') {
if (timer.type === 'timeout') {
clearTimeout(timer.ref);
} else {
clearInterval(timer.ref);
}
timer.status = 'cleared';
console.log(`定时器 #${timerId} 已清理`);
}
};
// 清理所有定时器
const clearAllTimers = () => {
if (timers.value.length === 0) {
alert('没有活动定时器可清理');
return;
}
if (confirm(`确定要清理所有 ${timers.value.length} 个定时器吗?`)) {
timers.value.forEach(timer => {
if (timer.status === 'active') {
if (timer.type === 'timeout') {
clearTimeout(timer.ref);
} else {
clearInterval(timer.ref);
}
timer.status = 'cleared';
}
});
console.log('所有定时器已清理');
}
};
// 更新进度条
const updateProgressBars = () => {
const activeTimersList = timers.value.filter(t => t.status === 'active');
activeTimersList.forEach(timer => {
const elapsed = Date.now() - timer.startTime;
const progress = Math.min(100, (elapsed / timer.duration) * 100);
timer.progress = progress;
// 如果是interval类型,进度条会循环
if (timer.type === 'interval' && progress >= 100) {
timer.startTime = Date.now();
}
});
};
// 切换子组件显示
const toggleComponent = () => {
showChildComponent.value = !showChildComponent.value;
};
// 设置一个定时器来更新进度条
onMounted(() => {
setInterval(updateProgressBars, 100);
});
return {
timers,
timerType,
timerDuration,
timerMessage,
showChildComponent,
activeTimers,
activeTimersCount,
clearedTimersCount,
totalTimersCount,
addTimer,
clearTimer,
clearAllTimers,
toggleComponent
};
}
});
app.mount('#app');
</script>
</body>
</html>