一文读懂 requestAnimationFrame

1,103 阅读4分钟

深入理解 requestAnimationFrame API

1. 简介

  • 什么是 requestAnimationFrame
    • 简述:requestAnimationFrame 是一个用于更新动画的 API,通过告诉浏览器你想执行动画,并要求在下一次重绘之前调用指定的回调函数。
    • 背景介绍:最初网页动画使用 setTimeoutsetInterval 控制时间间隔,但这些方法容易产生卡顿、掉帧问题。
  • 它相对于传统的 setTimeoutsetInterval 的优势
    • 性能提升:requestAnimationFrame 自动根据屏幕的刷新率来调整帧率,避免了硬编码的时间间隔。
    • 浏览器优化:浏览器会在屏幕即将重绘之前自动调用回调,确保动画的最佳时机。
    • 流畅动画:提高动画流畅度,减少卡顿,尤其在高性能需求场景下如游戏、视频渲染。

2. 工作原理

  • 解释 requestAnimationFrame 的基本工作机制

    • 浏览器每秒会更新屏幕多次(通常是60次),requestAnimationFrame 通过在每次更新前调用注册的回调函数,确保动画与屏幕刷新同步。
    • 注册后的回调函数会被放入任务队列,等到下次重绘前执行。
    • 帧率同步:requestAnimationFrame 会根据设备的帧率自动调整调用频率(例如高刷新率显示器会每秒多次调用)。
  • 图解展示浏览器如何分发和执行任务

requestAnimationFrame.drawio.png

3. 使用场景

  • 浏览器动画的流畅渲染

    • requestAnimationFrame 适用于处理元素位移、缩放、旋转等动画,确保动画不会出现明显的卡顿或跳帧。
  • 游戏开发中的实时更新与优化

    • 实时刷新画面,保持游戏角色或场景的动态更新与用户操作的实时反馈。
  • 动画节奏控制

    • requestAnimationFrame 可配合 CSS 动画或其他 JavaScript 动画框架使用,用于控制动画的节奏和帧率。
  • 节能与性能优化

    • 如果标签页不可见,浏览器会自动暂停 requestAnimationFrame 调用,减少不必要的 CPU 和内存消耗,节省电量。

4. setTimeout/setInterval 的对比

  • 通过实例代码展示三者的区别

    • setTimeout:每隔固定时间执行,但不会考虑屏幕刷新,容易导致掉帧。

      <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>setTimeout ex</title>
        <style>
          #box {
            width: 50px;
            height: 50px;
            background-color: red;
            position: absolute;
            top: 100px;
          }
        </style>
      </head>
      <body>
        <div id="box"></div>
        <script>
          let box = document.getElementById('box');
          let position = 0;
          
          function moveBox() {
            position += 5;  // 每次移动5px
            box.style.transform = `translateX(${position}px)`;
      
            if (position < window.innerWidth - 50) {
              setTimeout(moveBox, 16);  // 每16ms执行一次,大约为60fps
            }
          }
      
          setTimeout(moveBox, 16);  // 初始调用
        </script>
      </body>
      </html>
      
      
    • setInterval:持续执行,但时间不精确,可能与屏幕刷新不同步,与setTimeout相同,也会容易导致掉帧。

    • requestAnimationFrame:基于帧率调用,能与屏幕刷新完全同步。

      <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>requestAnimationFrame ex</title>
        <style>
          #box {
            width: 50px;
            height: 50px;
            background-color: blue;
            position: absolute;
            top: 300px;
          }
        </style>
      </head>
      <body>
        <div id="box"></div>
        <script>
          let box = document.getElementById('box');
          let position = 0;
      	
      	// 非常的丝滑      
          function moveBox() {
            position += 2;  // 每次移动2px
            box.style.transform = `translateX(${position}px)`;
      
            if (position < window.innerWidth - 50) {
              requestAnimationFrame(moveBox);  // 基于屏幕刷新率调用
            }
          }
      
          requestAnimationFrame(moveBox);  // 初始调用
        </script>
      </body>
      </html>
      
      
  • 为什么 requestAnimationFrame 更适合动画?

    • 自动同步屏幕刷新,避免不必要的计算。
    • 更好的性能:减少 CPU 使用率,使动画更加平滑。
    • 动态适应不同设备(如高帧率显示器或移动设备)。

5. 使用示例

  • 基本使用方法

    • 如何通过 requestAnimationFrame 创建简单的动画循环:
      function animate() {
          // 动画逻辑
          console.log("动画帧");
          requestAnimationFrame(animate);
      }
      requestAnimationFrame(animate);
      
    • 示例:一个不断移动的方块,每帧更新方块的位置,请看上文代码。
  • 高级应用:取消与嵌套 requestAnimationFrame

    • 如何使用 cancelAnimationFrame
      • 在特定条件下结束动画循环:
        // 例如在达到一定位置的时候,停止动画
        if (position < window.innerWidth - 50) {
            requestAnimationFrame(moveBox);  // 基于屏幕刷新率调用
        }
        
    • 嵌套 requestAnimationFrame
      • 当多个动画需要协调执行时,可在一个动画结束后嵌套调用下一个动画。

      • 例如,实现连续或连贯的动画序列:一个方块移动到一边后,开始改变颜色。

        function moveBox() {
            position += 2;  // 每次移动2px
            box.style.transform = `translateX(${position}px)`;
        
            if (position < window.innerWidth - 50) {
                requestAnimationFrame(changeBgColor);  // 基于屏幕刷新率调用
            }
        }
        
        function changeBgColor() {
            const randomIndex = Math.floor(Math.random()*3);
            box.style.backgroundColor = ['red', 'green', 'blue'][randomIndex];
            requestAnimationFrame(moveBox);
        }
        
  • 结合实际开发中的优化策略

    • 如何在长时间动画中避免性能瓶颈,例如分帧渲染、减少 DOM 操作、避免强制布局、调节帧率等。

      • 分帧渲染:将任务分成小块,分散到多个帧中。
      • 减少 DOM 操作:批量操作 DOM,避免频繁修改。
      • 避免强制布局:避免在同一帧内同时读取和修改 DOM。
      • 调节帧率:根据需求降低帧率,减少调用频率。
      // 优化策略还有很多啊,小编这里采取了调节帧率的方式
      let lastTime = 0;
      const fps = 10;
      
      function moveBox() {
          position += 5;  // 每次移动5px
          box.style.transform = `translateX(${position}px)`;
      
          if (position < window.innerWidth - 50) {
              requestAnimationFrame(changeBgColor);  // 基于屏幕刷新率调用
          }
      }
      
      function changeBgColor(timestamp) {	
          // 当前时间 - 上一次记录时间 >= 一秒内渲染一次的间隔时间
          if (timestamp - lastTime >= 1000 / fps) {
              // 更新动画
              const randomIndex = Math.floor(Math.random()*3);
              box.style.backgroundColor = ['red', 'green', 'blue'][randomIndex];
              lastTime = timestamp;
          }
          requestAnimationFrame(moveBox);
      }
      

6. 性能优化与陷阱

  • 提高动画性能的技巧
    • 减少主线程阻塞:避免在 requestAnimationFrame 回调中执行重型操作,如大量 DOM 操作或复杂计算。
    • 避免重复计算和重绘:只在必要时更新元素,减少无效的重绘和重排。
  • 常见陷阱与误区
    • 过度使用:虽然 requestAnimationFrame 更高效,但过度嵌套或不恰当的使用仍可能导致页面性能下降。
    • 内存泄漏问题:如果忘记使用 cancelAnimationFrame 取消不必要的动画,可能会导致动画持续执行,造成内存泄漏。