requestAnimationFrame
(通常简写为 rAF
) 这个浏览器方法。我会用一个生动的比喻开始,然后逐步深入技术细节。
一、一个生动的比喻:电影放映机
想象一下,你想在浏览器里做一个流畅的动画,就像播放一部电影。
-
旧方法 (
setTimeout
/setInterval
):-
你就像一个新手放映员,手里拿着一堆电影胶片(动画的每一帧)。
-
你买了一个闹钟 (
setTimeout
),设定它每 16.7 毫秒响一次(因为 1000ms / 60帧 ≈ 16.7ms)。 -
闹钟一响,你就赶紧把下一张胶片放到放映机上。
-
问题来了:
-
不精准:你的闹钟可能响得太早或太晚。如果放映机(浏览器)还没准备好放下一帧,你放了也是白放(浪费资源);如果放映机已经准备好了,你的闹钟却晚了,那么那一帧就会被跳过,导致画面卡顿(掉帧)。
-
浪费资源:即使观众都走了(你切换到了其他浏览器标签页),你的闹钟还在执着地响,你还在傻傻地换胶片,白白浪费电力(CPU 和电池)。
-
-
-
新方法 (
requestAnimationFrame
):-
你现在是一个聪明的放映员。
-
你不再自己设闹钟,而是直接告诉放映机(浏览器):"嘿,你下次准备好放映新胶片之前,叫我一下,我把新胶片给你。"
-
于是,浏览器在每一次要"绘制"新画面之前,就会主动通知你。你就在那个时机把计算好的新画面内容(比如更新元素的位置)交给它。
-
好处显而易见:
-
时机完美:你的所有工作都正好赶在浏览器需要的时候完成,不多不少,动画如丝般顺滑。
-
智能节能:如果观众走了(标签页被隐藏或最小化),放映机就暂停放映了,它自然也不会再叫你,你的工作也就自动暂停了,节省了宝贵的系统资源。
-
-
二、requestAnimationFrame
是什么?
requestAnimationFrame
是一个浏览器提供的 Web API,它的作用是请求浏览器在下一次重绘(repaint)之前,执行一个你指定的回调函数。
简单来说:你不是命令浏览器"立即"或"在XX毫秒后"更新动画,而是请求浏览器在它自己认为最合适的时机(也就是下一次渲染新画面前)来执行你的更新逻辑。
三、为什么它比 setTimeout
/ setInterval
更好?
特性 | setTimeout / setInterval |
requestAnimationFrame (rAF) |
---|---|---|
同步机制 | 与浏览器渲染异步。它有自己的内部时钟,不关心浏览器是否准备好渲染。 | 与浏览器渲染同步。它的执行时机由浏览器显示器的刷新率决定。 |
性能/流畅度 | 容易导致掉帧 或无效渲染,因为执行时机不精确,可能在一帧内执行多次,或错过某一帧。 | 最大程度地保证流畅。浏览器会确保你的回调在每一帧的绘制周期内只执行一次,避免了卡顿。 |
资源消耗 | 即使页面在后台或不可见,定时器依然会持续执行,消耗 CPU 和电池。 | 当页面在后台或不可见时,浏览器会自动暂停 执行,非常节能。 |
浏览器优化 | 浏览器无法优化。 | 浏览器可以将多个 rAF 回调(来自不同组件或库)合并到一次重绘中,提高渲染效率,避免"布局抖动"(Layout Thrashing)。 |
四、如何使用 requestAnimationFrame
?
它的用法非常简单,通常用于创建一个动画循环。
基础示例:让一个方块向右移动
html
<div id="box" style="position: absolute; left: 0; width: 50px; height: 50px; background: skyblue;"></div>
<script>
const box = document.getElementById('box');
let position = 0;
let animationFrameId;
// 动画循环函数
function animate() {
// 1. 更新状态(例如,位置)
position += 2;
box.style.left = position + 'px';
// 2. 如果动画没有结束,请求下一帧
if (position < 300) {
// 这是关键!请求浏览器在下一次重绘前再次调用 animate 函数
animationFrameId = requestAnimationFrame(animate);
}
}
// 启动动画
// 只需要调用一次,就会开启一个由浏览器驱动的循环
animationFrameId = requestAnimationFrame(animate);
// 如果需要,可以随时停止动画
// setTimeout(() => {
// cancelAnimationFrame(animationFrameId);
// console.log('动画已停止');
// }, 2000);
</script>
代码解析:
-
我们定义了一个
animate
函数,它负责单帧的动画逻辑:更新位置,并应用到 DOM 元素上。 -
在
animate
函数的末尾,我们再次调用requestAnimationFrame(animate)
。这就像接力赛,这一帧的选手跑完后,把接力棒交给了下一帧的自己,从而形成一个持续的循环。 -
这个循环不是死循环,它的执行频率由浏览器控制,通常是每秒 60 次(对于 60Hz 的屏幕)。
-
requestAnimationFrame
会返回一个 ID,你可以使用cancelAnimationFrame(id)
来随时终止这个动画循环。
进阶示例:使用时间戳实现匀速动画
requestAnimationFrame
的回调函数会接收一个高精度的时间戳参数(DOMHighResTimeStamp
),表示自页面加载以来经过的毫秒数。使用这个时间戳可以创建与帧率无关的、更精确的动画。
javascript
const box = document.getElementById('box');
let startTime = null;
const duration = 2000; // 动画持续时间 2000ms
const distance = 300; // 总移动距离 300px
function animate(currentTime) {
// 记录第一帧的时间
if (startTime === null) {
startTime = currentTime;
}
// 计算已经过的时间
const elapsedTime = currentTime - startTime;
// 计算当前应该在的位置(基于时间的百分比)
// Math.min 确保动画不会超出
const progress = Math.min(elapsedTime / duration, 1);
const newPosition = progress * distance;
box.style.transform = `translateX(${newPosition}px)`;
// 如果动画还没结束,继续请求下一帧
if (progress < 1) {
requestAnimationFrame(animate);
}
}
// 启动动画
requestAnimationFrame(animate);
这个进阶版本的好处是,无论用户的电脑帧率是 30fps 还是 144fps,这个动画都会在 2 秒内完成,只是高帧率的设备上动画过程会显得更平滑。
五、总结
-
是什么?
requestAnimationFrame
是一个请求浏览器在下次重绘前执行特定函数的 API。 -
为什么用? 为了创建高性能、流畅且节能的 Web 动画。
-
核心优势:
* 与刷新率同步:避免掉帧,动画更流畅。
* 智能暂停:页面不可见时自动暂停,节省 CPU/电池。
* 浏览器优化:能将多个动画更新合并为一次重绘。
- 怎么用? 创建一个递归调用的动画函数,在函数内部更新画面状态,并调用
requestAnimationFrame
请求下一帧。
所以,下次当你需要用 JavaScript 做任何涉及连续视觉变化(如动画、游戏循环、数据可视化更新)的需求时,requestAnimationFrame
应该是你的首选。