如何寫好倒計時

引言

本文講解倒計時為什么建議使用setTimeout而不使用setInterval,倒計時為什么存在誤差,以及如何解決。

倒計時器

在前端開發(fā)中,倒計時器功能比較常見,比如活動倒計時,假定只有10秒,比較常見的兩種寫法如下:

 //setTimeout實現(xiàn)方式
 var countdownTime = 10; //倒計時秒數(shù)
 
 var countdown = function() {
     var setTimeoutHandler = setTimeout(function () {
         countdownTime -- ;
         console.log('倒計時:' + countdownTime + ' 秒');
 
         if(countdownTime === 0) {
                 console.log('倒計時結(jié)束!');
                 clearTimeout(setTimeoutHandler);
         }else {
             countdown();
         }
 
     }, 1000)
 };
 
 countdown();
 //setInterval實現(xiàn)方式
 var countdownTime = 10; //倒計時秒數(shù)
 
 var countdown = function() {
     var setIntervalHandler = setInterval(function () {
         countdownTime -- ;
         console.log('倒計時:' + countdownTime + ' 秒');
 
         if(countdownTime === 0) {
             console.log('倒計時結(jié)束!');
             clearInterval(setIntervalHandler);
         }
 
     }, 1000)
 };
 
 countdown();

控制臺打印都是一樣的:

控制臺打印信息

分析上面的兩種寫法,第一種使用setTimeout方式,countdown遞歸函數(shù)調(diào)用,第二種使用setInterval方式。

setInterval 方法可按照指定的周期(以毫秒計)來調(diào)用函數(shù)或計算表達(dá)式。

setTimeout 方法用于在指定的毫秒數(shù)后調(diào)用函數(shù)或計算表達(dá)式。

相信大家對這兩個函數(shù)的用法都是比較了解的,都可以實現(xiàn)倒計時功能,且setInterval函數(shù)的周期調(diào)用特性更符合倒計時的業(yè)務(wù)場景,但事實真的是這樣么?

setTimeout與setInterval

那么問題來了,是使用setTimeout還是setInterval,還是兩個都可以?

setInterval執(zhí)行機制

JavaScript高級程序設(shè)計(第三版)關(guān)于時間間隔描述:

設(shè)定一個 150ms 后執(zhí)行的定時器不代表到了 150ms 代碼就立刻執(zhí)行,它表示代碼會在 150ms 后被加入到隊列中。如果在這個時間點上,隊列中沒有其他東西,那么這段代碼就會被執(zhí)行。

帶著這段描述,我們設(shè)定執(zhí)行代碼setInterval(func, interval),func函數(shù)執(zhí)行時間為1s,interval時間間隔為0.5s,那么這段代碼的執(zhí)行流程圖如下:

代碼執(zhí)行流程

0s時,setInterval函數(shù)觸發(fā),等待0.5s后,func第1次加入到事件隊列中,并在0.5-1.5s期間執(zhí)行了1s。

因為時間間隔為0.5s,所以在1s時func第2次加入到隊列中,但此時JS引擎處理方式是:當(dāng)使用setInterval時,僅當(dāng)沒有該定時器的任何其他代碼實例時,才將定時器代碼添加到隊列中。 因為在1s時,第1次加入隊列的func還在執(zhí)行,所以無法成功將func加入隊列中,這就出現(xiàn)了丟幀現(xiàn)象。

時間又過了0.5s,在1.5s時,func第3次加入到隊列中,此時第1次加入到隊列中func剛執(zhí)行完畢,第3次func可成功加入到隊列中并開始執(zhí)行。此時暴露出setInterval另一個問題,兩次func執(zhí)行的時間間隔遠(yuǎn)小于0.5s,代碼的執(zhí)行間隔比設(shè)定的間隔要小

setTimeout執(zhí)行機制

那么同樣的功能,使用setTimeout又會是什么現(xiàn)象呢,代碼片段:

 setTimeout(function(){
     //do something
     //arguments.callee 獲取對當(dāng)前執(zhí)行的函數(shù)的引用,在ES5嚴(yán)格模式中已廢棄。
     setTimeout(arguments.callee, interval);
 },interval)

func函數(shù)執(zhí)行時間為1s,interval時間間隔為0.5s,代碼的執(zhí)行流程圖如下:

代碼執(zhí)行流程

0s時,setTimeout函數(shù)觸發(fā),等待0.5s后,func第1次加入到事件隊列中,并在0.5-1.5s期間執(zhí)行了1s。

1.5s時func執(zhí)行結(jié)束,第二個setTimeout函數(shù)被觸發(fā),等待0.5s后,func第2次加入到隊列中,并在2s - 2.5s期間執(zhí)行了1s。

兩次func執(zhí)行間隔與設(shè)定的interval 0.5s一致,且不會出現(xiàn)丟幀的現(xiàn)象。

如何選擇

通過setTimeoutsetInterval兩個函數(shù)的執(zhí)行機制來看,setInterval存在兩個問題:

  1. 丟幀,如果JS隊列中已經(jīng)有一個它的實例,就不會向隊列中添加事件,所以這次的事件執(zhí)行就會丟失。
  2. 兩次的事件執(zhí)行時間間隔變小甚至無間隔,當(dāng)前事件執(zhí)行完后,馬上就會執(zhí)行隊列中已添加的事件。

所以,使用setTimeout,而不使用setInterval。

倒計時誤差

倒計時器是存在誤差的,我們做個測試,一看便知:

 var countIndex = 1; //倒計時任務(wù)執(zhí)行次數(shù)
 const timeout = 1000; //時間間隔1秒
 const startTime = new Date().getTime();
 
 countdown(timeout);
 
 function countdown(interval) {
     setTimeout(function () {
         const endTime = new Date().getTime();
 
         //誤差
         const deviation = endTime - (startTime + countIndex * timeout);
         console.log('第'+ countIndex +'次:累計誤差 '+ deviation + ' ms');
 
         countIndex ++ ;
 
         //執(zhí)行下一次倒計時
         countdown(timeout);
     }, interval)
 }

控制臺打?。?/p>

控制臺打印信息

這段代碼的作用是,計算出每次定時器結(jié)束時間開始時間加上總輪詢的時間的差值,也就是累計的誤差??梢詮目刂婆_打印信息看出,平均每秒存在2ms的誤差值。雖然每次誤差值都不大,但是如果倒計時10分鐘,最后就會差1.2秒,這在搶購秒殺的業(yè)務(wù)場景下是致命的BUG了。

如果你將瀏覽器切換Tab或者最小化一段時間后,再切回打開控制臺看又會看到神奇的一幕:

控制臺打印信息

打印第5次瀏覽器最小化,第10次時瀏覽器恢復(fù),可以看到從第6次到第9次瀏覽器最小化期間,每次偏差值是1000ms左右,等第11次瀏覽器恢復(fù)后,每次偏差值又變回2ms左右。驚不驚喜,意不意外!

為什么會存在誤差

存在2ms的誤差是因為JS是單線程的,執(zhí)行了setTimeout中的代碼塊耗時2ms左右,例子中的代碼塊沒有復(fù)雜邏輯就花費了2ms,可想而知在實際業(yè)務(wù)中肯定要消耗更長時間,而且會隨著計時器執(zhí)行次數(shù)疊加,造成更大的誤差。

而瀏覽器最小化后每次1000ms的誤差是因為瀏覽器性能優(yōu)化的一種機制。參考MDN中關(guān)于setTimeout的一段描述:

未被激活的tabs的定時最小延遲>=1000ms

為了優(yōu)化后臺tab的加載損耗(以及降低耗電量),在未被激活的tab中定時器的最小延時限制為1S(1000ms)。

Firefox 從version 5 (see bug 633421開始采取這種機制,1000ms的間隔值可以通過 dom.min_background_timeout_value 改變。Chrome 從 version 11 (crbug.com/66078)開始采用。 Android 版的Firefox對未被激活的后臺tabs的使用了15min的最小延遲間隔時間 ,并且這些tabs也能完全不被加載。

如何解決誤差

倒計時器的誤差是不可避免的,但是我們可以通過誤差值去調(diào)整每次執(zhí)行的時間間隔:

 var countIndex = 1; //倒計時任務(wù)執(zhí)行次數(shù)
 const timeout = 1000; //時間間隔1秒
 const startTime = new Date().getTime();
 
 countdown(timeout);
 
 function countdown(interval) {
     setTimeout(function () {
         const endTime = new Date().getTime();
 
         //誤差
         const deviation = endTime - (startTime + countIndex * timeout);
         countIndex ++ ;
 
         //執(zhí)行下一次倒計時,去除誤差的影響
         countdown(timeout - deviation);
     }, interval)
 }

執(zhí)行下一次倒計時,去除誤差的影響countdown(timeout - deviation),這里我們通過對下一次任務(wù)的調(diào)用時間做了調(diào)整,前面延遲了多少毫秒,那么我們下一個任務(wù)執(zhí)行就加快多少毫秒,這就是處理倒計時誤差的基本思路。

還有一種解決辦法就是通過獲取后臺服務(wù)器的時間去校準(zhǔn)倒計時,獲取本地時間實際上是不嚴(yán)謹(jǐn)?shù)模?code>new Date()獲取到的時間是本機系統(tǒng)的時間,用戶可以通過調(diào)整系統(tǒng)時間欺騙瀏覽器。所以通過獲取服務(wù)器時間校對是比較靠譜的一種做法。

修改系統(tǒng)時間

對于切換Tab瀏覽器倒計時器產(chǎn)生的大誤差,解決思路是切回瀏覽器界面后,通過監(jiān)聽頁面可見或被隱藏visibilitychange事件,獲取最新的時間,這樣用戶看到的就是沒有誤差的倒計時了。

 document.addEventListener('visibilityChange', function() {
     if (!document.hidden) {
       // get newest time
     }
 });

你學(xué)“廢”了么?


文章首發(fā)于我的博客 echeverra.cn,原創(chuàng)文章,轉(zhuǎn)載請注明出處。

歡迎關(guān)注我的微信公眾號 echeverra,一起學(xué)習(xí)進(jìn)步!不定時會有資源和福利相送哦!


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容