likes
comments
collection
share

深入理解 requestAnimationFrame什么是 requestAnimationFrame ? reques

作者站长头像
站长
· 阅读数 17

什么是 requestAnimationFrame

requestAnimationFrame 简称 rAF 是一个浏览器 API,它提供了一种更可预测的方式来接入浏览器渲染周期。

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

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

备注:若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用 requestAnimationFramerequestAnimationFrame 是一次性的。

setTimeout/setInterval 动画

过去,JavaScript 动画是使用 setTimeout() 或来执行的 setInterval()。你执行一小段动画,然后 setTimeout() 在几毫秒后再次调用此代码重复执行:

    let timer
    const performAnimation = () => {
      //...
      timer = setTimeout(performAnimation, 1000 / 60)
    }
    timer = setTimeout(performAnimation, 1000 / 60)

    // 通过获取超时或间隔参考并清除它来停止动画
    clearTimeout(timer)

或者

    const performAnimation = () => {
      //...
    }
    setInterval(performAnimation, 1000 / 60)

这种方法的问题在于,尽管我们精确设定了刷新率,浏览器仍可能因处理其他任务而延迟响应。这导致我们的 setTimeout() 调用错过了最佳时机进行界面重绘,进而被推迟到下一个可用的周期。

这很糟糕,因为我们丢失了一帧,而在下一帧中动画不得不补偿性地执行两次,这种突变会让用户明显感受到动画的卡顿。

requestAnimationFrame 动画

requestAnimationFrame 是执行动画的标准方式,尽管代码看起来与 setTimeout/setInterval 代码非常相似,但它的工作方式却非常不同:

    let request

    const performAnimation = () => {
      request = requestAnimationFrame(performAnimation)
      //animate something
    }

    requestAnimationFrame(performAnimation)

    //...

    cancelAnimationFrame(request) //stop the animation

先看下面这个例子,了解一下它是如何使用并运行的:

const test = document.querySelector<HTMLDivElement>("#test")!;

let i = 0;
let requestId: number;
function animation() {
  test.style.marginLeft = `${i}px`;

  requestId = requestAnimationFrame(animation);

  i++;

  if (i > 200) {
    cancelAnimationFrame(requestId);
  }
}

animation();

上面的代码 1s 大约执行 60 次,因为一般的屏幕硬件设备的刷新频率都是 60Hz,然后每执行一次大约是 16.6ms。使用 requestAnimationFrame 的时候,只需要反复调用它就可以实现动画效果。

同时 requestAnimationFrame 会返回一个请求 ID,是回调函数列表中的一个唯一值,可以使用 cancelAnimationFrame 通过传入该请求 ID 取消回调函数。

下图是上面例子的执行结果:

深入理解 requestAnimationFrame什么是 requestAnimationFrame ? reques

完整的例子戳 codesandbox  。对比后明显能看出,requestAnimationFrame 会比 setTimeout 流畅了很多。

浏览器优化

requestAnimationFrame 对 CPU 非常友好,如果当前窗口或标签页不可见,动画就会停止。Chrome 会尝试通过限制输入事件的处理来缓解由于 rAF 回调占用主线程时间过长而导致的问题。

requestAnimationFrame 出现之前,即使用户将标签页切换到后台,setTimeout/setInterval 依然会持续触发,这可能导致不必要的计算和电池消耗。现代浏览器为了节约电量,对 setTimeout/setInterval 实施了限制,即使在标签页不可见的情况下,也限制它们最多每秒执行一次。

通过使用 requestAnimationFrame,浏览器能够更有效地管理资源,确保动画在前台时平滑运行,而在后台时则暂停,这样不仅优化了性能,也提升了电池寿命。

执行时机

rAF 回调函数总是在下一个渲染帧中执行。在事件处理程序或通过 IntersectionObserverResizeObserver 等异步回调中排队的 rAF 调用,会被安排在下一个渲染帧中执行。这意味着所有在同一事件处理程序中排队的 rAF 回调都将在同一个渲染帧中按顺序依次执行。

同一事件处理程序中的多个 rAF 调用

如果在一个事件处理程序中有多个 rAF 回调排队,所有这些回调函数都会在同一个渲染帧中按顺序依次执行。这样可以确保这些操作在同一个重绘周期内完成,避免不必要的多次重绘。

// 事件处理程序中的 rAF 调用
document.getElementById('myButton').addEventListener('click', () => {
    requestAnimationFrame(() => {
        console.log('First rAF callback');
    });
    requestAnimationFrame(() => {
        console.log('Second rAF callback');
    });
});

在这个示例中,点击按钮后,两个 requestAnimationFrame 回调函数都会在下一个渲染帧中按顺序执行。

卡顿

假设在一个帧中有 5 个 rAF 回调排队,每个回调大约需要 100 毫秒。浏览器会尝试在目标帧中运行所有这些回调,即使这总共需要 500 毫秒。这种情况下,页面会出现明显的卡顿。

你可能会问,“为什么一帧内会有 5 个 rAF 回调?”这种情况可能会意外发生,尤其是在以下两种常见场景中:

  1. 在 rAF 回调结束时请求新的回调:如果您在每个 rAF 回调的末尾再次请求一个新的 rAF 回调。
  2. 从输入处理程序请求 rAF 回调:如果您在事件处理程序(如鼠标移动或键盘输入)中请求 rAF 回调。

结果是每帧的工作量成倍增加,可能导致严重的性能问题。

管理 rAF 回调

开发者需要自行管理 rAF 回调的调度和合并,以避免在同一帧内触发多个相同的回调,从而导致性能问题。

帧中事件的生命周期

下面两个图可以帮我们理解帧中的事件生命周期:

深入理解 requestAnimationFrame什么是 requestAnimationFrame ? reques 帧生命周期(主进程版本)

深入理解 requestAnimationFrame什么是 requestAnimationFrame ? reques 浏览器单帧内事件调度(多进程版本)

至此 requestAnimationFrame 的回调时机就清楚了,它会在 style/layout/paint 之前调用。

时间轴示例

setTimeout/setInterval

如果你使用 setTimeout 或 setInterval 实现动画,这是理想的时间线:

深入理解 requestAnimationFrame什么是 requestAnimationFrame ? reques

你有一组绘制(绿色)和渲染(紫色)事件,并且你的 JavaScript 代码在黄色框中(顺便说一下,这些也是浏览器 DevTools 中用来表示时间线的颜色)

深入理解 requestAnimationFrame什么是 requestAnimationFrame ? reques

插图展示了理想的情况。每 60 ms 进行一次绘制和渲染,动画在每一帧中间发生,非常有规律。

如果你对动画功能使用了更高频率(间隔小于 1000/60 ms)的调用:

深入理解 requestAnimationFrame什么是 requestAnimationFrame ? reques

请注意,在每一次渲染发生之前,我们在每一帧中调用 4 个动画步骤,这会让动画感觉非常不连贯。

如果由于其他代码阻塞了事件循环,setTimeout 无法按时运行会怎么样?我们最终会错过一帧:

深入理解 requestAnimationFrame什么是 requestAnimationFrame ? reques

如果动画步骤比你预期的要多一点怎么办?

深入理解 requestAnimationFrame什么是 requestAnimationFrame ? reques

绘制和渲染事件也将被延迟。

requestAnimationFrame

requestAnimationFrame() 工作的时间轴示例如下

深入理解 requestAnimationFrame什么是 requestAnimationFrame ? reques

通过浏览器开发者工具可以进一步观察

深入理解 requestAnimationFrame什么是 requestAnimationFrame ? reques

深入理解 requestAnimationFrame什么是 requestAnimationFrame ? reques

所有动画代码都在绘制和渲染事件之前运行。这使得代码更可预测,并且有足够的时间来制作动画,而不必担心超出我们可用的 16ms 时间。

参考

Window:requestAnimationFrame() 方法 - Web API | MDN

The requestAnimationFrame() guide

Chromium Docs - Life of a frame

requestAnimationFrame 执行机制探索 | 正经博客

转载自:https://juejin.cn/post/7418085392468426787
评论
请登录