keguigong

如何取消requestAnimationFrame发起的动画

  by  keguigong

查看最终实现的效果:Bouncing Balls Start

最近在学习 2D canvas 有关知识,并参照 MDN 文档(Let's bounce some balls)使用 canvas 编写一个碰撞小球程序,在将该程序结合 React Hook 进行开发的时候遇到了一些问题,这儿将一些遇到的问题以及对应的解决方案记录一下。

cancelAnimationFrame 无法取消动画

原本的让小球动起来的部分如下,函数执行完 return 动画的 ID 并请求动画执行,继而实现动画的连续运行。通过获取到的 ID 用于后续的取消动画。

// ball.js
function loop(...) {
  ...
  return start = () => {
    ctx.fillStyle = "rgba(0, 0, 0, 0.25)"
    ctx.fillRect(0, 0, width, height)
 
    for (let i = 0; i < balls.length; i++) {
      balls[i].draw(ctx)
      balls[i].update(width, height)
      balls[i].collisionDetect()
    }
    return requestAnimationFrame(start)
  }
}
// bouncing-balls.tsx
useEffect(() => {
  ...
  // 开始动画,并开始自动进行,将第一次的ID保存下来
  const id = loop(...)()
 
  return () => {
    ...
    // 页面销毁,取消动画
    cancelAnimationFrame(id)
  }
})

这样操作动画并不能被取消,原因在于每一次 requestAnimationFrame() 返回的值都是不一样的,仅仅保存了第一次的 id,肯定无法通过这个 id 去取消后面正在进行的动画。正确做法是需要时刻更新 id,确保在取消的时候 id 是正在进行的动画。

// 从start函数中移除
return requestAnimationFrame(start)
useEffect(() => {
  ...
  let id = 0
  // 封装一个新的startFn,便于使用id保存最新的动画
  const startFn = () => {
    loop(...)()
    id = requestAnimationFrame(startFn)
  }
  id = requestAnimationFrame(startFn)
 
  return () => {
    ...
    cancelAnimationFrame(id)
  }
})

经过上述改造,在离开页面的时候运动会被正常的取消,避免资源未回收。

多次进入页面小球运动速度加快

原因是由于上次的 requestAnimationFrame 并没有被取消,页面离开之后画布以及创建的小球并未被销毁,再次进入页面的时候再次调用了 requestAnimationFrame,使得 update() 函数在同一个 frame 中被执行了两次,该函数主要用于移动小球,所以产生了小球移动速度加快的情况,正常取消动画问题得以解决。

Ball.prototype.update = function (...) {
  ...
  // 通过每帧往位置上添加速度值,实现移动,加两次则移动速度加快
  this.x += this.velX
  this.y += this.velY
}

useEffect 中动画函数频繁被创建

创建小球比较耗费资源,如果在 useEffect 中直接创建,Next.js 会提示可以通过 useCallback 进行优化。同样的,我们进行改造。

const memoLoop = useCallback(canvasCtx && width && height ? loop(canvasCtx, width, height) : () => {}, [
  canvasCtx,
  width,
  height
])

memoLoop 用来保存 loop 函数返回的 start 函数,并且只有在 canvasCtxcanvasCtx 以及 canvasCtx 变化的时候才会重新调用 loop 函数。同样的,对于 useEffect 同样进行改造。

useEffect(() => {
  ...
  let animationId = 0
  const loop = () => {
    memoLoop()
    animationId = requestAnimationFrame(loop)
  }
  animationId = requestAnimationFrame(loop)
  ...
}, [canvasCtx, width, height, memoLoop])

注意在 useEffect 中使用了 memoLoop,记得将其添加进数组中。

完整的代码在请看 这里