likes
comments
collection
share

时间系列二:高精度计时器方案,worker计时,settimeout差值补偿,raf计时器,setInterval

作者站长头像
站长
· 阅读数 26

先有问题再有答案

  1. js计时器不准 这个是由哪些方面导致的?
  2. raf实现计时器会更准确?
  3. web worker实现计时器会更准确?
  4. 实现高精度计时器有哪些方案
  5. 这些方案有什么优缺点
  6. 如何理解settimeout和setInterval

背景

这里我们介绍下时间系列的第二篇高精度计时器方案

预告一下 第三篇 H5实时&秒杀方案

效果展示

时间系列二:高精度计时器方案,worker计时,settimeout差值补偿,raf计时器,setInterval

setInterval计时器

// 获取HTML页面上显示计时器结果的DOM元素
const intervalDom = document.getElementById('interval');
const workerDom = document.getElementById('worker');
const timeoutDom = document.getElementById('timeout');
const rafDom = document.getElementById('raf');

/**
 * 模拟主线程随机阻塞
 */
setInterval(() => {  // 每隔一段随机时间,阻塞JS线程一段时间,模拟JS线程被阻塞的情况
  let count;
  for (let i = 0; i < 999999999; i++) {
    count = i + count - count / 2;
  }
}, Math.random() * 10 * 1000)


/**
 * 使用 setInterval 进行计时
 */
function timer_interval() {
  let countTime = 0;  // 初始化计时器
  setInterval(() => { // 设置定时器,每一秒执行一次
    intervalDom.innerText = `interval: ${++countTime}`  // 更新DOM元素显示的文本
  }, 1000);
}
timer_interval();

计时器不准确的原因

  1. 可能会受到其他JS代码的影响。JavaScript是单线程运行的,也就是说在同一时间,只能有一个任务在运行。js的计时器是在设置时间后 加入到任务队列中 等待执行,只有将当前的任务队列清空才会执行到这个计时器。如果前面的代码执行时间过长,就会导致后续的计时器任务无法准时执行。例如,假设我们设置了一个定时器每隔1秒执行一次,但是在执行过程中任务队列中有一个操作耗时3秒,那么定时器就会在3秒后才能执行下一次,导致计时不准确。

  2. 另一个原因是浏览器对于后台标签页的优化处理。当浏览器标签页不在前台时,为了节省CPU的计算资源,浏览器可能会暂停或降低背景任务的频率。这同样可能导致计时器不准确。例如,当用户切换到其他标签页,原本每隔1秒执行一次的计时器可能被浏览器暂停,等到用户再次切换回来时,计时器才恢复执行。

settimeout差值补偿

/**
* 使用 setTimeout 进行计时,每次计时都会用系统时间修复时间差
*/
   function timer_setTimeout() {
     const speed = 1000; // 设置定时器的间隔速度为1000毫秒(1秒)
     let countTime = 0; // 初始化计时器计数变量
     let start = new Date().getTime(); // 记录计时开始时的时间戳

     // 定义计时器的执行函数
     function run() {
       countTime++; // 每次执行时递增计时器的计数

       // 计算按照计时器当前速度实际经过的时间(countTime * 速度)
       let realTime = (countTime * speed);

       // 计算从计时开始到现在系统经过的时间
       let sysTime = (Date.now() - start);

       // 计算实际时间和理想时间之间的差异
       let patch = (sysTime - realTime);

       // 使用系统时间进行修复,调整下一次setTimeout的延迟时间
       // 通过设置speed - diff,尝试校正setTimeout的延迟,以补偿偏差
       window.setTimeout(run, (speed - patch));

       // 更新页面元素timeoutDom的文本内容,显示当前计时器的值
       timeoutDom.innerText = `setTimeout: ${countTime}`;
     }

     // 启动计时器,初始调用run函数,并设置延迟为speed
     window.setTimeout(run, speed);
   }

// 调用函数,创建并启动setTimeout计时器
timer_setTimeout();

通过计算实际与预期的时间差,尝试修复时间偏差,以实现更准确的计时间隔。然而,这种方法可能不会在所有情况下都有效,如果回调函数 run 执行的时间过长,或者主线程被严重阻塞,那么计时器的精度仍然可能会受到影响。而且且当tab标签处于后台工作时,回调队列会被优化暂停执行,tab页切换回前台后会立即执行之前所有的回调队列。

所以这种方式依然会存在上面提到的两个问题...

requestAnimationFrame计时器

// 定义一个基于 requestAnimationFrame 的计时器函数
function timer_raf() {
 let delay = 1000; // 设置计时器的时间间隔为1000毫秒(1秒)
 let countTime = 0; // 初始化计时器计数变量
 let startTime = Date.now(); // 记录计时器开始的初始时间点

 // 定义计时器回调函数,用于更新DOM元素显示的计时数值
 const cb = () => {
   rafDom.innerText = `raf: ${++countTime}`; // 每次调用时递增countTime并更新到页面元素
 };

 // 定义循环函数,用于控制requestAnimationFrame的执行
 function loop() {
   const now = Date.now(); // 获取当前时间
   // 检查当前时间与计时器开始时间的差值是否大于或等于设定的时间间隔
   if (now - startTime >= delay) {
     cb(); // 如果是,则执行回调函数更新计时器
     startTime = now; // 重置计时器的开始时间为当前时间,以便下一次计时
   }
   requestAnimationFrame(loop); // 请求浏览器在下一可用帧中执行loop函数
 }

 // 启动循环,开始计时器
 loop();
}

// 调用函数,创建计时器
timer_raf();

这种计时器的优点是与浏览器的刷新率同步。然而,它的精度受到浏览器刷新率和js运行时性能的影响,可能在不同设备的不同刷新率下表现不同,在js长任务下导致不准确。因此并不能实现准确的计时器效果...

Web Worker方法

时间系列二:高精度计时器方案,worker计时,settimeout差值补偿,raf计时器,setInterval

/**
 * 使用 Web Worker 进行计时
 */
function timer_worker() {
  // 创建一个Blob对象,用于生成一个可以在Web Worker中执行的JavaScript代码URL
  const blob = new Blob([
    `let countTime = 0; // 初始化计数器变量
    self.setInterval(() => { // 在Web Worker的上下文中设置一个定时器
      countTime++; // 每次定时器触发时递增计数器
      self.postMessage(countTime); // 使用postMessage方法发送计数器的值到主线程
    }, 1000);` // 定时器的时间间隔设置为1秒
  ], { type: 'application/javascript' }); // 指定Blob的内容类型为JavaScript

  // 使用URL.createObjectURL方法创建一个可以被Web Worker使用的URL
  const worker = new Worker(URL.createObjectURL(blob));

  // 设置Web Worker的onmessage事件处理器
  // 当Web Worker使用postMessage发送消息时,该处理器会被触发
  worker.onmessage = ev => {
    // 更新DOM元素workerDom的文本内容,显示从Web Worker接收到的计时器值
    workerDom.innerText = `worker: ${ev.data}`;
  };
}

// 调用函数,创建并启动Web Worker计时器
timer_worker();

timer_worker 函数使用了HTML5的Web Workers API来在后台线程执行计时任务.

  • 首先,使用 Blob 对象构造一个包含JavaScript代码的Blob URL,这段代码定义了一个在Web Worker内部运行的计时器,每秒递增计数器并通过 postMessage 发送当前的计时值到主线程。
  • 然后,使用 new Worker(URL.createObjectURL(blob)) 创建一个新的 Worker 实例,它将执行上面通过Blob URL指定的JavaScript代码。
  • 通过设置 worker.onmessage 事件处理器,当Web Worker通过 postMessage 发送消息时,主线程可以接收这个消息,并更新页面元素 workerDom 的文本内容来显示计时器的当前值。

主线程 & worker线程彼此独立运行,主线程耗时任务导致阻塞并不会导致worker的计时不准确,可以有效的解决js单线程运行导致的问题1。 并且当tab被切换到后台worker仍然可以运行,所以也可以解决问题2. 是比较好的解决计时器不准确的方案。

结论

在对 setIntervalsetTimeout 差值补偿、requestAnimationFrame,以及 Web Worker。 这四种计时器方案的比较分析中。 我们可以得出以下结论:

运行在主线程中的计时器方案,如传统的 setInterval 和基于 setTimeout的差值补偿方法 ,尽管易于实现,但它们容易受到JavaScript单线程任务执行特性的影响,特别是在执行长时间运行的任务时,可能导致计时不准确。此外,这些方案还可能受到浏览器标签页切换时的性能限制。

相比之下,requestAnimationFrame 主要适用于动画帧同步,虽然与浏览器刷新率同步,但同样受限于主线程。

Web Worker 提供了一个在主线程之外独立运行的计时解决方案,它能有效避免因主线程任务阻塞导致的计时不准确问题,并且不受浏览器性能限制的影响,是目前实现精确计时任务的推荐方案

完整代码

github.com/wuyunqiang/…

setTimeout vs setInterval

setInterval 回调函数的执行频率由浏览器控制 每过一定时间向任务队列推入一次回调 如果主线程长时间运行时 可能会出现 回调任务没有被及时执行 引发任务堆积 然后在短时间内调用多次的情况。而通过递归setTimeout实现的计时器 ,每次 setTimeout 回调执行结束后,再设置下一次的 setTimeout,这样就能保证任务之间的执行顺序和时间间隔,确保了回调函数的执行频率是完全受我们控制的,不会出现任务堆积的情况。

建议:

永远不要使用setInterval,可以使用setTimeout模拟setInterval

转载自:https://juejin.cn/post/7391745629876781056
评论
请登录