
本文详解如何通过分离动画类、利用 animationend 事件触发链式执行,配合 setTimeout 实现 10 个圆点依次随机出现、缩放淡出,并无限循环——彻底解决定时器重置导致动画失效的问题。
本文详解如何通过分离动画类、利用 `animationend` 事件触发链式执行,配合 `settimeout` 实现 10 个圆点依次随机出现、缩放淡出,并无限循环——彻底解决定时器重置导致动画失效的问题。
要实现「10 个圆点在 1 秒内以 0.1 秒间隔依次随机出现 → 各自从中心缩放放大并透明度归零 → 整个序列无缝循环」的效果,关键不在于堆叠 setTimeout 或强行重启函数,而在于解耦样式状态与动画触发时机。原始代码中直接在 forEach 中连续调用 circ() 并依赖 setTimeout(..., N * 100) 启动动画,会导致所有圆点共享同一轮动画状态;更严重的是,CSS 动画在 class 未变化时不会重播(即使 animation 属性值未变),因此后续循环中 .circle 元素无法再次触发动画。
✅ 正确解法的核心是三步策略:
- 分离动画控制类:将 animation 声明移至独立类(如 .anim),而非写死在基础 .circle 上;
- 动态增删动画类:每次展示前先移除 .anim,再通过 setTimeout 异步添加,强制浏览器重绘并触发新动画;
- 用 animationend 驱动下一轮:每个圆点动画结束时监听事件,递归调度自身下一次出现(形成闭环),避免全局 setTimeout(go, 1000) 破坏节奏。
以下是完整可运行实现:
<!DOCTYPE html>
<html>
<head>
<style>
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);
/* 初始状态无动画 */
}
.anim {
animation: scale 1s ease-out forwards;
}
@keyframes scale {
to {
transform: translate(-50%, -50%) scale(3);
opacity: 0;
}
}
</style>
</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>
function go() {
const circles = document.querySelectorAll('.circle');
const width = window.innerWidth;
const height = window.innerHeight;
const createCircleStyle = () => {
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');
return { left: `${X}px`, top: `${Y}px`, width: `${Size}px`, height: `${Size}px`, backgroundColor: `#${R}${G}${B}` };
};
const animateCircle = (circle) => {
const style = createCircleStyle();
// 清除动画类,重置状态
circle.classList.remove('anim');
Object.assign(circle.style, style);
// 异步添加动画类,确保样式已应用后再触发动画
setTimeout(() => circle.classList.add('anim'), 0);
};
// 为每个圆点绑定 animationend 事件,实现自我循环
circles.forEach((circle, index) => {
circle.addEventListener('animationend', () => animateCircle(circle));
// 初始序列:第 i 个圆点延迟 i × 100ms 启动
setTimeout(() => animateCircle(circle), index * 100);
});
}
// 首次启动
go();
// 可选:响应窗口大小变化(如旋转设备)
window.addEventListener('resize', () => {
// 重置所有圆点位置/尺寸需重新触发,此处仅做示意
document.querySelectorAll('.circle').forEach(c => c.classList.remove('anim'));
});
</script>
</body>
</html>? 关键注意事项:
立即学习“前端免费学习笔记(深入)”;
- setTimeout(() => circle.classList.add('anim'), 0) 不可省略为同步操作——CSS 动画需要 DOM 重排(reflow)后才能生效,异步延迟确保浏览器已应用新 style;
- 使用 .padStart(2, '0') 防止十六进制颜色值出现单字符(如 A → 0A),避免 #F00 类错误;
- animation: ... forwards 保证动画结束后保留 opacity: 0 状态,避免闪烁;
- 若需暂停/重启,可封装 go() 为返回清理函数的工厂模式,便于生命周期管理。
该方案完全规避了「全局定时器重跑整个函数」引发的状态冲突,每个圆点自治运行、彼此解耦,既满足精确时序要求,又具备高可维护性与扩展性——是 CSS 动画驱动复杂序列的推荐实践范式。

















