Skip to content
 

理解 requestAnimationFrame:现代 Web 动画的基石

更新: 9/9/2025字数: 0 字 时长: 0 分钟


一、为什么我们需要 requestAnimationFrame?

在 Web 开发的早期,我们通常使用 setTimeoutsetInterval 来创建动画效果。然而,这些方法存在根本性的缺陷:它们与浏览器的渲染周期不同步,可能导致卡顿、丢帧和电池快速耗尽等问题。

requestAnimationFrame 的出现彻底改变了这一局面,它为 Web 动画提供了一个与浏览器渲染机制深度集成的解决方案。

什么是 requestAnimationFrame?

window.requestAnimationFrame() 方法会告诉浏览器你希望执行一个动画。它要求浏览器在下一次重绘之前,调用用户提供的回调函数。

对回调函数的调用频率通常与显示器的刷新率相匹配。虽然 75hz、120hz 和 144hz 也被广泛使用,但是最常见的刷新率还是 60hz(每秒 60 个周期/帧)。为了提高性能和电池寿命,大多数浏览器都会暂停在后台选项卡或者隐藏的 <iframe> 中运行的 requestAnimationFrame()。

基本语法

javascript
// 请求动画帧
const requestId = requestAnimationFrame(callback);

// 取消动画帧
cancelAnimationFrame(requestId);

二、工作原理:与浏览器渲染周期同步

2.1 浏览器渲染流水线

要理解 requestAnimationFrame 的强大之处,我们需要先了解浏览器的渲染过程:

1. JavaScript执行 → 2. 样式计算 → 3. 布局 → 4. 绘制 → 5. 合成

requestAnimationFrame 的回调函数在第一步执行,确保所有的动画更新都在同一渲染周期内完成。

2.2 与 setTimeout 实际案例对比

先说一下特性:

特性requestAnimationFramesetTimeout/setInterval
执行时机浏览器下一次重绘前固定时间间隔
性能高效,自动暂停(标签页隐藏时)持续执行,浪费资源
帧率与屏幕刷新率同步(通常 60fps)固定时间间隔
兼容性现代浏览器所有浏览器

案例代码:
下面案例展示 requestAnimationFrame 和 setTimeout/setInterval 的区别:

html
<!DOCTYPE html>
<html>
  <head>
    <style>
      .box {
        width: 50px;
        height: 50px;
        background: red;
        position: relative;
      }
      .container {
        height: 100px;
        border: 1px solid #ccc;
        margin: 20px 0;
      }
    </style>
  </head>
  <body>
    <h2>requestAnimationFrame vs setTimeout 对比</h2>

    <div class="container">
      <div class="box" id="rafBox"></div>
      <p>requestAnimationFrame (流畅)</p>
    </div>

    <div class="container">
      <div class="box" id="timeoutBox"></div>
      <p>setTimeout (可能卡顿)</p>
    </div>

    <script>
      // requestAnimationFrame 版本
      function animateWithRAF() {
        const box = document.getElementById("rafBox");
        let position = 0;
        let lastTime = 0;

        function step(timestamp) {
          if (!lastTime) lastTime = timestamp;

          // 基于时间差计算移动距离,保证速度一致
          const elapsed = timestamp - lastTime;
          position += (elapsed / 16) * 2; // 60fps基准

          if (position > 300) position = 0;

          box.style.left = position + "px";
          lastTime = timestamp;

          requestAnimationFrame(step);
        }

        requestAnimationFrame(step);
      }

      // setTimeout 版本
      function animateWithTimeout() {
        const box = document.getElementById("timeoutBox");
        let position = 0;

        function step() {
          position += 2;
          if (position > 300) position = 0;

          box.style.left = position + "px";

          setTimeout(step, 16); // 模拟60fps
        }

        setTimeout(step, 16);
      }

      animateWithRAF();
      animateWithTimeout();
    </script>
  </body>
</html>

效果如下:

requestAnimationFrame 对比

可以看出 requestAnimationFrame 对比 setTimeout 更流畅,因为 requestAnimationFrame 是在浏览器重绘前执行,而 setTimeout 是在固定时间间隔后执行。

三、为什么 requestAnimationFrame 是异步宏任务?

虽然 JavaScript 是单线程的,但浏览器使用事件循环机制来处理异步操作。在这个模型中:

  • 同步任务:立即执行,阻塞后续代码
  • 微任务:在当前任务结束后立即执行(如 Promise)
  • 宏任务:在下次事件循环时执行(如 setTimeout)
  • requestAnimationFrame:特殊的宏任务,在渲染前执行

执行顺序示例

javascript
console.log("1. 同步开始");

Promise.resolve().then(() => console.log("3. 微任务"));
setTimeout(() => console.log("5. 宏任务"), 0);
requestAnimationFrame(() => console.log("4. rAF"));

console.log("2. 同步结束");

// 输出顺序:
// 1. 同步开始
// 2. 同步结束
// 3. 微任务
// 4. rAF (渲染前执行)
// 5. 宏任务 (下次事件循环)

四、实际案例:进度条动画

下面是一个完整的进度条动画示例,展示了 requestAnimationFrame 的实际应用:

html
<!DOCTYPE html>
<html>
  <head>
    <style>
      .progress-container {
        width: 100%;
        height: 20px;
        background-color: #f0f0f0;
        border-radius: 10px;
        overflow: hidden;
      }

      .progress-bar {
        height: 100%;
        width: 0%;
        background-color: #4caf50;
        border-radius: 10px;
        transition: none; /* 禁用CSS过渡 */
      }

      .controls {
        margin: 20px 0;
      }
    </style>
  </head>
  <body>
    <div class="progress-container">
      <div class="progress-bar" id="progressBar"></div>
    </div>

    <div class="controls">
      <button onclick="startProgress()">开始进度</button>
      <button onclick="resetProgress()">重置</button>
    </div>

    <script>
      const progressBar = document.getElementById("progressBar");
      let animationId;

      function startProgress() {
        cancelAnimationFrame(animationId);

        const startTime = performance.now();
        const duration = 2000; // 2秒完成

        function animate(timestamp) {
          const elapsed = timestamp - startTime;
          const progress = Math.min(elapsed / duration, 1);

          // 更新进度条
          progressBar.style.width = progress * 100 + "%";

          if (progress < 1) {
            animationId = requestAnimationFrame(animate);
          }
        }

        animationId = requestAnimationFrame(animate);
      }

      function resetProgress() {
        cancelAnimationFrame(animationId);
        progressBar.style.width = "0%";
      }
    </script>
  </body>
</html>

效果如下:

进度条动画

requestAnimationFrame 进度条动画

我见青山多妩媚,料青山见我应如是。