JavaScript 中的 requestAnimationFrame 使用详解
在现代浏览器中,动画效果的实现有多种方式,其中 requestAnimationFrame 是一种高效且流畅的方法。与传统的 setTimeout 或 setInterval 不同,requestAnimationFrame 由浏览器专门为动画优化而设计,能根据屏幕刷新率自动调整回调函数的执行频率,通常为每秒60次,从而避免不必要的性能损耗和画面卡顿。
什么是 requestAnimationFrame
requestAnimationFrame 是浏览器提供的一个 API,它告诉浏览器在下次重绘之前执行指定的回调函数。这个机制让动画与浏览器的绘制周期同步,减少 CPU 和 GPU 的负载,提升动画的流畅度。
它的核心优势包括:
- 自动适应屏幕刷新率,不会像
setInterval那样出现丢帧或过度绘制。 - 当页面隐藏或最小化时,浏览器会自动暂停回调,节省资源。
- 提供精确的时间戳参数,便于计算动画进度。
基本语法
requestAnimationFrame 的用法非常简单,只需要传入一个回调函数作为参数。这个回调函数会接收一个 DOMHighResTimeStamp 参数,表示动画开始执行的时间戳。
// 基本调用方式
const animationId = requestAnimationFrame(function(timestamp) {
// 在这里编写动画逻辑
console.log("动画帧执行,时间戳:", timestamp);
});每次调用 requestAnimationFrame 都会返回一个唯一的 ID,这个 ID 可以用于取消动画,类似于 setTimeout 的返回值。
实现一个简单的动画循环
要让动画持续运行,需要在回调函数内部再次调用 requestAnimationFrame,形成一个递归循环。下面是一个让方块从左向右移动的示例。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>简单动画示例</title>
<style>
#box {
width: 50px;
height: 50px;
background-color: #3498db;
position: absolute;
top: 100px;
left: 0;
}
</style>
</head>
<body>
<div id="box"></div>
<script>
const box = document.getElementById('box');
let positionX = 0;
const targetX = 400; // 目标位置
let startTime = null;
function moveBox(timestamp) {
if (!startTime) {
startTime = timestamp; // 记录开始时间
}
const elapsed = timestamp - startTime; // 已经过去的时间(毫秒)
const duration = 2000; // 动画持续 2 秒
const progress = Math.min(elapsed / duration, 1); // 0 到 1 之间的进度
// 线性插值计算当前位置
positionX = progress * targetX;
box.style.left = positionX + 'px';
if (progress < 1) {
// 动画还没结束,继续请求下一帧
requestAnimationFrame(moveBox);
}
}
// 启动动画
requestAnimationFrame(moveBox);
</script>
</body>
</html>在这个例子中,requestAnimationFrame 递归调用自身,直到方块移动到目标位置。通过计算时间差,我们能够精确控制动画的持续时间和进度,即使帧率发生变化,动画的总体时长依然保持稳定。
使用时间戳实现匀速运动
上面的代码使用了耗时比例来控制位置。如果希望实现更复杂的运动效果,比如速度控制或缓动函数,可以基于时间戳进行更精细的计算。下面是一个使用 requestAnimationFrame 实现匀速直线运动的示例。
// 匀速运动函数
function animate(element, startX, endX, duration) {
let startTime = null;
function step(timestamp) {
if (!startTime) {
startTime = timestamp;
}
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / duration, 1); // 进度 0-1
// 线性插值
const currentX = startX + (endX - startX) * progress;
element.style.left = currentX + 'px';
if (progress < 1) {
requestAnimationFrame(step);
} else {
// 动画结束,可以执行回调
console.log('动画已完成');
}
}
requestAnimationFrame(step);
}
// 使用示例
const ball = document.getElementById('ball');
animate(ball, 0, 500, 3000);取消动画
当不再需要动画时,可以使用 cancelAnimationFrame 方法并传入之前返回的 ID 来停止动画。这在组件卸载或用户交互时非常有用,能避免不必要的性能开销。
// 启动动画并保存 ID
let animationId = requestAnimationFrame(function move(timestamp) {
// 动画逻辑...
animationId = requestAnimationFrame(move); // 重新请求
});
// 在某个条件下取消动画
function stopAnimation() {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
console.log('动画已取消');
}
}
// 模拟 3 秒后停止动画
setTimeout(stopAnimation, 3000);在上面的代码中,cancelAnimationFrame 接收了 requestAnimationFrame 返回的 ID,从而中断了动画循环。如果忘记取消,回调会持续执行,直到页面关闭或手动停止。
与 setTimeout 的性能对比
很多人习惯用 setTimeout 或 setInterval 来实现动画,但它们的缺点很明显:无法与浏览器的绘制周期同步。当 setTimeout 的回调时间不准确时,可能出现丢帧或重复绘制,导致动画卡顿。
下面是一个简单的对比示例,展示两个方法在相同任务下的表现差异。
// 使用 requestAnimationFrame 统计帧数
let rAFCount = 0;
let rAFStart = performance.now();
function countRAF(timestamp) {
rAFCount++;
if (timestamp - rAFStart < 1000) {
requestAnimationFrame(countRAF);
} else {
console.log('requestAnimationFrame 帧数:', rAFCount);
}
}
requestAnimationFrame(countRAF);
// 使用 setTimeout 统计帧数
let timeoutCount = 0;
let timeoutStart = performance.now();
function countTimeout() {
timeoutCount++;
if (performance.now() - timeoutStart < 1000) {
setTimeout(countTimeout, 16); // 模拟 60fps
} else {
console.log('setTimeout 帧数:', timeoutCount);
}
}
setTimeout(countTimeout, 16);在实际运行中,requestAnimationFrame 的帧数通常会稳定在屏幕刷新率附近(如60帧),而 setTimeout 由于事件循环的延迟积累,帧数往往偏低且不稳定。
requestAnimationFrame 的常见应用场景
除了简单的元素移动,requestAnimationFrame 在以下场景中也十分常用:
- 游戏循环:实现角色移动、碰撞检测、粒子系统等实时逻辑。
- 滚动动画:监听滚动事件并驱动视差效果或进度条动画。
- Canvas 绘图:在
<canvas>上绘制连续帧,实现图表动画或特效。 - 进度条动画:平滑更新进度条的宽度或颜色。
- 图片轮播或幻灯片:控制过渡效果的流畅切换。
注意事项
在使用 requestAnimationFrame 时,有几点需要留意:
- 回调函数中的耗时代码会影响帧率,尽量将复杂的计算放在
requestAnimationFrame之外,或者分批处理。 - 不要在回调中执行 DOM 操作导致重排或重绘,这会抵消它带来的性能优势。
- 如果动画已经不需要继续,务必调用
cancelAnimationFrame来取消,防止内存泄漏。 - 在 Web Workers 中无法使用
requestAnimationFrame,因为它依赖于主线程的 UI 更新周期。
总结
requestAnimationFrame 是现代浏览器中实现高性能动画的首选方法。它能够与屏幕刷新率同步,自动暂停隐藏页面的动画,并提供精确的时间戳用于进度计算。通过递归调用自身来形成动画循环,配合 cancelAnimationFrame 进行资源管理,可以轻松构建流畅、高效且易于控制的动画效果。无论是简单的元素移动还是复杂的游戏渲染,掌握 requestAnimationFrame 都能显著提升用户体验。