关于倒计时的小误差。

572 阅读2分钟

背景

 前端页面倒计时功能在很多场景中会用到,如广告页面的倒计时,活动倒计时,最常见的是手机获取验证码后,1min/30s的倒计时。

传统的实现方式

example 1
  var timer;
  var timer_div = $('#timer_div');  
  var second = 10; // 倒计时时间为 10 s
  var start = new Date().getTime();  //获取当前时间
  var count = 0;  

  clearInterval(timer);
  timer = setInterval(showTime, 1000);
  
  function showTime() {     
    if (second === 0) { 
        ... // 业务代码
        clearInterval(timer);   
        return false;
    }
    count++; 
    console.log(new Date().getTime() - (start + count * 1000)); // 按我们正常的预期,是想着定时器每秒执行一次,每次输出应该是0 。
    timer_div.html('<div>' + second + 's</div>');
    second--;
  }
结果

可以很明显看到,这里输出的并不是我们预期的结果。接下来我们来看来一个较极端的例子
example2
function runForSeconds(s) {  
    var start = +new Date();  
    while (start + s * 1000 > (+new Date())) {}
}

document.body.addEventListener("click", function () {  
    runForSeconds(10);
}, false);

setTimeout(function () {  
    console.log("Done!");
}, 1000 * 3);
结果
    3秒内点击 body 后
    
    以为是这样:
    |----1s----|----2s----|----3s----|--->console.log("Done!")--->|----10s-----|
    其实是这样:
    |----1s----|----2s----|--------10s--------|--->console.log("Done!");
结论
由于代码执行占用时间或是假如在执行定时器的过程中有同步UI事件的代码,同步代码会立即执行,该优先执行的代码快的耗时将导致倒计时往往误差非常大。(实际上在移动端的滚动页面中是有可能出现这种情况的)
优化后的方案
// 线程占用
setInterval(function () { 
  var j = 0; 
  while(j++ < 100000000); 
}, 0); 
 
//倒计时
var interval = 1000,
    ms = 50000,  // 倒计时长 50000ms
    count = 0,
    startTime = new Date().getTime();
if (ms >= 0) {  
    var timeCounter = setTimeout(countDownStart, interval);       
} 
  
function countDownStart() {
  count++;  
  var offset = new Date().getTime() - (startTime + count * interval);  
  var nextTime = interval - offset;  
  var daytohour = 0; 

  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执行时间的误差修正,保证 setTimeout 执行时间一致。若冻结时间特别长的,还要做特殊处理。
tip:以上是在学习了前辈的分享后做的一点小记录,有兴趣的可以交流交流。在此感谢wjq前辈