引言
在前端开发中,我们经常需要处理定时任务或动画效果。JavaScript提供了多种实现方式,其中最常用的是setTimeout和setInterval,而requestAnimationFrame则是HTML5新增的API,专为动画优化设计。本文将深入探讨这三种定时器的工作原理、优缺点,并展示如何使用requestAnimationFrame模拟前两者的功能,以解决它们存在的问题。
一、setTimeout详解
1.1 基本用法
// 延迟1000毫秒后执行回调函数
const timeoutId = setTimeout(() => {
console.log('延迟执行');
}, 1000);
// 取消定时器
clearTimeout(timeoutId);
1.2 优点
- 简单易用:API直观,学习成本低
- 灵活性高:可以设置任意延迟时间
- 广泛兼容:所有浏览器都支持
1.3 缺陷
- 精度问题:实际延迟时间可能大于设定值
- 最小延迟限制:HTML5规范规定最小延迟为4ms
- 执行顺序不确定:受主线程任务队列影响
- 阻塞问题:如果主线程繁忙,回调会被延迟执行
二、setInterval详解
2.1 基本用法
// 每隔1000毫秒执行一次回调函数
const intervalId = setInterval(() => {
console.log('定时执行');
}, 1000);
// 取消定时器
clearInterval(intervalId);
2.2 优点
- 自动重复执行:无需手动递归调用
- 实现简单:适合周期性任务
- 资源占用稳定:相比递归setTimeout更高效
2.3 缺陷
- 累积执行问题:如果回调执行时间超过间隔时间,会导致回调堆积,存在跳帧情况,可能多个定时器连续执行
- 时间漂移:长期运行后,实际执行间隔会偏离预期
- 无执行完成保证:页面卸载时可能无法执行清理操作
- 同样存在最小延迟限制:与setTimeout相同
三、requestAnimationFrame详解
3.1 基本用法
function animate(timestamp) {
// 动画逻辑
console.log('动画帧执行');
requestAnimationFrame(animate);
}
// 启动动画
const rafId = requestAnimationFrame(animate);
// 取消动画
cancelAnimationFrame(rafId);
3.2 优点
- 与屏幕刷新率同步:通常为60fps,避免过度绘制
- 浏览器优化:后台标签页或隐藏元素时会暂停执行
- 性能更好:减少CPU占用,延长移动设备电池寿命
- 时间精度高:使用系统时间戳,不受执行时间影响
- 自动调整:根据设备性能动态调整帧率
3.3 缺陷
- 时间间隔不可控:由浏览器决定执行时机
- 不适合精确计时:无法保证毫秒级精度
- 兼容性问题:IE9及以下不支持
- 需要手动递归:实现循环需要显式调用
四、三者性能对比
| 特性 | setTimeout | setInterval | requestAnimationFrame |
|---|---|---|---|
| 执行时机 | 延迟指定时间后 | 每隔指定时间 | 下一帧渲染前 |
| 精度 | 低(可能延迟) | 低(可能漂移) | 高(与刷新同步) |
| 最小延迟 | 4ms | 4ms | 约16ms(60fps) |
| CPU占用 | 中 | 中 | 低 |
| 后台执行 | 延迟增加 | 延迟增加 | 暂停 |
| 动画优化 | 无 | 无 | 有 |
4.1三者动画示例 以及 执行差值校对
对比: 查看动画流畅度 以及 执行偏差(查看log)
4.2、总结
1.
setTimeout和setInterval适用于简单的定时任务,但存在精度和性能问题;
2.requestAnimationFrame专为动画优化,提供更好的性能和用户体验。
下面我们通过模拟实现,我们可以结合requestAnimationFrame的优势来替代传统定时器。
五、用requestAnimationFrame模拟定时器
5.1 模拟setTimeout
// raf.js
function rafTimeout(callback, delay) {
const start = performance.now();
let id;
function loop(timestamp) {
// timestamp参数:与performance.now()的返回值相同,它表示requestAnimationFrame() 开始去执行回调函数的时刻
const elapsed = timestamp - start;
if (elapsed >= delay) {
callback();
} else {
id = requestAnimationFrame(loop);
}
}
id = requestAnimationFrame(loop);
return id;
}
// 使用示例
const timeoutId = rafTimeout(() => {
console.log('模拟setTimeout执行');
}, 1000);
// 取消
cancelAnimationFrame(timeoutId);
5.2 模拟setInterval
setInterval就是 在 模拟setTimeout执行之后 重新计算间隔 做判断
// raf.js
/**
*
* @param callback
* @param delay 延时ms
* @param interval 模拟setInterval ++++++++
* @returns {number}
*/
function rafTimeout(callback, delay, interval = false) {
let start = performance.now();
let id;
function loop(timestamp) {
const elapsed = timestamp - start;
if (elapsed >= delay) {
callback();
// ++++++++ 新增
if (interval) {
start = timestamp;
id = requestAnimationFrame(loop);
}
// ++++++++ 新增
} else {
id = requestAnimationFrame(loop);
}
}
id = requestAnimationFrame(loop);
return id;
}
// 使用示例
const intervalId = rafTimeout(() => {
console.log('模拟setInterval执行');
}, 1000, true);
// 取消
// cancelAnimationFrame(intervalId);
setTimeout(() => {
cancelAnimationFrame(intervalId);
}, 50)
发现问题: 由于我们是通过 多次执行
requestAnimationFrame根据时间戳判断时间再做callback的,也就意味着 其实我们返回的id其实是一直在变动的,我们错过在第一次执行 requestAnimationFrame 方法之后 就无法正确获取当前 raf的id, 我们可以将id交给引用类型来接管
// id => { id }
function rafTimeout(callback, delay, interval = false) {
// ...
// let id; // ----
const raf = { id: requestAnimationFrame(loop) } // ++++++++
function loop(timestamp) {
// ...
if (interval) {
// ...
// id = requestAnimationFrame(loop) --------
raf.id = requestAnimationFrame(loop); // ++++++++
}
} else {
// id = requestAnimationFrame(loop) --------
raf.id = requestAnimationFrame(loop); // ++++++++
}
}
return raf; // ++++
}
六 完整代码
// raf.js
/**
*
* @param callback
* @param delay 延时ms
* @param interval 模拟setInterval
* @returns {number}
*/
function rafTimeout(callback, delay, interval = false) {
let start = performance.now();
const raf = { id: requestAnimationFrame(loop) }
function loop(timestamp) {
const elapsed = timestamp - start;
if (elapsed >= delay) {
callback();
if (interval) {
start = timestamp;
raf.id = requestAnimationFrame(loop);
}
} else {
raf.id = requestAnimationFrame(loop);
}
}
return raf;
}
// 取消 rafTimeout
function cancelRaf(raf) {
if (raf && raf.id) cancelAnimationFrame(raf.id)
}
// 使用示例
const raf = rafTimeout(() => {
console.log('模拟setInterval执行');
}, 1000, true);
// 间隔2s取消
setTimeout(() => {
cancelRaf(raf);
}, 2020)