JavaScript让浏览器按固定时间间隔执行异步回调的最佳办法

81 阅读3分钟

背景

接到一个需求,需要用固定33毫秒间隔去执行任务

解决方案

setInterval

优点:使用简单,阅读简单

缺点:

  1. 某些浏览器中,特别是Firefox浏览器,setInterval的精确性可能会受到影响,导致无法实现准确的间隔时间。这是因为setInterval的执行时间不保证准确,可能会受到其他任务和浏览器性能的影响。
  2. setInterval不考虑回调函数的执行时间,如果回调函数的执行时间较长,可能会导致多个回调函数并发执行。如果内含异步函数,那么不能保证异步执行完成再顺序执行下一个回调

requestAnimationFrame结合时间戳的方法

requestAnimationFrame在每个浏览器重绘之前执行回调函数,以提供更平滑的动画效果。一般用于绘制样式,动效.

优点:

  1. 每一帧都执行,精确到每一帧,最为精准
  2. 支持异步逻辑递归执行
  3. 当页面隐藏时,不会执行,节约性能

缺点:

  1. 如果是高刷屏,那么浏览器的FPS就会超过60,也就是低于16ms,不好保证时间间隔
  2. 触发过于频繁,如果判断逻辑较为复杂,会比较耗费性能
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结合时间戳的方法

优点:

  1. 动态计算上一次执行与这次执行的间隔,动态计算出下一次执行的间隔时间
  2. 可以配合异步方法,使得内部的异步方法执行完成之后再去调用下一次方法
  3. 不需要频繁进行判断计算

缺点:

  1. 准确度尚可
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结合时间戳的方法