requestAnimationFrame
浏览器提供的动画调度 API,回调在下一次渲染前执行。与 setTimeout 不同,它与显示器刷新率同步(通常 60fps),是实现流畅动画的标准方式。
定义
requestAnimationFrame(rAF) 是浏览器提供的动画调度 API。传入的回调函数会在浏览器下一次重绘之前执行,确保动画更新与屏幕刷新率同步。
const id = requestAnimationFrame(callback);
cancelAnimationFrame(id); // 取消尚未执行的回调
requestAnimationFrame 返回一个整数 ID,可以传给 cancelAnimationFrame 来取消回调——两者成对使用,类似 setTimeout / clearTimeout 的关系。
在事件循环中的位置
根据 WHATWG HTML 规范的 event loop processing model,rAF 回调在渲染流水线中有独立的执行时机,处于微任务清空之后、浏览器实际渲染之前:
宏任务执行 → 微任务队列清空 → rAF 回调执行 → Style → Layout → Paint
rAF 回调既不属于宏任务队列,也不属于微任务队列——它是渲染步骤的一部分。这意味着在 rAF 回调中修改 DOM 或样式,会在同一帧内被渲染引擎处理,不会产生额外的重排。
与 setTimeout 的区别
| 维度 | requestAnimationFrame | setTimeout(fn, 16) |
|---|---|---|
| 同步刷新率 | 自动适配显示器(60Hz/120Hz/144Hz) | 固定间隔,可能与刷新率偏移 |
| 页面隐藏时 | 自动暂停,节省资源 | 继续执行,浪费 CPU |
| 执行时机 | 渲染前,保证视觉一致性 | 下一轮宏任务,可能错过渲染时机 |
| 动画流畅度 | 稳定无撕裂 | 可能掉帧或画面撕裂 |
| 时间精度 | 回调接收高精度时间戳 | 需要手动 Date.now() 或 performance.now() |
典型应用场景
1. 平滑动画
最基本的用途——用 rAF 驱动逐帧动画,取代 setInterval:
function animate(timestamp) {
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / duration, 1);
element.style.transform = `translateX(${progress * 300}px)`;
if (progress < 1) {
requestAnimationFrame(animate);
}
}
const startTime = performance.now();
const duration = 1000;
requestAnimationFrame(animate);
rAF 回调接收一个 DOMHighResTimeStamp 参数(单位毫秒,精度到微秒),用它计算帧间隔可以实现与帧率无关的动画——无论 60Hz 还是 144Hz 屏幕,动画总时长一致。
2. 滚动/Resize 节流
用 rAF 替代传统的 throttle 函数处理高频事件:
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
updateScrollPosition(window.scrollY);
ticking = false;
});
ticking = true;
}
});
每帧最多处理一次,自动与渲染同步。比 lodash.throttle(fn, 16) 更精准。
3. Canvas 游戏循环
Canvas 2D 或 WebGL 的渲染循环标准写法:
function gameLoop(timestamp) {
update(timestamp); // 更新游戏状态
render(); // 重绘画面
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
4. DOM 批量更新
当需要在一帧内收集多次状态变化并统一应用到 DOM:
let pending = false;
let updates = [];
function scheduleUpdate(change) {
updates.push(change);
if (!pending) {
pending = true;
requestAnimationFrame(() => {
applyUpdates(updates);
updates = [];
pending = false;
});
}
}
React 的 setState 批处理和 Vue 的 nextTick 在部分场景下采用了类似的思路。
可视化:rAF 驱动的缓动小球
rAF 缓动动画
注意事项
- 每次调用只注册一次回调:rAF 回调内再次调用
requestAnimationFrame注册的回调在下一帧执行,不会在当前帧无限递归 - 后台标签页行为:在后台标签页中 rAF 会被暂停或降频至约 1fps,节省系统资源
- 必须成对使用:通过
cancelAnimationFrame(id)取消回调,避免组件卸载后继续执行动画导致内存泄漏 - 不要在 rAF 中做重计算:每帧只有约 16ms(60Hz),超时会导致掉帧。将耗时计算拆分或移到 Web Worker
// React 组件中的正确用法——cleanup 时取消 rAF
useEffect(() => {
let id;
function tick() {
// 动画逻辑...
id = requestAnimationFrame(tick);
}
id = requestAnimationFrame(tick);
return () => cancelAnimationFrame(id);
}, []);
引用本术语的文章