问题描述
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,再跑一遍代码,出现的时间又一样:
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同样会出现时间偏差的问题:
问题分析
分析上问题的出现,需要充分理解JS的事件循环机制。
不理解的同学可以参考这篇文章:做一些动图,学习一下EventLoop
我们知道,setTimeOut和setInterVal中时间参数并不是到点就立即执行,而是到点将其回调事件加入事件队列中。按照队列先进先出的性质,该回调事件到点之后是否能执行取决于是否属于队列首位,如果前头还有其他事件在等待,则不能按点执行。这并是导致事件等待执行时间出现误差的原因。
在《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 处的公式可计算出当前执行倒计时的时间与实际应执行时间的偏差,进而可以计算出下次执行倒计时的时间。
参考文献
- 《JavaScript高级程序设计(第三版)》
- JavaScript 前端倒计时纠偏实现