Skip to content
On this page

倒计时的精确度

先放代码,需要自定义 setTimeout

查看代码
js
/**
 * 
 * @param {number} timeStemp 倒计时时间撮
 * @param {number} delay 延时秒数
 * @param {number} init 是否自执行 默认 true
 * @param {Function} success 延时执行函数
 * @param {Function} complete 轮询执行完成后回调函数
 * 
 */
function CountdownUtil(options) {
  var _options = Object.assign({
    timeStemp,
    delay: 1000,
    init: true,
    success: null,
    complete: null
  }, options);

  var timeStemp = _options.timeStemp;
  var delay = _options.delay;

  var timer;
  var count = 0;
  var stop = true;
  var startTime;

  // 开始
  this.start = function() {
    stop = false;
    startTime = new Date().getTime();
    startCountDown(delay);
  }


  // 暂停
  this.stop = function() {
    if (!timer) {
      throw Error('当前定时器不存在');
    }
    stop = true;
  }

  // 清除倒计时
  this.clear = function() {
    stop = true;
    timer = null;
  }

  if (_options.init) {
    this.start();
  }

  var _this = this;

  function startCountDown(interval) {
    if (stop) {
      return;
    }
    timer = setTimeout(function() {
      clearTimeout(timer);
      
      // 倒计时还有多久结束
      var restTime = timeStemp - delay * count;
      var fomatTime = restTime;
      if (restTime < 0) {
        restTime = 0;
      }

      fomatTime = fomatTimeStemp(fomatTime);

      // 执行轮询回调
      _options.success && _options.success(Object.assign(fomatTime, { diff: restTime }));

      // 倒计时结束
      if (!restTime) {
        _this.clear();
        _options.complete && _options.complete();
        return;
      }

      count++;
      var endTime = new Date().getTime();

      // 当前差值 = 轮询的当前时间 - (最初时间 + 间隔时间 * 轮询次数)
      var diff = endTime - (startTime + delay * count);

      if (diff < 0) {
        diff = 0;
      }
      console.log(`diff: ${diff}`);
  
      startCountDown(delay - diff);
    }, interval);
  }

  function fomatTimeStemp(total) {
    const seconds = Math.floor( (total/1000) % 60 );
    const minutes = Math.floor( (total/1000/60) % 60 );
    const hours = Math.floor( (total/(1000*60*60)) % 24 );
    const days = Math.floor( total/(1000*60*60*24) );

    return {
      days,
      hours,
      minutes,
      seconds
    };
  }

  return this;

}

CountdownUtil.setTimeout = function(callback, num) {
  var _startTime = new Date().getTime();
  var _count = 0;
  var _timer;

  customSetTimeout(num);

  function customSetTimeout(interval) {
    _timer = setTimeout(function() {
      clearTimeout(_timer);
      
      // 执行轮询回调
      callback && callback();

      _count++;
      var _endTime = new Date().getTime();

      // 当前差值 = 轮询的当前时间 - (最初时间 + 间隔时间 * 轮询次数)
      var _diff = _endTime - (_startTime + num * _count);

      if (_diff < 0) {
        _diff = 0;
      }
      console.log(`diff: ${_diff}`);
      const next = num - _diff
      customSetTimeout(next === 0 ? num : next);
    }, interval);
  }
  return _timer;
}

CountdownUtil.clearTimeout = function(timer) {
  clearTimeout(timer);
}

浏览器的"休眠"

关于这一点处理,其实更多的跟业务相关,不同的业务可能有不一样的处理方式。

举个例子:

  • 网页实现一个 5 天的倒计时功能
  • 倒计时的剩余数通过请求获取,初始为432000(s),也就是 5 天,并且服务器端也会进行一个倒计时

前端其实只是负责一个倒计时UI的显示,能让用户感知到有这么一回事,真正倒计时的还是放在了服务器端。

前面看到了切换 tab,或者网页最小化时,有延迟,那么我们只要监听用户什么时候回到页面,这个时候再去请求服务器端最新的剩余时间,重新开始倒计时,修正造成的延迟。

利用 visibilitychange 事件,切换 Tab 页以及浏览器最小化时倒计时误差修正。

js
// 处理页面可见属性的改变
document.addEventListener('visibilityChange', () => {
    if (!document.hidden) {
      // 获取服务端新的倒计时
    }
});

任务队列

JavaScript 语言是单线程,同一时间只能做一件事。单线程就意味着所有任务需要排队,前一个任务结束,才会执行后一个任务,如果前一个任务耗时很长,后一个任务就不得不一直等着。

任务分为两种:

  • 同步任务:在主线程上排队执行的任务,只有前一个执行完毕,才会执行下一个

  • 异步任务:不进入主线程,而进入“任务队列”的任务,只有“任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程

事件循环:主线程从“任务队列”中读取事件,这个过程是循环不断的,所以整个的运行机制又称 Event Loop

为什么使用 setTimeout 实现倒计时,而不是 setInterval?

setInterval 指定的是“开始执行”之间的间隔,并不考虑每次任务执行本身所消耗的时间。因此实际上,两次执行之间的间隔会小于指定的时间。比如,setInterval 指定每 100ms 执行一次,每次执行需要 5ms,那么第一次执行结束后 95ms,第二次执行就会开始。如果某次执行耗时特别长,比如需要 105ms,那么它结束后,下一次执行就会立即开始。

为了确保两次执行之间有固定的间隔,可以不用 setInterval,而是每次执行结束后,使用 setTimeout 指定下一次执行的具体时间。

为什么 setTimeout 会出现误差

setTimeout 作为异步任务,在实现倒计时功能的时候,除了执行我们功能的实现代码,还会有主线程对任务队列的读取及执行等过程,这些过程也需要耗费一些时间,所以会因为 Event Loop 的机制出现些许误差。

相关DEMO:地址

积硅步,至千里。

公众号码不停指,欢迎关注。