
本文详解如何通过分离动画类、利用 animationend 事件触发链式执行,配合 setTimeout 控制毫秒级时序,实现 10 个随机位置圆圈逐个绽放并无限循环的流畅动画效果。
本文详解如何通过分离动画类、利用 `animationend` 事件触发链式执行,配合 `settimeout` 控制毫秒级时序,实现 10 个随机位置圆圈逐个绽放并无限循环的流畅动画效果。
要实现“10 个圆圈在 1 秒内(每 100ms 一个)依次从屏幕随机位置中心绽放、放大并淡出,且整个过程无限循环”,关键在于避免动画复用失效和确保时序可控、可重入。原代码中直接在 forEach 中连续调用 circ() 导致所有圆圈同时启动;而将 setTimeout 放在外部 go() 调用中又无法维持单轮动画的连贯性——根本原因在于:CSS 动画一旦已应用且未重置状态,再次添加相同 animation 类不会重新触发。
✅ 正确解法:动画类动态切换 + animationend 驱动下一轮
核心思路是:
- 将动画逻辑完全交由 CSS 管理(性能更优、声明式清晰);
- 用独立的 .anim 类控制动画播放,每次播放前先移除该类,再异步添加,强制浏览器重新触发动画;
- 利用 animationend 事件监听单个圆圈动画结束,自动触发下一个圆圈(本例中用于无缝循环:最后一个圆圈动画结束后,立即重启第一轮);
- 使用 setTimeout(..., N * 100) 精确控制 10 个圆圈的启动偏移,保证 0.1 秒间隔。
以下是完整、可直接运行的实现:
✅ HTML(精简结构)
<!DOCTYPE html> <html> <head> <link rel="stylesheet" href="style.css"> </head> <body> <!-- 10 个占位圆圈 --> <span class="circle"></span> <span class="circle"></span> <span class="circle"></span> <span class="circle"></span> <span class="circle"></span> <span class="circle"></span> <span class="circle"></span> <span class="circle"></span> <span class="circle"></span> <span class="circle"></span> <script src="script.js"></script> </body> </html>
✅ CSS(分离静态样式与动画行为)
body {
background-color: #000;
width: 100vw;
height: 100vh;
margin: 0;
overflow: hidden; /* 防止滚动条干扰 */
position: relative; /* 确保绝对定位子元素以 viewport 为参考 */
}
.circle {
position: absolute;
border-radius: 50%;
transform: translate(-50%, -50%) scale(0.1); /* 初始极小尺寸 */
opacity: 1;
}
.anim {
animation: scale 1s ease-out forwards; /* forwards 保持最终状态(opacity: 0) */
}
@keyframes scale {
from {
transform: translate(-50%, -50%) scale(0.1);
opacity: 1;
}
to {
transform: translate(-50%, -50%) scale(3);
opacity: 0;
}
}? 注意:animation-fill-mode: forwards(简写为 forwards)至关重要——它确保动画结束后元素保留 to 状态(即 opacity: 0),避免闪烁或回弹。
立即学习“前端免费学习笔记(深入)”;
✅ JavaScript(精准时序 + 循环驱动)
function go() {
const circles = document.querySelectorAll('.circle');
const width = window.innerWidth;
const height = window.innerHeight;
// 单个圆圈初始化 & 播放函数
const initCircle = (circle, index) => {
// 随机位置、尺寸、颜色
const X = Math.floor(Math.random() * width);
const Y = Math.floor(Math.random() * height);
const Size = Math.floor((Math.random() * 6) + 5) * 10;
const R = Math.floor(Math.random() * 255).toString(16).padStart(2, '0');
const G = Math.floor(Math.random() * 255).toString(16).padStart(2, '0');
const B = Math.floor(Math.random() * 255).toString(16).padStart(2, '0');
circle.style.left = `${X}px`;
circle.style.top = `${Y}px`;
circle.style.width = `${Size}px`;
circle.style.height = `${Size}px`;
circle.style.backgroundColor = `#${R}${G}${B}`;
// 关键:移除动画类 → 强制重置 → 异步添加 → 触发新动画
circle.classList.remove('anim');
setTimeout(() => circle.classList.add('anim'), 0);
};
// 启动第一轮:10 个圆圈按 100ms 间隔依次启动
circles.forEach((circle, i) => {
// 监听自身动画结束,触发下一轮(即重启整个序列)
circle.addEventListener('animationend', () => {
if (i === circles.length - 1) { // 最后一个圆圈结束时,重启
go();
}
});
// 按索引错开启动时间:0ms, 100ms, 200ms...900ms
setTimeout(initCircle, i * 100, circle, i);
});
}
// 启动首轮
go();? 关键细节说明:
- setTimeout(..., 0) 是必要的“微任务延迟”,确保 DOM 样式更新完成后再触发动画,否则移除/添加 .anim 类可能被浏览器合并,导致动画不触发;
- padStart(2, '0') 修复了原代码中十六进制颜色值不足两位(如 a → 0a)导致的显示异常;
- animationend 仅绑定一次(在 go() 初始化时),避免重复监听;
- 循环逻辑简洁:仅当最后一个圆圈动画结束时调用 go(),自然形成无限序列,无需全局定时器,无累积延迟风险。
✅ 总结:为什么这样更健壮?
| 问题点 | 原方案缺陷 | 本方案优势 |
|---|---|---|
| 动画不重播 | 直接复用 .anim 类,浏览器跳过重复动画 | 动态 remove + add + setTimeout(0) 强制重触发 |
| 时序失控 | 外层 setTimeout(go, 1000) 导致整轮硬同步,无法衔接上一轮末尾 | 由 animationend 精确驱动下一轮起点,节奏稳定 |
| 颜色/尺寸异常 | toString(16) 可能生成 1, f 等单字符,拼接后颜色错误 | padStart(2, '0') 保证标准 6 位 HEX |
| 内存与性能 | 每次 go() 重复绑定事件 | 事件监听在初始化时一次性注册,无泄漏 |
此模式不仅适用于圆圈动画,更是实现任意序列化 CSS 动画循环(如加载指示器、粒子路径、卡片翻转流)的通用范式:状态归零 → 数据更新 → 异步触发动画 → 事件接力。

















