JavaScript实现精准倒计时

当需要实现精准倒计时,但setTimerout和setInterval并不理想时,或许你可以看看这篇文章!

「精准」倒计时

需求

  • 购物网站需要秒杀倒计时
  • 抢购倒计时
  • 倒计时抢红包

精确计时的需求

JavaScript实现技能功能自然会想到以下两个方法

1
2
setTimeout()
setInterval()

存在问题

用以上方法实现后发现,两个手机打开同一个活动页面后倒计时的时间不同,可能存在几秒甚至几分钟的误差。

前几天在抢薛之谦演唱会的票时就发现和朋友的时间不一致(可能这就是导致我没抢到票的原因吧哈哈哈)

可能原因

1.读取的是客户端时间,客户端时间可以随时调整,所以与服务器时间可能不一致。

  • 运行时间长了,也会造成不一致
1
2
3
4
// 以下代码并不一定在1s后执行
setTimeout(()=>{
console.log("timeout")
},1000);
1
2
3
4
5
6
7
// 例1
setTimeout(()=>{
console.log("timeout")
},1000);
for(let i = 0;i<1e10;i++){
console.log(i)
}
1
2
3
4
5
6
7
8
9
// 例2
var start = new Date().getTime();
var count = 0;

//定时器测试
setInterval(function(){
count++;
console.log( new Date().getTime() - (start + count * 1000));
},1000);
  • 预期的输出都是0,因为每一秒触发一次,每次+1s
  • 如果再加上一个定时器执行其他任务的话,这个就会更不准

1425095227_64_w308_h208

timeout并不一定在1s后输出

  • JS是单线程的
  • 只有主线程的任务执行完后才会去轮询任务队列
    • 主线程 > 微任务 > 宏任务
    • setTimeout属于宏任务
  • 只有for执行完主线程没有其他任务时才会去轮询任务队列,任务队列还是遵循先进先出的。
  1. 没有考虑js冻结运行耗费时间;(特别是移动端容易出现,下滑页面时倒计时不动了)

  2. 没有考虑页面渲染和函数运行累积时间;

  3. 其他代码逻辑问题;

计时器原理

参考前端界大牛John.Resig (jQuery作者) 写的Javascript计时器工作原理 — 《How JavaScript Timers Work》

JavaScript是单线程的,必须按队列先后顺序执行

  • 随着多核CPU的出现,为了充分发挥计算机算力,出现了Web Worker,为JavaScript创造多线程环境,运行主线程创建Worker线程,将一些任务分给Woker线程运行,两个线程互不干扰,Worker线程执行完后将结果返回给主线程
  • 参考

下图需要垂直往下看

  • 左边10、20、30、40、50是时间轴,单位为ms
  • 右边Timer是setTimeout、Interval是setInterval 前面的毫秒数是设置的时间
    • Fires为触发
  • 蓝色块为执行该代码块需要的时间
  • 第一个代码块约执行18ms、第二个代码块约执行11ms、….
  • js是单线程,同一时间只能执行一个js代码
    • 当异步事件发生后,回放入执行队列,线程空闲后才会依次执行该队列

img

第一个模块初始化了两个定时器,第一个定时器在10ms后触发,此时第一个模块还没执行完成,所以该定时器并不会立即执行,会将其回调函数放入执行队列等待执行

因为Mouse Click CallBack比Timer先触发,所以会先执行Mouse Click CallBack的回调函数在执行Timer(如图)

第二次触发Interval的时候,第一次触发的Interval并未执行,此时并不管,直接把回调加入队列,所以导致下面两个Interval执行中间没有时间间隔

解决思路

  1. 客户端http请求服务器时间;

  2. 服务器响应完成;

  3. 服务器通过网络传输时间数据到客户端;

  4. 客户端根据活动开始时间和服务器时间差做倒计时显示;

网络也有误差

当前服务器时间 = 服务器系统返回时间 + 网络传输时间 + 前端渲染时间 + 常量(可选)

计算误差,然后减去误差时间得到等待下次执行时间

  • 例如每隔1s执行一次,阻塞了1.5s,所以误差有0.5s,就等待0.5s后触发
    • 这个0.5s的误差可以计算出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//进行线程占用
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);
}
}

1425117036_60_w420_h263

以上实现可能导致倒计时忽快忽慢

  • 例如可以每次减少20ms,慢慢靠近真值,不让用户发觉倒计时变快了。

部分代码图片来自