我们来彻底讲明白 requestAnimationFrame (通常简称为 rAF)。
要真正理解它,我们不能只看它是什么,更要明白它为什么会出现,它解决了什么根本性的问题。
一、requestAnimationFrame 的诞生背景:旧时代的困境
在 rAF 出现之前,要在网页上实现一个连续的动画效果,开发者通常依赖 setTimeout 或 setInterval。比如,想让一个元素每秒移动60次,你可能会这样写:
JavaScript
// 旧方法:使用 setInterval
let position = 0;
const element = document.getElementById('my-box');
setInterval(() => {
position += 1; // 每次移动 1px
element.style.transform = `translateX(${position}px)`;
}, 16); // 尝试模拟 60fps (1000ms / 60 ≈ 16.67ms)
这个看似可行的方法,背后却隐藏着几个致命的缺陷:
-
时机不精确:
setTimeout和setInterval只是将你的回调函数放入一个宏任务队列中,并不能保证在精确的16ms后执行。如果主线程正在忙于处理其他任务(比如复杂的计算或DOM操作),你的动画回调就会被延迟,导致掉帧和卡顿。 -
与渲染不同步:这是最核心的问题。显示器的刷新是固定频率的(比如 60Hz 或 144Hz),而
setTimeout的执行时机与这个刷新周期完全无关。这会导致:-
过度绘制:在一帧的时间内(例如16.7ms),你的
setInterval可能执行了两次,但屏幕只刷新了一次。第二次的计算和DOM修改就完全浪费了。 -
丢帧抖动:你的代码可能在屏幕刚刚完成一帧绘制后才执行,那么这次更新就要等到下一帧才能显示出来,视觉上就会感觉慢了一拍,产生抖动(Jank)。
-
-
资源浪费:即使用户切换到了其他浏览器标签页,导致你的动画页面完全不可见,
setInterval默认情况下仍然在后台不知疲倦地运行,持续消耗CPU和电池资源。
浏览器开发者们看到了这些问题,他们需要一个 与浏览器渲染节奏同步、并且足够智能 的 API。于是,requestAnimationFrame 应运而生。
二、requestAnimationFrame 是什么?从第一性原理理解
requestAnimationFrame 的本质不是一个传统的定时器,而是一个 “请求”。
你调用它,就等于向浏览器发出一个请求:“浏览器,请在你下一次准备重绘(Repaint)屏幕之前,执行一下我指定的这个函数。”
这个简单的模式,完美地解决了上述所有问题:
-
时机精准,与渲染同步:浏览器是“帧”的管理者,它最清楚何时进行下一次绘制。
rAF回调函数被安排在浏览器绘制流程的最佳时机点,确保你的每一次更新都能赶上当次绘制,从而实现最流畅的动画。不多不少,一帧只执行一次。 -
避免无效渲染:因为
rAF的执行与屏幕刷新率绑定(通常是 60Hz,即大约每 16.7ms 一次),所以不会发生在一帧内执行多次动画代码的浪费情况。 -
智能的资源管理:当页面处于非激活状态(例如最小化或切换到其他标签页)时,浏览器会自动暂停
rAF的执行。当页面再次激活时,它会无缝地继续执行。这极大地节省了CPU和电池资源。 -
由浏览器决定频率:你不再需要猜测
16、17还是1000/60。浏览器会根据当前显示器的刷新率(可能是60Hz, 75Hz, 120Hz, 144Hz等)自动调整执行频率,始终以最优的节奏运行。
三、核心用法与语法
1. 基础动画循环
这是最经典的用法。要创建一个持续的动画,你需要在回调函数的结尾再次调用 requestAnimationFrame,形成一个循环。
JavaScript
const element = document.getElementById('my-box');
let start = null; // 用于记录动画开始时间
function step(timestamp) {
// timestamp 是由浏览器传入的高精度时间戳,表示 rAF 回调开始执行的时间
if (!start) start = timestamp;
const progress = timestamp - start; // 计算从开始到现在经过了多少毫秒
// 让元素在2秒内向右移动500px
// progress / 2000 得到一个 0 到 1 的进度值
element.style.transform = 'translateX(' + Math.min(progress / 2000, 1) * 500 + 'px)';
if (progress < 2000) {
// 如果动画未结束,则请求下一帧
window.requestAnimationFrame(step);
}
}
// 启动动画
window.requestAnimationFrame(step);
关键点:
-
timestamp参数:这是rAF的精髓所在。它让你能创建帧率无关的动画。无论用户的显示器是60Hz还是144Hz,动画完成的总时长都应该是2秒。通过计算时间差(deltaTime),你可以让动画的运动速度基于真实时间,而不是基于帧数。 -
循环调用:在
step函数内部再次调用requestAnimationFrame(step)是创建连续动画的关键。 -
取消动画:
requestAnimationFrame会返回一个ID,你可以使用cancelAnimationFrame(id)来中途停止动画。
JavaScript
let animationId = window.requestAnimationFrame(step);
// 在某个时刻,比如用户点击按钮时
document.getElementById('stopBtn').onclick = function() {
window.cancelAnimationFrame(animationId);
};
四、全面且深入的用法扩展
rAF 的能力远不止于简单的DOM动画。它的核心价值在于“同步于渲染”,这使得它在任何与视觉更新相关的场景下都非常有用。
1. 高性能的事件处理(滚动、缩放、鼠标移动)
像 scroll, resize, mousemove 这类事件触发频率极高。如果你在事件回调里直接进行复杂的DOM操作,会引发大量的重排和重绘,导致页面卡顿。
rAF 提供了一种优雅的节流(Throttle)方案。
错误示范:
JavaScript
window.addEventListener('scroll', () => {
// 每次滚动都直接操作DOM,性能极差
document.body.style.backgroundColor = `hsl(${window.scrollY % 360}, 50%, 50%)`;
});
正确且高性能的用法:
JavaScript
let isTicking = false;
let lastScrollY = 0;
function updateOnScroll() {
// 在这里进行所有耗费性能的DOM读取和写入
document.body.style.backgroundColor = `hsl(${lastScrollY % 360}, 50%, 50%)`;
// 重置标志位
isTicking = false;
}
window.addEventListener('scroll', () => {
lastScrollY = window.scrollY; // 仅更新变量
if (!isTicking) {
// 如果当前没有正在等待执行的 rAF,就请求一个
window.requestAnimationFrame(updateOnScroll);
isTicking = true; // 设置标志位,防止在一帧内重复请求
}
});
原理分析:
用户的滚动操作可能在16.7ms内触发了10次 scroll 事件。上述代码只会将最新的滚动位置 lastScrollY 记录下来,并且只会在下一帧绘制前调用一次 updateOnScroll 函数。这确保了无论事件触发多频繁,DOM更新的频率永远不会超过屏幕的刷新率,实现了完美的性能优化。
2. Canvas 和 WebGL 游戏/视觉渲染主循环
对于 <canvas> 和 WebGL 编程,rAF 是驱动整个渲染流程的唯一标准。游戏或交互式视觉应用的主循环就是一个典型的 rAF 循环。
JavaScript
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 游戏状态
const player = { x: 50, y: 50, speed: 200 }; // 速度:200像素/秒
let lastTime = 0;
function gameLoop(currentTime) {
const deltaTime = (currentTime - lastTime) / 1000; // 计算距离上一帧的时间差(秒)
lastTime = currentTime;
// 1. 更新状态 (Update)
update(deltaTime);
// 2. 清空画布并绘制 (Draw)
draw();
// 3. 请求下一帧
requestAnimationFrame(gameLoop);
}
function update(dt) {
// 根据时间差来更新玩家位置,保证帧率无关
player.x += player.speed * dt;
if (player.x > canvas.width) player.x = 0;
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height); // 清空画布
ctx.fillStyle = 'red';
ctx.fillRect(player.x, player.y, 20, 20); // 绘制玩家
}
// 启动游戏循环
requestAnimationFrame(gameLoop);
这个 update -> draw 的循环结构是所有实时图形应用的基础,而 rAF 是驱动它的心脏。
3. 性能监控:实时FPS计算
想知道你的页面动画或交互的实际帧率(FPS)吗?rAF 是最精确的测量工具。
JavaScript
let frameCount = 0;
let lastTimestamp = performance.now();
const fpsDisplay = document.getElementById('fps');
function measureFPS(timestamp) {
frameCount++;
if (timestamp - lastTimestamp >= 1000) {
fpsDisplay.textContent = `FPS: ${frameCount}`;
frameCount = 0;
lastTimestamp = timestamp;
}
requestAnimationFrame(measureFPS);
}
requestAnimationFrame(measureFPS);
这段代码每秒钟统计一次 rAF 的调用次数,这个次数就是当前页面渲染的实际帧率。
4. 同步多个动画的启动
如果你需要多个独立的动画在视觉上完全同时开始,将它们的启动函数放在同一个 rAF 回调中是最佳实践。
JavaScript
function animateBox1() { /* ... */ }
function animateText() { /* ... */ }
function animateBackground() { /* ... */ }
// 同时请求启动,确保它们在同一帧开始
requestAnimationFrame(() => {
animateBox1();
animateText();
animateBackground();
});
五、与 CSS 动画/过渡的抉择
虽然 rAF 非常强大,但并非所有动画都需要它。
-
优先选择 CSS Transitions 和 Animations:
-
场景:简单的、定义明确的“状态A到状态B”的过渡(如
hover效果、菜单滑入滑出)。 -
优势:语法简单(声明式),浏览器可以进行深度优化,比如将动画完全交给GPU处理,完全不占用主线程(JS线程)资源,性能通常是最好的。
-
-
选择
requestAnimationFrame:-
场景:
-
需要与用户输入实时交互的动画(如跟随鼠标的特效)。
-
复杂的、基于物理计算的动画(如粒子系统、布料模拟)。
-
动画路径或时间线不固定,需要由JS动态计算。
-
在
<canvas>或 WebGL 上进行绘制。 -
需要精确控制动画过程中的每一个细节,或者需要与其他逻辑同步。
-
-
总结
requestAnimationFrame 是现代Web动画和高性能视觉更新的基石。它并非一个简单的定时器,而是开发者与浏览器渲染引擎之间的一个协作协议。
-
核心价值:将JavaScript的计算与浏览器的绘制周期同步,以屏幕刷新率为节奏,不多也不少。
-
核心优势:流畅(无卡顿)、高效(智能暂停,节省CPU和电池)、精确(帧率自适应)。
-
核心用法:
-
JS动画:创建基于时间戳的、帧率无关的流畅动画。
-
事件节流:优化高频事件(滚动、鼠标移动)的回调,防止性能瓶颈。
-
渲染循环:驱动Canvas/WebGL等实时图形应用。
-
理解并掌握 requestAnimationFrame,就意味着你掌握了在浏览器中创建高性能、流畅视觉体验的关键钥匙。对于任何涉及动态视觉更新的场景,它都应该是你的首选方案。