优化:使用requestAnimationFrame

129 阅读2分钟

在requestAnimationFrame之前,使用js实现一个动画一般是用定时器来实现:


    <div id="box"></div>
    
      const box = document.getElementById('box')
      const distance = 500 // 目标移动距离
      const duration = 2000 // 运动持续时间
      const fps = 60 // 每秒60帧
      const interval = 1000 / fps // 每帧的时间间隔
      const step = distance / (duration / interval) // 每帧移动的距离
      let currentPosition = 0
      let inter = null

      function boxAnimate() {
        currentPosition += step
        box.style.transform = `translateX(${currentPosition}px)`
        console.log('currentPosition', currentPosition)
        // 停止动画
        if (currentPosition >= distance && inter) {
          clearInterval(inter)
          inter = null
        }
      }
      
      startBtn.addEventListener('click', function () {
        inter = setInterval(boxAnimate, interval)
      })

使用requestAnimationFrame实现:


      
    <div id="box1"></div>
      
      const box1 = document.getElementById('box1')
      let start = null

      function box1Animate(timestamp) {
        console.log('timestamp', timestamp)
        if (!start) start = timestamp
        const progress = timestamp - start

        // 计算新的位置
        box1.style.transform = `translateX(${Math.min(
          (progress / duration) * distance,
          distance
        )}px)`

        // 如果动画未完成,请求下一帧
        if (progress < duration) {
          requestAnimationFrame(box1Animate)
        }
      }

      startBtn.addEventListener('click', function () {
        // 启动动画
        requestAnimationFrame(box1Animate)
      })

动画.gif

乍一看好像也没啥区别啊,但这2种实现方式有本质区别:

  1. 定时器不能保证与刷新率同步(计时器准确性问题),可能会导致丢帧,造成页面卡顿;requestAnimationFrame是基于浏览器的重绘周期来执行动画,每次屏幕刷新时调用一次,一般情况下,显示屏的刷新率为60hz(每秒60帧),差不多每隔16.7ms调用一次,当显示器的刷新率变化时,requestAnimationFrame可以自动匹配最新的刷新率,非常强大
  2. 当页面隐藏时(例如切到其他标签页),定时器还会继续执行,requestAnimationFrame会自动暂停

因此,requestAnimationFrame有很高的流畅度,它在css3动画实现不了的时候,具有很高的便利性,可以更好地结合canvas或webGL中

完整代码:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>移动动画</title>
    <style>
      #box {
        width: 50px;
        height: 50px;
        background-color: lightblue;
      }
      #box1 {
        width: 50px;
        height: 50px;
        background-color: gold;
      }
    </style>
  </head>
  <body>
    <button id="startBtn">开始</button>
    <button id="resetBtn">重置</button>
    <div id="box"></div>
    <div id="box1"></div>

    <script>
      const box = document.getElementById('box')
      const distance = 500 // 目标移动距离
      const duration = 2000 // 运动持续时间
      const fps = 60 // 每秒60帧
      const interval = 1000 / fps // 每帧的时间间隔
      const step = distance / (duration / interval) // 每帧移动的距离
      let currentPosition = 0
      let inter = null
      let animationId = null

      function boxAnimate() {
        currentPosition += step
        box.style.transform = `translateX(${currentPosition}px)`
        console.log('currentPosition', currentPosition)
        // 停止动画
        if (currentPosition >= distance && inter) {
          clearInterval(inter)
          inter = null
        }
      }

      const box1 = document.getElementById('box1')
      let start = null

      function box1Animate(timestamp) {
        console.log('timestamp', timestamp)
        if (!start) start = timestamp
        const progress = timestamp - start

        // 计算新的位置
        box1.style.transform = `translateX(${Math.min(
          (progress / duration) * distance,
          distance
        )}px)`

        // 如果动画未完成,请求下一帧
        if (progress < duration) {
          animationId = requestAnimationFrame(box1Animate)
        } else {
          if (animationId) {
            cancelAnimationFrame(animationId)
          }
        }
      }

      startBtn.addEventListener('click', function () {
        inter = setInterval(boxAnimate, interval)
        // 启动动画
        animationId = requestAnimationFrame(box1Animate)
      })

      resetBtn.addEventListener('click', function () {
        window.location.reload()
      })
    </script>
  </body>
</html>