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
,除非有特殊需求(如教学、自定义调度)。
希望这份指南帮你理解定时器的底层逻辑! 🚀