You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaScript爆炸动画无法停止执行的代码问题排查

代码存在的核心问题
  • 嵌套setInterval未做清理,造成定时器无限叠加:外层setInterval每2秒执行一次,每次执行都会新建一个内层setInterval,没有任何终止逻辑,随着运行时间增长,爆炸回调会被触发成百上千次,动画自然无限重复无法停止。且整个气球变大→爆炸的流程是单次线性逻辑,根本不需要循环触发的定时器。
  • 爆炸坐标取值错误:传入explode函数的是气球DOM元素,代码直接读取元素的clientX/clientY属性,这两个属性只有鼠标事件对象才携带,普通DOM元素不存在这两个值,会导致爆炸画布定位到页面左上角。
  • 渲染循环无终止逻辑:粒子动画的requestAnimationFrame递归没有停止条件,哪怕所有粒子完全消失,循环也会一直运行占用性能;且每次爆炸生成的canvas元素从不清理,会在页面内堆积大量无用DOM节点。
  • 气球状态逻辑矛盾:CSS里给气球加了无限循环的上下漂浮动画,JS触发爆炸把气球设为隐藏后,没有重置气球状态也没有停止CSS动画,后续定时器重复触发时会操作已经隐藏的气球元素,逻辑完全混乱。
修复方法
  1. 把所有循环触发的setInterval替换为单次触发的setTimeout,匹配“等待2秒开始变大→等待3秒变大动画完成→触发爆炸→流程结束”的单次线性逻辑,从根源避免定时器无限循环。
  2. 调用爆炸函数前,通过getBoundingClientRect()获取气球在视口中的实际位置,计算中心点坐标作为爆炸原点,修正爆炸位置偏移问题。
  3. 给粒子渲染循环加终止判断:当所有粒子的透明度、半径都衰减到0以下时,移除对应的canvas元素,终止requestAnimationFrame循环,避免性能浪费和DOM堆积。
  4. 爆炸触发后移除气球元素,避免后续逻辑重复操作已销毁的节点。

修复后完整代码

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 09:15:42