
requestAnimationFrame 详解及与 setTimeout/setInterval 的比较
requestAnimationFrame(简称 rAF)是浏览器提供的专门用于 动画渲染 的 API,相比 setTimeout 和 setInterval,它在性能和流畅度上有显著优势。以下是详细解析和对比:
1. requestAnimationFrame 详解
基本语法
const requestID = requestAnimationFrame(callback);
• callback:在浏览器下一次重绘之前执行的函数。
• 返回值:requestID(用于取消:cancelAnimationFrame(requestID))。
核心特点
-
与浏览器刷新率同步
• 默认以 60Hz(16.67ms/帧) 的频率执行(匹配屏幕刷新率)。
• 避免丢帧或过度渲染,保证动画流畅。 -
自动暂停后台标签页
• 当页面隐藏或最小化时,rAF会自动暂停,节省 CPU/GPU 资源。 -
高性能
• 浏览器会优化rAF的调用,合并同一帧内的多次更新。 -
精确的时间戳参数
• 回调函数接收一个DOMHighResTimeStamp参数,表示触发时间:requestAnimationFrame((timestamp) => {console.log(timestamp); // 精确到微秒 });
示例:动画循环
function animate() {// 更新动画状态console.log("Animating...");// 循环调用requestAnimationFrame(animate);
}// 启动动画
animate();
2. requestAnimationFrame vs setTimeout/setInterval
对比维度
| 特性 | requestAnimationFrame | setTimeout/setInterval |
|---|---|---|
| 执行频率 | 与屏幕刷新率同步(~60Hz) | 固定时间间隔(可能不匹配刷新率) |
| 后台标签页行为 | 自动暂停 | 继续执行(浪费资源) |
| 动画流畅度 | 高(无丢帧) | 可能卡顿(因主线程阻塞或帧率不稳定) |
| CPU/GPU 负载 | 低(浏览器优化) | 高(频繁触发回调) |
| 适用场景 | 动画、高频视觉更新 | 延迟任务、低频轮询 |
关键差异
(1)时间精度与帧率
• rAF:按屏幕刷新率(如 60Hz)执行,避免过度渲染。
• setTimeout(fn, 16):
• 理论上模拟 60Hz,但实际可能因主线程阻塞导致延迟。
• 浏览器最小延迟限制(4ms)可能破坏时序。
(2)资源占用
• rAF:浏览器智能调度,合并帧内更新。
// 连续调用 rAF 会被优化
requestAnimationFrame(animate);
requestAnimationFrame(animate); // 可能合并到同一帧
• setInterval:严格按间隔执行,即使前一帧未完成也可能触发新回调,导致堆积。
(3)动画示例对比
setTimeout 实现动画(不推荐)
function animate() {console.log("Animating...");setTimeout(animate, 16); // 尝试模拟 60Hz
}
animate();
问题:
• 可能因主线程阻塞导致卡顿。
• 后台标签页仍执行,浪费资源。
rAF 实现动画(推荐)
function animate() {console.log("Animating...");requestAnimationFrame(animate);
}
animate();
优势:
• 自动匹配刷新率,流畅且节能。
• 后台自动暂停。
3. 如何选择?
使用 requestAnimationFrame 当:
• 需要 流畅动画(如 CSS 变换、Canvas 绘图)。
• 高频更新 UI(如游戏、实时图表)。
• 希望 节省资源(特别是移动端)。
使用 setTimeout/setInterval 当:
• 需要 精确控制延迟(如 1 秒后跳转页面)。
• 执行 非视觉任务(如轮询 API)。
• 兼容旧浏览器(rAF 需 IE10+)。
4. 进阶技巧
(1)计算帧率(FPS)
let lastTime = 0;
function animate(timestamp) {const fps = 1000 / (timestamp - lastTime); // 计算帧率console.log(`FPS: ${fps.toFixed(2)}`);lastTime = timestamp;requestAnimationFrame(animate);
}
animate();
(2)降级兼容(旧浏览器)
const rAF = window.requestAnimationFrame || window.webkitRequestAnimationFrame || function(callback) {return setTimeout(callback, 16);};
(3)控制动画速度
let startTime;
function animate(timestamp) {if (!startTime) startTime = timestamp;const progress = timestamp - startTime; // 动画已运行时间const duration = 2000; // 动画总时长(2秒)if (progress < duration) {const ratio = progress / duration; // 0~1console.log(`进度: ${(ratio * 100).toFixed(1)}%`);requestAnimationFrame(animate);}
}
animate();
5. 总结
| API | 最佳场景 | 注意事项 |
|---|---|---|
requestAnimationFrame | 动画、高频渲染 | 无需手动控制帧率 |
setTimeout | 单次延迟任务 | 避免用于动画(可能卡顿) |
setInterval | 低频轮询(如每 5 秒检查数据) | 注意清理(clearInterval) |
黄金法则:
凡是涉及 视觉更新 的,优先用
requestAnimationFrame;
非视觉任务(如逻辑控制),再用setTimeout/setInterval。
手写 setTimeout 和 setInterval(JavaScript 实现)
由于 setTimeout 和 setInterval 是浏览器/Node.js 提供的 Web API,我们无法完全用纯 JavaScript 实现它们(因为它们依赖底层事件循环机制)。但我们可以用 JavaScript 模拟 它们的行为,并理解其核心逻辑。
1. 手写 setTimeout(模拟版)
思路
• 使用 Date.now() 计算时间差。
• 用 requestAnimationFrame(浏览器)或 while 循环(Node.js)检查是否到达延迟时间。
代码实现(浏览器环境)
function mySetTimeout(callback, delay) {const startTime = Date.now();function checkTime() {const currentTime = Date.now();if (currentTime - startTime >= delay) {callback(); // 时间到了,执行回调} else {requestAnimationFrame(checkTime); // 继续检查}}requestAnimationFrame(checkTime);
}// 测试
mySetTimeout(() => console.log("Hello after 1s"), 1000);
说明:
• requestAnimationFrame 是浏览器 API,用于在下一帧渲染前执行回调(约 60fps)。
• 此方法 不精确(requestAnimationFrame 不是严格计时器),但能模拟 setTimeout 的异步行为。
2. 手写 setInterval(模拟版)
思路
• 递归调用 mySetTimeout 实现循环执行。
• 用 clear 方法模拟 clearInterval。
代码实现
function mySetInterval(callback, interval) {let timerId = null;function execute() {callback();timerId = mySetTimeout(execute, interval); // 递归调用}timerId = mySetTimeout(execute, interval);return {clear: () => {// 模拟 clearIntervalif (timerId) {// 这里需要实现 clearMyTimeout,但简化版无法真正取消console.log("Interval cleared");timerId = null;}}};
}// 测试
const interval = mySetInterval(() => console.log("Tick"), 1000);
setTimeout(() => interval.clear(), 5000); // 5秒后停止
问题:
• 由于 mySetTimeout 无法真正取消(没有 clearMyTimeout),此方法 无法完全模拟 setInterval。
3. 更精确的实现(基于 Promise + async/await)
思路
• 用 Promise + setTimeout 模拟可控的 mySetTimeout。
• 用 async/await 实现 mySetInterval。
代码
// 精确版 mySetTimeout
function mySetTimeout(callback, delay) {return new Promise((resolve) => {setTimeout(() => {callback();resolve();}, delay);});
}// 精确版 mySetInterval
async function mySetInterval(callback, interval) {while (true) {await mySetTimeout(callback, interval);}
}// 测试
(async () => {mySetInterval(() => console.log("Tick"), 1000);
})();
特点:
• 基于原生 setTimeout,计时更精确。
• 用 while(true) 实现循环,但 无法直接取消(需额外逻辑)。
4. 终极方案(完整模拟 clearTimeout 和 clearInterval)
思路
• 用 Map 存储所有定时器 ID。
• 提供 clearMyTimeout 和 clearMyInterval 方法。
完整代码
const timers = new Map();
let id = 0;// 模拟 setTimeout
function mySetTimeout(callback, delay) {const timerId = id++;const startTime = Date.now();function checkTime() {const currentTime = Date.now();if (currentTime - startTime >= delay) {callback();timers.delete(timerId); // 执行后移除} else if (timers.has(timerId)) {requestAnimationFrame(checkTime); // 继续检查}}timers.set(timerId, true);requestAnimationFrame(checkTime);return timerId;
}// 模拟 clearTimeout
function clearMyTimeout(timerId) {if (timers.has(timerId)) {timers.delete(timerId); // 标记为取消}
}// 模拟 setInterval
function mySetInterval(callback, interval) {const timerId = id++;function execute() {if (!timers.has(timerId)) return; // 已取消callback();mySetTimeout(execute, interval); // 递归调用}timers.set(timerId, true);mySetTimeout(execute, interval);return timerId;
}// 模拟 clearInterval
function clearMyInterval(timerId) {clearMyTimeout(timerId); // 复用逻辑
}// 测试
const timeoutId = mySetTimeout(() => console.log("Timeout"), 1000);
const intervalId = mySetInterval(() => console.log("Interval"), 1000);setTimeout(() => {clearMyTimeout(timeoutId);clearMyInterval(intervalId);
}, 3000);
说明:
• 用 Map 存储定时器 ID,clearMyTimeout 和 clearMyInterval 可以取消任务。
• 仍然依赖 requestAnimationFrame,不是严格精确,但能模拟基本行为。
5. 总结
| 方法 | 优点 | 缺点 |
|---|---|---|
mySetTimeout | 简单模拟异步延迟 | 不精确,依赖 requestAnimationFrame |
mySetInterval | 模拟循环执行 | 无法真正取消 |
Promise 版 | 更接近原生行为 | 仍依赖原生 setTimeout |
| 终极方案 | 支持取消,更完整 | 代码较复杂 |
关键点
setTimeout和setInterval是浏览器/Node.js 提供的 API,无法完全用 JS 实现。- 模拟版依赖
requestAnimationFrame或Promise,无法做到完全精确。 - 最佳实践:直接使用原生
setTimeout和setInterval,除非有特殊需求(如教学、自定义调度)。
希望这份指南帮你理解定时器的底层逻辑! 🚀
