使用 JavaScript 實現倒計時、JS 定時器注意事項
1. JS 倒計時#
agenda 插件需要增加番茄鐘功能,在 github 上找到 usePomodoro ,看起來功能挺全的,而且使用 hook 使用起來很方便,雖然 star 數量並不多,但是這個功能也不複雜,如果有問題我自己能 hold 住,所以最終就選了它。
但是在功能開發完上線後才發現,總是計時不準確,明明設置的 25min 番茄鐘,結果一個小時過去了,倒計時也沒跑完。
後來拿著手機對比研究一下,確定了當軟件處於後臺時就會出現誤差。突然想起來以前也遇見過類似的情況,當瀏覽器 tab 處於非激活的狀態時,定時器就可能不準確。
去倉庫看了下代碼,果然問題出在這裡:
// 原代碼使用 setInterval,來計算倒計時剩餘時長
setInterval(tick, 1000)
當 tab 不處於激活狀態(tab 被隱藏,瀏覽器處於後臺)時,setInterval 的觸發間隔就並不是設定的 1000ms。這可能是瀏覽器出於性能與耗電等考慮設計的機制。
那麼如何處理呢?
既然 js 不能保證代碼每隔 1s 執行一次,就只能從外部下手。比較普遍的方法是使用系統時間作為參照物:
function countDown(length) {
const start = new Date().valueOf()
const timer = setInterval(() => {
const now = new Date().valueOf()
const past = now - start
if (past <= 0) {
console.log('count down end')
clearInterval(timer)
}
}, 1000)
}
知道開始時間及倒計時的時長後,就能算出結束時間,也能在每次定時器回調中知道已經過去的時間。將計時的任務交給系統,js 只是輪詢查流逝的時間,這樣就可以避免誤差。
這個是我提的 pr。
這個方法也有一個問題,如果用戶一直讓頁面處於非激活狀態,在倒計時應該結束的那一刻,setInterval 可能也無法執行,需要到用戶激活 tab,才能再次觸發。
如果需要解決這個問題,感覺一個方案是包裝成 app 來進行後臺保活,一個方案是把狀態信息存到服務器,當計時結束時服務器給 js 發消息。
2. js 定時器注意事項#
基礎使用很簡單,我們這裡只說需要注意的幾個點。
2.1 定時器嵌套#
setInterval
回調可以嵌入對另一個定時器的調用,為了減輕對性能的影響,一旦定時器嵌套超過 5 層,則瀏覽器將強制設置定時器最小時間間隔為 4ms。
2.2 確保代碼執行時間短於定時器間隔#
定時器回調屬於異步任務,只有調用定時器的主線程其他代碼執行完畢才會執行回調函數。如果前面的代碼執行時間超過定時器間隔,那麼定時器就不會在正確的時間觸發。所以瀏覽器無法保證一定在正確的時間觸發回調。
2.3 未被激活的 tabs 的定時最小延遲 ≥ 1000ms#
為優化後臺 tab 的加載損耗(以及降低耗電量),在未被激活的 tab 中定時器最小延遲時間為 1000ms。
2.4 最大延遲值#
瀏覽器以 32 位帶符號整數存儲延時,當延時大於 2147483647 毫秒 (大約 24.8 天) 時就會溢出,此時定時器會立即執行。