JavaScript 倒數計時踩坑集錦

哈利破特發表於2019-03-04

前陣子,專案中加了個倒數計時的需求,接手的時候

複製貼上幹

啪啪啪三聲,搞定,送測

let countdown = 100000; // ms 伺服器返回的倒數計時剩餘時間

function startCountdown() {
    setTimeout({
        countdown -= 1000;
        if (/* some */) {
            // do some
            startCountdown();
        }
    }, 1000);
}
複製程式碼

某個彩筆開發:這波有bug我吃shi。

然後測試小姐姐反手就給了我幾個 bug

哭

  • bug1: 你這東西不準啊,我看著幾分鐘,有好幾秒的延遲
  • bug2: 你這東西有問題啊,我縮小瀏覽器,等一會再開啟,延遲了幾分鐘

某個彩筆開發:不可能,我再自測一下,打你臉,我測的時候明明沒問題。

五分鐘後:額,額,額~emmmmmmmmm....

好嘛,我改。

原因

瀏覽器中的定時器任務是有誤差的,也就是我們常說的 setTimeout 為什麼不準的問題,這裡涉及到 js 單執行緒以及執行機制,感興趣的可以去了解一下,很多文章都有介紹。

在我的程式碼中,造成 bug 的原因:

  • 沒考慮誤差的疊加,也就是沒有處理誤差
  • 沒考慮瀏覽器的"休眠"

處理

1. 沒考慮誤差的疊加,也就是沒有處理誤差

先看一下優化後的程式碼

let countdown = 100000; // ms 伺服器返回的倒數計時剩餘時間
let countIndex = 1; // 倒數計時任務執行次數
const timeout = 1000; // 觸發倒數計時任務的時間間隙
const startTime = new Date().getTime();

startCountdown(timeout);

function startCountdown(interval) {
  setTimeout(() => {
    const endTime = new Date().getTime();
    // 偏差值
    const deviation = endTime - (startTime + countIndex * timeout);
    console.log(`${countIndex}: 偏差${deviation}ms`);
    
    countIndex++;
    
    // 下一次倒數計時
    startCountdown(timeout - deviation);
  }, interval);
}
複製程式碼

幾乎沒有什麼邏輯的執行快,就已經有每秒平均 5ms 的延遲,那麼 10 分鐘的延遲將會累加到 3000ms。

JavaScript 倒數計時踩坑集錦

倒數計時誤差是不能避免的,但是我們能儘可能的減小這個誤差。

// 下次倒數計時任務執行的等待時間 = 1s - 誤差
startCountdown(timeout - deviation);
複製程式碼

這裡我們通過對下一次任務的呼叫時間做了調整,前面延遲了多少毫秒,那麼我下一個任務執行就加快多少毫秒,這就是處理倒數計時誤差的基本思路。

2. 沒考慮瀏覽器的"休眠"

On most browsers inactive tabs have low priority execution and this can affect JavaScript timers.

用我蹩腳的英語翻譯一下:在大多數瀏覽器中,待用的 tab 頁優先順序較低,這會對 JavaScript 的定時器造成影響。

上面的大多數瀏覽器不包括 ie,在 ie 下沒有這個問題(ie真好用)。

JavaScript 倒數計時踩坑集錦

蹩腳 + 概括:為了節能,對於執行在後臺的 tab 頁,定時器的延遲毫秒數被我們設定為 >= 1000ms。

相關連結:

針對第二點

複製上面的程式碼,在瀏覽器偵錯程式裡面試試,切換 tab 頁後,明顯的可以看出延遲了。

JavaScript 倒數計時踩坑集錦

關於這一點處理,其實更多的跟業務相關,不同的業務可能有不一樣的處理方式。

由我的需求引申出一個例子:

  • 網頁實現一個 5 天的倒數計時功能
  • 倒數計時的剩餘數通過請求獲取,初始為432000(s),也就是5天,並且伺服器端也會進行一個倒數計時

我猜,目前大部分的倒數計時功能都是這樣實現的,前端其實只是負責一個倒數計時UI的顯示,能讓使用者感知到有這麼一回事,真正倒數計時的還是放在了伺服器端。

前面看到了切換 tab,或者網頁最小化時,有延遲,那麼我們只要監聽使用者什麼時候回到頁面,這個時候再去請求伺服器端最新的剩餘時間,重新開始倒數計時,修正造成的延遲。

目前有兩種方案監聽:

對於window.focus + window.blur,即使網頁仍呈現在使用者面前,也會觸發此事件,比如從瀏覽器切到微信聊天(此時網頁依然可見),但是仍會觸發 window.blur 事件。

因此我利用 visibilitychange 事件來處理切換 tab 頁以及瀏覽器最小化時的倒數計時誤差修正。

// 處理頁面可見屬性的改變
document.addEventListener('visibilityChange', () => {
    if (!document.hidden) {
      // get newest downtime
    }
});
複製程式碼

總結

在查資料的同時,好像還有幾種方案也可以實現倒數計時。

但是 requestAnimationFrame 好像也不能解決第二種情況:

JavaScript 倒數計時踩坑集錦

Web Worker 過兩天玩一下,還沒搞過呢。

第一次寫文章,寫得不好請見諒,也請指出。有更好的方法也可以分享一下,感恩~!

相關文章