分析
我们知道,setTimeOut和setInterVal中时间参数并不是到点就立即执行,而是到点将其回调事件加入事件队列中。按照队列先进先出的性质,该回调事件到点之后是否能执行取决于是否属于队列首位,如果前头还有其他事件在等待,则不能按点执行。这并是导致事件等待执行时间出现误差的原因。
计时器
JS 中最常用的计时器有两种,分别是 setInterval 和 setTimeout 。下面来分析一下两种计时器 setInterval 和 setTimeout 差别。
相同点
1、setTimeout 和 setInterval 都属于宏任务;
2、均在指定的时间间隔后,调用函数或计算表达式;
不同点
1、setTimeout 在指定的时间后执行一次就停止了;而 setInterval 是会一直循环的执行下去;
2、setInterval 每执行一次就会将对应的函数丢到任务队列中。若任务队列中存在同样的任务,也就是说上一次的任务尚未执行,则当前任务会被忽略掉,而不会丢进任务队列当中;
解决方案
上面已经分析过了,使用 setInterval 的问题是没有办法保证函数每次都在指定的时间间隔后去执行,所以如何调整函数的执行时间,使其能够在预期的时间间隔后执行,就成了问题的关键。
这里通常的方案是通过 setTimeout + 递归的思路去解决,通过递归实现和 setInterval 一样的无限循环执行的效果,通过 setTimeout 去实现指定时间间隔后执行代码的效果,但是这个时间间隔却不能是固定的,不然会有和 setInterval 类似的误差问题。这里需要在每次执行的时候,通过计算实际的执行时间和理论上应该执行的时间做对比,计算出误差,并减去对应的误差,使得实际的实行时间和理论的执行时间保持一致。
// 指定的时间间隔
const interval = 1000;
// 倒计时剩余的时间(毫秒)
let ms = 1000000;
// 执行次数
let count = 0;
// 记录开始时的时间戳
const start = new Date().getTime();
// 用于计时器赋值,便于清空
let timeCounter;
// 首次执行
if (ms >= 0) {
timeCounter = setTimeout(countDownStart, interval);
}
function countDownStart() {
// 每次执行函数,count进行累加,便于计算时间间隔
count++;
// 计算误差 = 当前实际执行函数的时间 - 理论上应该执行的时间
const offset = new Date().getTime() - (start + count * interval);
// 那么下一次 setTimeout 的执行时间 = 指定的时间间隔 - 误差时间
const next = interval - offset;
if (next < 0) {
next = 0;
}
// 计算倒计时剩余的时间
ms -= interval;
console.log(
`误差:${offset} ms,下一次执行:${next} ms 后,离活动开始还有:${ms} ms`
);
if (ms < 0) {
// 如果倒计时结束,则清空计时器
clearTimeout(timeCounter);
} else {
// 如果倒计时未结束,则进行递归继续倒计时
timeCounter = setTimeout(countDownStart, next);
}
}