深入理解 requestAnimationFrame什么是 requestAnimationFrame ? reques
什么是 requestAnimationFrame
requestAnimationFrame
简称 rAF
是一个浏览器 API,它提供了一种更可预测的方式来接入浏览器渲染周期。
window.requestAnimationFrame()
方法会告诉浏览器你希望执行一个动画。它要求浏览器在下一次重绘之前,调用用户提供的回调函数。
对回调函数的调用频率通常与显示器的刷新率相匹配。虽然 75hz、120hz 和 144hz 也被广泛使用,但是最常见的刷新率还是 60hz(每秒 60 个周期/帧)。为了提高性能和电池寿命,大多数浏览器都会暂停在后台选项卡或者隐藏的 <iframe>
中运行的 requestAnimationFrame()
。
备注:若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用
requestAnimationFrame
。requestAnimationFrame
是一次性的。
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 取消回调函数。
下图是上面例子的执行结果:
完整的例子戳 codesandbox 。对比后明显能看出,requestAnimationFrame
会比 setTimeout
流畅了很多。
浏览器优化
requestAnimationFrame
对 CPU 非常友好,如果当前窗口或标签页不可见,动画就会停止。Chrome 会尝试通过限制输入事件的处理来缓解由于 rAF
回调占用主线程时间过长而导致的问题。
在 requestAnimationFrame
出现之前,即使用户将标签页切换到后台,setTimeout/setInterval
依然会持续触发,这可能导致不必要的计算和电池消耗。现代浏览器为了节约电量,对 setTimeout/setInterval
实施了限制,即使在标签页不可见的情况下,也限制它们最多每秒执行一次。
通过使用 requestAnimationFrame
,浏览器能够更有效地管理资源,确保动画在前台时平滑运行,而在后台时则暂停,这样不仅优化了性能,也提升了电池寿命。
执行时机
rAF
回调函数总是在下一个渲染帧中执行。在事件处理程序或通过 IntersectionObserver
和 ResizeObserver
等异步回调中排队的 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
回调?”这种情况可能会意外发生,尤其是在以下两种常见场景中:
- 在 rAF 回调结束时请求新的回调:如果您在每个
rAF
回调的末尾再次请求一个新的rAF
回调。 - 从输入处理程序请求 rAF 回调:如果您在事件处理程序(如鼠标移动或键盘输入)中请求
rAF
回调。
结果是每帧的工作量成倍增加,可能导致严重的性能问题。
管理 rAF 回调
开发者需要自行管理 rAF
回调的调度和合并,以避免在同一帧内触发多个相同的回调,从而导致性能问题。
帧中事件的生命周期
下面两个图可以帮我们理解帧中的事件生命周期:
帧生命周期(主进程版本)
浏览器单帧内事件调度(多进程版本)
至此 requestAnimationFrame
的回调时机就清楚了,它会在 style/layout/paint
之前调用。
时间轴示例
setTimeout/setInterval
如果你使用 setTimeout 或 setInterval 实现动画,这是理想的时间线:
你有一组绘制(绿色)和渲染(紫色)事件,并且你的 JavaScript 代码在黄色框中(顺便说一下,这些也是浏览器 DevTools 中用来表示时间线的颜色)
插图展示了理想的情况。每 60 ms 进行一次绘制和渲染,动画在每一帧中间发生,非常有规律。
如果你对动画功能使用了更高频率(间隔小于 1000/60 ms)的调用:
请注意,在每一次渲染发生之前,我们在每一帧中调用 4 个动画步骤,这会让动画感觉非常不连贯。
如果由于其他代码阻塞了事件循环,setTimeout
无法按时运行会怎么样?我们最终会错过一帧:
如果动画步骤比你预期的要多一点怎么办?
绘制和渲染事件也将被延迟。
requestAnimationFrame
requestAnimationFrame()
工作的时间轴示例如下
通过浏览器开发者工具可以进一步观察
所有动画代码都在绘制和渲染事件之前运行。这使得代码更可预测,并且有足够的时间来制作动画,而不必担心超出我们可用的 16ms 时间。
参考
Window:requestAnimationFrame() 方法 - Web API | MDN
The requestAnimationFrame() guide
转载自:https://juejin.cn/post/7418085392468426787