JavaScript爆炸动画无法停止执行的代码问题排查
代码存在的核心问题
- 嵌套
setInterval未做清理,造成定时器无限叠加:外层setInterval每2秒执行一次,每次执行都会新建一个内层setInterval,没有任何终止逻辑,随着运行时间增长,爆炸回调会被触发成百上千次,动画自然无限重复无法停止。且整个气球变大→爆炸的流程是单次线性逻辑,根本不需要循环触发的定时器。 - 爆炸坐标取值错误:传入
explode函数的是气球DOM元素,代码直接读取元素的clientX/clientY属性,这两个属性只有鼠标事件对象才携带,普通DOM元素不存在这两个值,会导致爆炸画布定位到页面左上角。 - 渲染循环无终止逻辑:粒子动画的
requestAnimationFrame递归没有停止条件,哪怕所有粒子完全消失,循环也会一直运行占用性能;且每次爆炸生成的canvas元素从不清理,会在页面内堆积大量无用DOM节点。 - 气球状态逻辑矛盾:CSS里给气球加了无限循环的上下漂浮动画,JS触发爆炸把气球设为隐藏后,没有重置气球状态也没有停止CSS动画,后续定时器重复触发时会操作已经隐藏的气球元素,逻辑完全混乱。
修复方法
- 把所有循环触发的
setInterval替换为单次触发的setTimeout,匹配“等待2秒开始变大→等待3秒变大动画完成→触发爆炸→流程结束”的单次线性逻辑,从根源避免定时器无限循环。 - 调用爆炸函数前,通过
getBoundingClientRect()获取气球在视口中的实际位置,计算中心点坐标作为爆炸原点,修正爆炸位置偏移问题。 - 给粒子渲染循环加终止判断:当所有粒子的透明度、半径都衰减到0以下时,移除对应的canvas元素,终止
requestAnimationFrame循环,避免性能浪费和DOM堆积。 - 爆炸触发后移除气球元素,避免后续逻辑重复操作已销毁的节点。
修复后完整代码
JS部分
function grow() { const balloon = document.querySelector('.balloon'); if (!balloon) return; // 2秒后开始执行变大动画 setTimeout(() => { balloon.style.width = '200px'; balloon.style.height = '200px'; // 等3秒变大动画完成后触发爆炸 setTimeout(() => { balloon.style.display = 'none'; explode(balloon); // 爆炸后移除气球节点,避免重复操作 balloon.remove(); }, 3000); }, 2000); }; grow(); function explode(balloonEl) { // 获取气球实际位置,计算爆炸中心点 const rect = balloonEl.getBoundingClientRect(); const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; const c = document.createElement('canvas') const ctx = c.getContext('2d') const ratio = window.devicePixelRatio const particles = [] let animationId = null; document.body.appendChild(c) c.style.position = 'absolute' c.style.left = (x - 100) + 'px' c.style.top = (y - 100) + 'px' c.style.pointerEvents = 'none' c.style.width = 200 + 'px' c.style.height = 200 + 'px' c.width = 200 * ratio c.height = 200 * ratio function Particle() { return { x: c.width / 2, y: c.height / 2, radius: r(20, 30), color: 'rgb(' + [r(0, 255), r(0, 255), r(0, 255)].join(',') + ')', rotation: r(0, 360, true), speed: r(8, 12), friction: 0.9, opacity: r(0, 0.5, true), yVel: 0, gravity: 0.1 } } for (var i = 0; ++i < 25;) { particles.push(Particle()) } function render() { ctx.clearRect(0, 0, c.width, c.height) let allDead = true; particles.forEach(function(p) { angleTools.moveOnAngle(p, p.speed) p.opacity -= 0.01 p.speed *= p.friction p.radius *= p.friction p.yVel += p.gravity p.y += p.yVel if (p.opacity < 0 || p.radius < 0) return // 存在还在显示的粒子 allDead = false; ctx.beginPath() ctx.globalAlpha = p.opacity ctx.fillStyle = p.color ctx.arc(p.x, p.y, p.radius, 0, 2 * Math.PI, false) ctx.fill() }) // 所有粒子消失,终止循环,清理canvas if (allDead) { cancelAnimationFrame(animationId); c.remove(); return; } animationId = requestAnimationFrame(render) } render(); } const angleTools = { getAngle: function(t, n) { var a = n.x - t.x, e = n.y - t.y; return Math.atan2(e, a) / Math.PI * 180 }, getDistance: function(t, n) { var a = t.x - n.x, e = t.y - n.y; return Math.sqrt(a * a + e * e) }, moveOnAngle: function(t, n) { var a = this.getOneFrameDistance(t, n); t.x += a.x, t.y += a.y }, getOneFrameDistance: function(t, n) { return { x: n * Math.cos(t.rotation * Math.PI / 180), y: n * Math.sin(t.rotation * Math.PI / 180) } } }; function r(a, b, c) { return parseFloat((Math.random() * ((a ? a : 1) - (b ? b : 0)) + (b ? b : 0)).toFixed(c ? c : 0)); }
CSS部分
body { margin: 20px; background: hsl(70, 31%, 85%); } .balloon { display: inline-block; width: 103px; height: 125px; background: hsl(215, 50%, 65%); border-radius: 80%; position: relative; box-shadow: inset -10px -10px 0 rgba(0, 0, 0, 0.07); margin: 20px 30px; transition: transform 0.5s ease, width 3s, height 3s; z-index: 10; animation: balloons 4s ease-in-out infinite; transform-origin: bottom center; } @keyframes balloons { 0%, 100% { transform: translateY(0) rotate(-4deg); } 50% { transform: translateY(-25px) rotate(4deg); } } .balloon:before { content: "▲"; font-size: 20px; color: hsl(215, 30%, 50%); display: block; text-align: center; width: 100%; position: absolute; bottom: -12px; z-index: -100; }
注:原CSS中写了原生CSS不支持的嵌套规则和未被使用的爆炸粒子样式,已做移除,同时给气球的transition属性补上宽高配置,保证变大动画平滑。
HTML部分
<div class="balloon"></div>
内容的提问来源于stack exchange,提问作者Clinton Njiru
相关产品推荐
相关产品推荐

