背景
接到一个需求,需要用固定33毫秒间隔去执行任务
解决方案
setInterval
优点:使用简单,阅读简单
缺点:
- 某些浏览器中,特别是Firefox浏览器,
setInterval
的精确性可能会受到影响,导致无法实现准确的间隔时间。这是因为setInterval
的执行时间不保证准确,可能会受到其他任务和浏览器性能的影响。 setInterval
不考虑回调函数的执行时间,如果回调函数的执行时间较长,可能会导致多个回调函数并发执行。如果内含异步函数,那么不能保证异步执行完成再顺序执行下一个回调
requestAnimationFrame
结合时间戳的方法
requestAnimationFrame
在每个浏览器重绘之前执行回调函数,以提供更平滑的动画效果。一般用于绘制样式,动效.
优点:
- 每一帧都执行,精确到每一帧,最为精准
- 支持异步逻辑递归执行
- 当页面隐藏时,不会执行,节约性能
缺点:
- 如果是高刷屏,那么浏览器的FPS就会超过60,也就是低于16ms,不好保证时间间隔
- 触发过于频繁,如果判断逻辑较为复杂,会比较耗费性能
const desiredInterval = 32 // 目标执行间隔为32毫秒
// 初始化上一次回调的时间戳
let previousTimestamp = performance.now()
const animate = async timestamp => {
// 检查是否已经达到指定的执行间隔
if (timestamp - previousTimestamp >= desiredInterval) {
// 执行您的操作...
console.log('Animating...')
// await ...
// 更新上一次回调的时间戳
previousTimestamp = timestamp
}
// 请求下一次回调
requestAnimationFrame(animate)
}
// 启动
requestAnimationFrame(animate)
setTimeout
结合时间戳的方法
优点:
- 动态计算上一次执行与这次执行的间隔,动态计算出下一次执行的间隔时间
- 可以配合异步方法,使得内部的异步方法执行完成之后再去调用下一次方法
- 不需要频繁进行判断计算
缺点:
- 准确度尚可
let timer=null //timerId 用于clear
let desiredInterval = 33; // 目标执行间隔为33毫秒
let previousTimestamp = performance.now(); // 上一次回调的时间戳
const exec = async() => {
const currentTimestamp = performance.now(); // 当前时间戳
const elapsed = currentTimestamp - previousTimestamp; // 计算自上一次回调以来的时间差
// ...
// await ...
previousTimestamp = currentTimestamp; // 更新上一次回调的时间戳
// 调整下一次回调的延迟时间并且执行定时任务
const delay = Math.max(0, desiredInterval - elapsed);
timer=setTimeout(exec, delay);
}
// 首次调用
exec();
delay浮动为0~33ms
如果上一个任务执行得比较慢,到现在间隔比较久了,那么delay为0马上执行
如果上一个任务很快执行完了,执行到现在间隔没那么久,那么计算得出的delay就在(0,33]这个范围内
但是由于setTimeout是依然是宏任务,并且浏览器单线程的问题,宏任务队列中如果有其他的宏任务待执行,那么无论delay是不是0,其实都会有少些延迟
只不过 动态计算的delay更为准确,每次执行间隔都比上面的方法较为准确
结论
- 如果轮巡判断逻辑简单低逻辑,需要细粒度精细到每一帧准确,那么就使用
requestAnimationFrame
结合时间戳的方法 - 如果轮巡判断逻辑比较复杂,耗时,不需要精细到每一帧的执行间隔,那么就使用
setTimeout
结合时间戳的方法