倒计时时间偏差问题解析

118 阅读3分钟

分析

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

计时器

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);
  }
}