前陣子,專案中加了個倒數計時的需求,接手的時候
啪啪啪三聲,搞定,送測
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。
倒數計時誤差是不能避免的,但是我們能儘可能的減小這個誤差。
// 下次倒數計時任務執行的等待時間 = 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真好用)。
蹩腳 + 概括:為了節能,對於執行在後臺的 tab 頁,定時器的延遲毫秒數被我們設定為 >= 1000ms。
相關連結:
- How can I make setInterval also work when a tab is inactive in Chrome?
- mozilla – Timeouts in inactive tabs throttled to >=1000ms
針對第二點
複製上面的程式碼,在瀏覽器偵錯程式裡面試試,切換 tab 頁後,明顯的可以看出延遲了。
關於這一點處理,其實更多的跟業務相關,不同的業務可能有不一樣的處理方式。
由我的需求引申出一個例子:
- 網頁實現一個 5 天的倒數計時功能
- 倒數計時的剩餘數通過請求獲取,初始為432000(s),也就是5天,並且伺服器端也會進行一個倒數計時
我猜,目前大部分的倒數計時功能都是這樣實現的,前端其實只是負責一個倒數計時UI的顯示,能讓使用者感知到有這麼一回事,真正倒數計時的還是放在了伺服器端。
前面看到了切換 tab,或者網頁最小化時,有延遲,那麼我們只要監聽使用者什麼時候回到頁面,這個時候再去請求伺服器端最新的剩餘時間,重新開始倒數計時,修正造成的延遲。
目前有兩種方案監聽:
- window.focus + window.blur
- visibilitychange 事件
對於window.focus + window.blur,即使網頁仍呈現在使用者面前,也會觸發此事件,比如從瀏覽器切到微信聊天(此時網頁依然可見),但是仍會觸發 window.blur 事件。
因此我利用 visibilitychange 事件來處理切換 tab 頁以及瀏覽器最小化時的倒數計時誤差修正。
// 處理頁面可見屬性的改變
document.addEventListener(`visibilityChange`, () => {
if (!document.hidden) {
// get newest downtime
}
});
複製程式碼
總結
在查資料的同時,好像還有幾種方案也可以實現倒數計時。
但是 requestAnimationFrame 好像也不能解決第二種情況:
Web Worker 過兩天玩一下,還沒搞過呢。
第一次寫文章,寫得不好請見諒,也請指出。有更好的方法也可以分享一下,感恩~!