setTimeOut/setInterval出现时间偏差问题原因及解决方案【每日一问20210829】

3,629 阅读2分钟

问题描述

1. 用setInterval实现计时

const startTime = new Date().getTime();
let count = 0;
const interval = setInterval(function () {
    count++
    console.log(new Date().getTime() - (startTime + count * 1000) + 'ms')
    if(count == 10){
    clearInterval(interval);
    }
}, 1000)

new Date().getTime() - (startTime + count * 1000) 理想情况应该是0ms,但是我们可以看到在控制台中并不都是0ms,再跑一遍代码,出现的时间又一样:

截屏2021-08-29 00.41.56.png

2. 用setTimeout实现计时

const startTime = new Date().getTime(),interval = 1000;
let  count = 0
let timer = setTimeout(doFunc,interval);
function doFunc(){
    count++
    console.log(new Date().getTime() - (startTime + count * 1000) + 'ms');
  if(count < 10){
	    timer = setTimeout(doFunc,interval);
    }
}

使用setTimeOut同样会出现时间偏差的问题:

截屏2021-08-29 15.55.17.png

问题分析

分析上问题的出现,需要充分理解JS的事件循环机制。

不理解的同学可以参考这篇文章:做一些动图,学习一下EventLoop

我们知道,setTimeOutsetInterVal中时间参数并不是到点就立即执行,而是到点将其回调事件加入事件队列中。按照队列先进先出的性质,该回调事件到点之后是否能执行取决于是否属于队列首位,如果前头还有其他事件在等待,则不能按点执行。这并是导致事件等待执行时间出现误差的原因。

在《JavaScript高级程序设计(第三版)》22.3中,有描述:

定时器对队列的工作方式是,当特定时间过去后将代码插入。注意,给队列添加代码并不意味着对它立刻执行,而只能表示它会尽快执行。设定一个 150ms 后执行的定时器不代表到了 150ms 代码就立刻执行,它表示代码会在 150ms 后被加入到队列中。如果在这个时间点上,队列中没有其他东西,那么这段代码就会被执行。

解决方案

既然有时间偏差问题,我们想到的是能不能通过动态计算时间偏差值,并动态调整执行setTimeout的间隔,以尽量调整整体的时间偏差。

比如定时每隔1000ms倒计时,首次运行时候时间偏差2ms,那么下一次执行时候就把时间参数提早2ms,也就是998ms,如果下次还出现偏差,继续调整……如此往复,并不能保证每一次间隔都相同,但能在整体上减少时间偏差。

代码如下:

const interval = 1000
let ms = 50000,  // 从服务器和活动开始时间计算出的时间差,这里测试用 50000 ms
let count = 0
const startTime = new Date().getTime()
let timeCounter
if( ms >= 0) {
  timeCounter = setTimeout(countDownStart, interval)
}
 
function countDownStart () {
   count++
   const offset = new Date().getTime() - (startTime + count * interval) // A
   let nextTime = interval - offset
   if (nextTime < 0) { 
       nextTime = 0 
   }
   ms -= interval
   console.log(`误差:${offset} ms,下一次执行:${nextTime} ms 后,离活动开始还有:${ms} ms`)
   if (ms < 0) {
     clearTimeout(timeCounter)
   } else {
     timeCounter = setTimeout(countDownStart, nextTime)
   }
 }

代码的基本原理并不复杂:通过递归调用 setTimeout 进行倒计时操作的执行。而每次执行函数时会维护一个 count 变量,用以记录已经执行过的倒计时次数,使用代码 A 处的公式可计算出当前执行倒计时的时间与实际应执行时间的偏差,进而可以计算出下次执行倒计时的时间。

参考文献