(转)JS实现活动精确倒计时

背景

前端页面倒计时功能在很多场景中会用到,如运营活动开始倒计时和活动结束倒计时,又如购物网站的秒杀倒计时,抢购倒计时,还有我们手Q春节抢红包倒计时等等……. 最近的话费代付项目中,也涉及倒计时功能,但在开发过程中遇到一些麻烦和坑点,下面和大家分享一下最后是如何解决的。

坑点

手Q春节抢明星红包活动,就有产品吐槽两个手机在不同时间点打开同一个活动显示的开抢倒计时不一样,误差大的甚至相差几分钟,导致某些用户在活动显示还未开始红包就已被抢完了。为什么误差会这么大呢?

对于这个问题,前台开发同学一般会猜测是这个原因:倒计时读取了客户端时间造成的,因为客户端时间和服务端时间有误差,应该读取服务器时间。

是的,倒计时不应该读取客户端时间,客户端时间用户可以随时调整,会造成不一致,应读取服务器返回时间。但实践证明,做了这一步还未够,页面运行时间长了,新开的页面和原打开页面还是存在误差。

京东团购也存在这个问题:

造成误差的原因主要有几种可能:

1. 没有考虑js冻结运行耗费时间;(特别是移动端容易出现,下滑页面时倒计时不动了)

2. 没有考虑页面渲染和函数运行累积时间;(京东的误差貌似属于这种)

3. 其他代码逻辑问题(这种情况就复杂了,这里不讨论);

计时器原理

倒计时功能离不开setTimeout或setInterval这两个函数,要用好这两个函数必先了解好Javascript解释器的工作原理,前端界大牛John.Resig (jQuery作者) 有篇文章很好讲解了Javascript解释器工作原理 — 《How JavaScript Timers Work》

前端开发同学都知道,javascript是单线程的(web worker除外),更好理解的解释是javascript解释器是单线程工作,它不能在处理一个ajax的callback的同时去处理click event的callback,而是必须按照先后队列顺序执行。

这图包含信息量很大,这里按照自己的理解描述一下:

这图从上往下看,垂直方向是时间,以ms为单位,蓝色模块是执行代码所占的时间段,如第一个代码模块执行js占用了约18ms, 第二个模块执行js占用了约11ms,其他模块类似。由于js是单线程执行,同一时间只能执行一个js代码(同一时间其他异步事件执行会被阻塞 ) , 当异步事件发生时,它会进入代码执行队列,执行线程空闲时依照队列顺序依次执行代码。

第一个模块初始化了两个定时器,一个10ms延迟的setTimeout和10ms的setInterval。这些定时器可能会在我们第一个代码块执行结束之前就触发,这取决于定时器在第一个代码块中启动的位置和时间。注意,定时器虽然触发了,但是并不会立即执行,它只是把需要延迟执行的函数按时间先后加入了执行队列,在线程的某一个空闲的时间点,这个函数就能够得到执行。

按照第一个模块事件触发的顺序(Mouse Click Occurs -. 10ms Timer Fires),第一个模块代码执行结束后,按照队列中等待的先后顺序执行事件,先执行Mouse Click CallBack再执行Timer。在执行Mouse Click CallBack模块时,Interval第一次触发未执行加入队列。在执行Timer模块时,Interval第二次触发未执行加入队列。待Mouse Click CallBack和Timer模块都执行完毕后,再依次执行队列中已触发的Interval事件。后面模块由于没有阻塞的事件了,所以按照既定10ms执行Interval事件。

倒计时问题

如果上面Javascript计时器原理理解了,就很好明白倒计时功能存在问题的隐患。

先看一段测试代码:

var  start  =  new  Date().getTime();

var  count  =  0;

//定时器测试

setInterval(function(){

 count++;

 console.log(  new  Date().getTime()  -  (start  +  count *  1000));

},1000);

目测代码就知道运行结果,定时器每秒执行一次,每次输出应该是0 。

实际输出:

结论:由于代码执行占用时间和其他事件阻塞原因,导致有些事件执行延迟了几ms,但影响很微。

下面加一段阻塞代码看看:


var  start  =  new  Date().getTime();

var  count  =  0;

//占用线程事件

setInterval(function(){

 var  j  =  0;

 while(j++  <  100000000);

},  0);

//定时器测试

setInterval(function(){

 count++;

 console.log(  new  Date().getTime()  -  (start  +  count *  1000));

},1000);

实际输出:

结论:由于加了很占线程的阻塞事件,导致定时器事件每次执行延迟越来越严重。

由于实际项目中,执行计时器的同时,会有很多其他异步阻塞事件,会导致倒计时功能不精确。

解决思路

这里先分析一下从获取服务器时间到前端显示倒计时的过程:

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

2. 服务器响应完成;

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

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

服务器响应完成的时间其实就是服务器时间,但经过网络传输这一步,就会产生误差了,误差大小视网络环境而异,这部分时间前端也没有什么好办法计算出来,一般是几十ms以内,大的可能有几百ms。

可以得出:当前服务器时间 = 服务器系统返回时间 + 网络传输时间 + 前端渲染时间 + 常量(可选),这里重点是说要考虑前端渲染的时间,避免不同浏览器渲染快慢差异造成明显的时间不同步,这是第一点。(网络传输时间忽略或加个常量呗)

获得服务器时间后,前端进入倒计时计算和计时器显示,这步就要考虑js代码冻结和线程阻塞造成计时器延时问题了,我的思路是通过引入计数器,判断计时器延迟执行的时间来调整,尽量让误差缩小,不同浏览器不同时间段打开页面倒计时误差可控制在1s以内

关键实现代码如下:

//继续线程占用

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执行时间一致。若冻结时间特别长的,还要做特殊处理。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容