(轉(zhuǎn))JS實(shí)現(xiàn)活動(dòng)精確倒計(jì)時(shí)

背景

前端頁面倒計(jì)時(shí)功能在很多場(chǎng)景中會(huì)用到,如運(yùn)營(yíng)活動(dòng)開始倒計(jì)時(shí)和活動(dòng)結(jié)束倒計(jì)時(shí),又如購物網(wǎng)站的秒殺倒計(jì)時(shí),搶購倒計(jì)時(shí),還有我們手Q春節(jié)搶紅包倒計(jì)時(shí)等等……. 最近的話費(fèi)代付項(xiàng)目中,也涉及倒計(jì)時(shí)功能,但在開發(fā)過程中遇到一些麻煩和坑點(diǎn),下面和大家分享一下最后是如何解決的。

坑點(diǎn)

手Q春節(jié)搶明星紅包活動(dòng),就有產(chǎn)品吐槽兩個(gè)手機(jī)在不同時(shí)間點(diǎn)打開同一個(gè)活動(dòng)顯示的開搶倒計(jì)時(shí)不一樣,誤差大的甚至相差幾分鐘,導(dǎo)致某些用戶在活動(dòng)顯示還未開始紅包就已被搶完了。為什么誤差會(huì)這么大呢?

對(duì)于這個(gè)問題,前臺(tái)開發(fā)同學(xué)一般會(huì)猜測(cè)是這個(gè)原因:倒計(jì)時(shí)讀取了客戶端時(shí)間造成的,因?yàn)榭蛻舳藭r(shí)間和服務(wù)端時(shí)間有誤差,應(yīng)該讀取服務(wù)器時(shí)間。

是的,倒計(jì)時(shí)不應(yīng)該讀取客戶端時(shí)間,客戶端時(shí)間用戶可以隨時(shí)調(diào)整,會(huì)造成不一致,應(yīng)讀取服務(wù)器返回時(shí)間。但實(shí)踐證明,做了這一步還未夠,頁面運(yùn)行時(shí)間長(zhǎng)了,新開的頁面和原打開頁面還是存在誤差。

京東團(tuán)購也存在這個(gè)問題:

造成誤差的原因主要有幾種可能:

1. 沒有考慮js凍結(jié)運(yùn)行耗費(fèi)時(shí)間;(特別是移動(dòng)端容易出現(xiàn),下滑頁面時(shí)倒計(jì)時(shí)不動(dòng)了)

2. 沒有考慮頁面渲染和函數(shù)運(yùn)行累積時(shí)間;(京東的誤差貌似屬于這種)

3. 其他代碼邏輯問題(這種情況就復(fù)雜了,這里不討論);

計(jì)時(shí)器原理

倒計(jì)時(shí)功能離不開setTimeout或setInterval這兩個(gè)函數(shù),要用好這兩個(gè)函數(shù)必先了解好Javascript解釋器的工作原理,前端界大牛John.Resig (jQuery作者) 有篇文章很好講解了Javascript解釋器工作原理 — 《How JavaScript Timers Work》

前端開發(fā)同學(xué)都知道,javascript是單線程的(web worker除外),更好理解的解釋是javascript解釋器是單線程工作,它不能在處理一個(gè)ajax的callback的同時(shí)去處理click event的callback,而是必須按照先后隊(duì)列順序執(zhí)行。

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

這圖從上往下看,垂直方向是時(shí)間,以ms為單位,藍(lán)色模塊是執(zhí)行代碼所占的時(shí)間段,如第一個(gè)代碼模塊執(zhí)行js占用了約18ms, 第二個(gè)模塊執(zhí)行js占用了約11ms,其他模塊類似。由于js是單線程執(zhí)行,同一時(shí)間只能執(zhí)行一個(gè)js代碼(同一時(shí)間其他異步事件執(zhí)行會(huì)被阻塞 ) , 當(dāng)異步事件發(fā)生時(shí),它會(huì)進(jìn)入代碼執(zhí)行隊(duì)列,執(zhí)行線程空閑時(shí)依照隊(duì)列順序依次執(zhí)行代碼。

第一個(gè)模塊初始化了兩個(gè)定時(shí)器,一個(gè)10ms延遲的setTimeout和10ms的setInterval。這些定時(shí)器可能會(huì)在我們第一個(gè)代碼塊執(zhí)行結(jié)束之前就觸發(fā),這取決于定時(shí)器在第一個(gè)代碼塊中啟動(dòng)的位置和時(shí)間。注意,定時(shí)器雖然觸發(fā)了,但是并不會(huì)立即執(zhí)行,它只是把需要延遲執(zhí)行的函數(shù)按時(shí)間先后加入了執(zhí)行隊(duì)列,在線程的某一個(gè)空閑的時(shí)間點(diǎn),這個(gè)函數(shù)就能夠得到執(zhí)行。

按照第一個(gè)模塊事件觸發(fā)的順序(Mouse Click Occurs -. 10ms Timer Fires),第一個(gè)模塊代碼執(zhí)行結(jié)束后,按照隊(duì)列中等待的先后順序執(zhí)行事件,先執(zhí)行Mouse Click CallBack再執(zhí)行Timer。在執(zhí)行Mouse Click CallBack模塊時(shí),Interval第一次觸發(fā)未執(zhí)行加入隊(duì)列。在執(zhí)行Timer模塊時(shí),Interval第二次觸發(fā)未執(zhí)行加入隊(duì)列。待Mouse Click CallBack和Timer模塊都執(zhí)行完畢后,再依次執(zhí)行隊(duì)列中已觸發(fā)的Interval事件。后面模塊由于沒有阻塞的事件了,所以按照既定10ms執(zhí)行Interval事件。

倒計(jì)時(shí)問題

如果上面Javascript計(jì)時(shí)器原理理解了,就很好明白倒計(jì)時(shí)功能存在問題的隱患。

先看一段測(cè)試代碼:

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

var  count  =  0;

//定時(shí)器測(cè)試

setInterval(function(){

 count++;

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

},1000);

目測(cè)代碼就知道運(yùn)行結(jié)果,定時(shí)器每秒執(zhí)行一次,每次輸出應(yīng)該是0 。

實(shí)際輸出:

結(jié)論:由于代碼執(zhí)行占用時(shí)間和其他事件阻塞原因,導(dǎo)致有些事件執(zhí)行延遲了幾ms,但影響很微。

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


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

var  count  =  0;

//占用線程事件

setInterval(function(){

 var  j  =  0;

 while(j++  <  100000000);

},  0);

//定時(shí)器測(cè)試

setInterval(function(){

 count++;

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

},1000);

實(shí)際輸出:

結(jié)論:由于加了很占線程的阻塞事件,導(dǎo)致定時(shí)器事件每次執(zhí)行延遲越來越嚴(yán)重。

由于實(shí)際項(xiàng)目中,執(zhí)行計(jì)時(shí)器的同時(shí),會(huì)有很多其他異步阻塞事件,會(huì)導(dǎo)致倒計(jì)時(shí)功能不精確。

解決思路

這里先分析一下從獲取服務(wù)器時(shí)間到前端顯示倒計(jì)時(shí)的過程:

1. 客戶端http請(qǐng)求服務(wù)器時(shí)間;

2. 服務(wù)器響應(yīng)完成;

3. 服務(wù)器通過網(wǎng)絡(luò)傳輸時(shí)間數(shù)據(jù)到客戶端;

4. 客戶端根據(jù)活動(dòng)開始時(shí)間和服務(wù)器時(shí)間差做倒計(jì)時(shí)顯示;

服務(wù)器響應(yīng)完成的時(shí)間其實(shí)就是服務(wù)器時(shí)間,但經(jīng)過網(wǎng)絡(luò)傳輸這一步,就會(huì)產(chǎn)生誤差了,誤差大小視網(wǎng)絡(luò)環(huán)境而異,這部分時(shí)間前端也沒有什么好辦法計(jì)算出來,一般是幾十ms以內(nèi),大的可能有幾百ms。

可以得出:當(dāng)前服務(wù)器時(shí)間 = 服務(wù)器系統(tǒng)返回時(shí)間 + 網(wǎng)絡(luò)傳輸時(shí)間 + 前端渲染時(shí)間 + 常量(可選),這里重點(diǎn)是說要考慮前端渲染的時(shí)間,避免不同瀏覽器渲染快慢差異造成明顯的時(shí)間不同步,這是第一點(diǎn)。(網(wǎng)絡(luò)傳輸時(shí)間忽略或加個(gè)常量唄)

獲得服務(wù)器時(shí)間后,前端進(jìn)入倒計(jì)時(shí)計(jì)算和計(jì)時(shí)器顯示,這步就要考慮js代碼凍結(jié)和線程阻塞造成計(jì)時(shí)器延時(shí)問題了,我的思路是通過引入計(jì)數(shù)器,判斷計(jì)時(shí)器延遲執(zhí)行的時(shí)間來調(diào)整,盡量讓誤差縮小,不同瀏覽器不同時(shí)間段打開頁面倒計(jì)時(shí)誤差可控制在1s以內(nèi)

關(guān)鍵實(shí)現(xiàn)代碼如下:

//繼續(xù)線程占用

setInterval(function(){

 var  j  =  0;

 while(j++  <  100000000);

},  0);

//倒計(jì)時(shí)

var  interval  =  1000,

 ms  =  50000,  //從服務(wù)器和活動(dòng)開始時(shí)間計(jì)算出的時(shí)間差,這里測(cè)試用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,下一次執(zhí)行:"  +  nextTime  +  "ms后,離活動(dòng)開始還有:"  +  ms  +  "ms");

 if(ms  <  0){

              clearTimeout(timeCounter);

 }else{

              timeCounter  =  setTimeout(countDownStart,nextTime);

 }

}

運(yùn)行結(jié)果:

結(jié)論:由于線程阻塞延遲問題,做了setTimeout執(zhí)行時(shí)間的誤差修正,保證setTimeout執(zhí)行時(shí)間一致。若凍結(jié)時(shí)間特別長(zhǎng)的,還要做特殊處理。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容