requestAnimationFrame API

6dianbiqi Lv2

requestAnimationFrame API

在 Web 应用中,实现动画效果的方法有:

  • CSS3:Transition(过度) / Animation(动画)
  • HTML5:Canvas
  • JavaScript:setInterval(定时器) / requestAnimationFrame(请求动画帧) / jQuery

这篇文章主要介绍 requestAnimationFrame 请求动画帧,并尝试它的原理以及优势。

0. 了解相关概念

0.1 动画帧请求回调函数列表

每个 Document 都有一个动画帧请求回调函数列表,该列表可以看成是由一对 <handlerId, callback> 组成的集合。其中 handlerId 是一个整数,唯一地标识了数据在列表中的位置;callback 是回调函数。

0.2 屏幕刷新频率

屏幕刷新频率,即屏幕刷新的速度,通常由赫兹(Hz)表示,例如 60Hz 表示屏幕每秒刷新 60 次,也就是每 16.7ms 刷新一次。

0.3 动画如何实现的?

首先,屏幕上的图像以每秒60次的频率刷新,由于刷新频率很高,因此用户感觉不到它在刷新。而 动画本质就是要让人眼看到图像被刷新而引起变化的视觉效果,这个变化要以连贯的、平滑的方式进行过渡。 那怎么样才能做到这种效果呢?

刷新频率为 60Hz 的屏幕每 16.7ms 刷新一次,那么我们在屏幕每次刷新前,将图像的位置向左移动一个像素,即 1px。这样一来,屏幕每次刷出来的图像位置都比前一个要差 1px,这样用户就会看到图像在移动。而由于人眼的视觉停留效应,当前位置的图像停留在大脑的印象还没消失,紧接着图像又被移到了下一个位置,因此才会看到图像在流畅的移动,这就是视觉效果上形成的动画。

0.4 浏览器如何标识页面是否处于可见状态?

当页面被最小化或者被切换成后台标签页时,页面为不可见,浏览器会触发一个 visibilitychange 事件,并设置 document.hidden 属性为 true;切换到显示状态时,页面为可见,也同样触发一个 visibilitychange 事件,设置 document.hidden 属性为 false

1. 为什么需要 requestAnimationFrame

我们知道,有些 CSS 无法实现的动画,我们可以尝试用 js setInterval 来实现,大概是下面这样:

1
2
3
4
5
6
7
(function() {
function updateAnimations() {
doAnimation1();
doAnimation2();
}
setInterval(updateAnimations, 100);
})();

该代码实现的功能是每隔100毫秒执行函数操作来达到动画效果,然而,使用计时器真的可靠吗?

在实际开发中,利用 setInterval 实现的动画在某些低端机上会出现卡顿、抖动的现象。 这种现象的产生有两个原因:

  • setInterval 的执行时间并不是确定的。在 Javascript 中, setInterval 任务(宏任务)被放进了异步队列中,只有当主线程上的任务执行完以后,才会去检查该队列里的任务是否需要开始执行,因此 setInterval 的实际执行时间一般要比其设定的时间晚一些。
  • 刷新频率受屏幕分辨率和屏幕尺寸的影响,因此不同设备的屏幕刷新频率可能会不同,而 setInterval 只能设置一个固定的时间间隔,这个时间不一定和屏幕的刷新时间相同。

以上两种情况都会导致 setInterval 的执行步调和屏幕的刷新步调不一致,从而引起【丢帧】现象。

那为什么步调不一致就会引起丢帧呢?

首先,我们使用 setInterval 实现动画时,我们只是在内存中对图像的属性进行改变,而这个变化必须要等到屏幕下次刷新时才会被更新到屏幕上。那么如果改变图像属性和更新图像这两个操作的步调不一致,就可能会导致中间某一次的操作被跨越过去,而直接更新下一次改变后的图像。

举个例子,假设屏幕每隔 16.7ms 刷新一次,而 setInterval 每隔10ms设置图像向左移动1px, 就会出现如下绘制过程:

  • 第 0 ms : 屏幕未刷新,等待中,setInterval 也未执行,等待中;
  • 第 10 ms: 屏幕未刷新,等待中,setInterval 开始执行并设置图像属性 left=1px;
  • 第 16.7 ms: 屏幕刷新,屏幕上的图像向左移动了 1px,setInterval 未执行,继续等待中;
  • 第 20 ms: 屏幕未刷新,等待中,setInterval 开始执行并设置 left = 2px;
  • 第 30 ms: 屏幕未刷新,等待中,setInterval 开始执行并设置 left = 3px;
  • 第 33.4 ms: 屏幕刷新,屏幕上的图像向左移动了 3px,setInterval 未执行,继续等待中;

可以看到,屏幕没有更新 left = 2px 那一帧的画面,图像直接从 1px 的位置跳到了 3px 的位置,这就是丢帧现象,用户看到的动画就卡顿了。

所以,requestAnimationFrame 的出现就是为了解决这个问题。与 setInterval 相比,requestAnimationFrame 最大的优势是由系统来决定回调函数的执行时机。具体一点讲,如果屏幕刷新率是60Hz,那么回调函数就每16.7ms 被执行一次,如果刷新率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms,换句话说就是,requestAnimationFrame 的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。

2. 使用 requestAnimationFrame

2.1 语法

1
2
3
4
5
// 请求动画帧
const handlerId = window.requestAnimationFrame(callback);

// 取消动画函数
window.cancelAnimationFrame(handlerId);

2.2 实例

下面是一个简单的用 requestAnimationFrame 实现的动画:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>requestAnimationFrame 动画示例</title>
<style>
#box {
width: 50px;
height: 50px;
background-color: red;
position: absolute;
top: 0;
left: 0;
}
</style>
</head>
<body>
<div id="box"></div>
<script>
const box = document.getElementById('box');
let position = 0;
const speed = 2; // 每帧移动的像素数
let flag = true;

function animate() {
position += speed * (flag ? 1 : -1);
box.style.transform = `translateX(${position}px)`;

// 当方块移动到屏幕右边缘时,重置位置
if (position > window.innerWidth - 52) {
flag = false;
} else if (position < 0) {
flag = true;
}

requestAnimationFrame(animate);
}

// 启动动画
requestAnimationFrame(animate);
</script>
</body>
</html>

3. requestAnimationFrame 的优势

  • CPU 节能:使用 setInterval 实现的动画,当页面被隐藏或最小化时,setInterval 仍然在后台执行动画任务,由于此时页面处于不可见或不可用状态,刷新动画是没有意义的,完全是浪费 CPU 资源。而 requestAnimationFrame 则完全不同,当页面处于未激活的状态下,该页面的屏幕刷新任务也会被系统暂停,因此跟着系统步伐走的 requestAnimationFrame 也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了 CPU 开销。

  • 函数节流:在高频率事件( resize / scroll 等 )中,为了防止在一个刷新间隔内发生多次函数执行,使用 requestAnimationFrame 可保证每个刷新间隔内,函数只被执行一次,这样既能保证流畅性,也能更好的节省函数执行的开销。一个刷新间隔内函数执行多次时没有意义的,因为显示器每 16.7ms 刷新一次,多次绘制并不会在屏幕上体现出来。

  • 标题: requestAnimationFrame API
  • 作者: 6dianbiqi
  • 创建于 : 2025-02-21 13:36:13
  • 更新于 : 2025-02-21 20:30:24
  • 链接: https://6dianbiqi.com/2025/02/21/requestAnimationFrame-API/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。