
本文详解如何使用 CSS 动画 + animationend 事件 + 动态类控制,精准实现10个圆圈以100ms间隔依次出现、缩放淡出,并自动无缝循环播放,彻底解决定时器失效与动画不触发问题。
本文详解如何使用 css 动画 + `animationend` 事件 + 动态类控制,精准实现10个圆圈以100ms间隔依次出现、缩放淡出,并自动无缝循环播放,彻底解决定时器失效与动画不触发问题。
要实现“10个圆圈在随机位置依次(每0.1秒一个)从中心生长并淡出,且全过程无限重复”,关键在于分离动画触发逻辑与样式定义,避免直接复用 animation 属性导致浏览器跳过重复动画(CSS 动画在相同属性连续应用时可能被忽略)。原代码中 setTimeout(circ, N * 100) 仅触发一次初始化,而 setTimeout(go, 1000) 重跑整个函数却未重置动画状态,导致后续无动画效果——这正是核心症结。
✅ 正确方案:用 animationend 驱动链式循环
我们采用「事件驱动 + 类名切换」策略:
- 将动画逻辑剥离至独立 CSS 类 .anim;
- 每次动画结束时监听 animationend,立即为下一个圆圈(或同一圆圈)重新添加 .anim 类;
- 利用 setTimeout 控制10个圆圈的初始启动时序(0ms, 100ms, …, 900ms);
- 所有圆圈共用同一套动画逻辑,天然支持无限循环。
? 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; /* 确保绝对定位子元素相对 body 布局 */
}
.circle {
position: absolute;
border-radius: 50%;
transform: translate(-50%, -50%) scale(0.1); /* 初始极小尺寸 */
/* 注意:此处不声明 animation,交由 .anim 类控制 */
}
.anim {
animation: scale 1s ease-out forwards; /* forwards 保持最终透明状态 */
}
@keyframes scale {
from {
opacity: 1;
transform: translate(-50%, -50%) scale(0.1);
}
to {
opacity: 0;
transform: translate(-50%, -50%) scale(3);
}
}⚠️ 关键点:.anim 类必须包含 forwards,确保动画结束后 opacity: 0 和 scale(3) 状态保留,避免视觉闪烁;同时 .circle 基础类只定义静态样式,动画完全由类增删触发。
? 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');
const RGB = `#${R}${G}${B}`;
// 移除旧动画类(重置状态)
circle.classList.remove('anim');
// 设置随机位置与样式
circle.style.left = `${X}px`;
circle.style.top = `${Y}px`;
circle.style.width = `${Size}px`;
circle.style.height = `${Size}px`;
circle.style.backgroundColor = RGB;
// 关键:异步添加动画类,确保浏览器重绘后触发新动画
setTimeout(() => circle.classList.add('anim'), 0);
};
// 为每个圆圈绑定动画结束事件,并设置初始延迟启动
circles.forEach((circle, i) => {
// 动画结束后,立即为当前圆圈重新初始化(实现循环)
circle.addEventListener('animationend', () => initCircle(circle, i));
// 按索引错开启动时间:0ms, 100ms, ..., 900ms
setTimeout(() => initCircle(circle, i), i * 100);
});
}
// 启动
go();
// 可选:响应窗口大小变化(如需适配 resize)
window.addEventListener('resize', () => {
// 若需动态更新 width/height,可在此调用 go() 或优化为局部重置
});✅ setTimeout(..., 0) 是核心技巧:它将 classList.add('anim') 推入任务队列末尾,确保 DOM 样式已应用完毕,浏览器能检测到“类变化”从而触发新动画。若同步添加,浏览器会因优化跳过重复动画。
? 注意事项与最佳实践
- 十六进制补零:toString(16) 可能返回单字符(如 "a"),需用 .padStart(2, '0') 补足两位,避免颜色异常(如 #f0f → #0f0f 错误);
- 性能优化:10个元素轻量,但若扩展至百级,建议改用 requestAnimationFrame 或 Canvas 渲染;
- 无障碍考虑:纯装饰性动画建议添加 prefers-reduced-motion 查询,对敏感用户降级;
- 调试技巧:临时移除 forwards 或添加 animation-play-state: paused 辅助观察关键帧。
此方案彻底规避了 setTimeout(go, 1000) 的粗粒度重载问题,以事件为纽带构建高精度、可维护、无限鲁棒的动画流水线——每一环都可控,每一次循环都可靠。

















