聊一下活動倒計時的一些實踐方案與思考。方案總體來說分爲兩種:
方案一:依賴本地時間
當然不能簡單粗暴的直接獲取本地時間來倒計時,結果可能是每個用戶的倒計時時間千差萬別,而且用戶可以惡意更改本地時間去繞過倒計時操作。
方案:接口獲取到服務前當前時間以後,與本地時間做一個差值計算。循環中使用本地時間加時間差得出當前時間,來計算倒計時時間,並在一定頻率內更新服務器、本地時間差值。
// 接口獲取服務器當前時間,並計算與本地時間的時間差
const timeDiff = serverTime - new Date().getTime()
setTimeout(() => {
// 當前時間 = 本地時間 + 時間差
const currTime = new Date().getTime() + timeDiff
// 倒計時時間 = 截止時間 - 當前時間
const countDown = endTime - currTime
// 顯示倒計時信息
setCountDownInfo(countDown)
}, 1000)
優點:
-
js
倒計時有延遲。如果單純使用js
的setTimeout
倒計時,代碼運行時間越長偏差越大,本地時間的準確性就很可以實時糾正js
倒計時的偏差。 -
pc端瀏覽器進入後臺運行,
js
計時器會變慢;移動端應用進入後臺運行,js計時器會直接停止,導致倒計時嚴重偏差。而依賴本地時間,就可以糾正這些偏差。
缺點:
用戶也是可以修改本地時間繞過計時器的。需要一定的頻率,獲取服務器時間更新時間差值,防止用戶修改本地時間。
方案二:js
倒計時
方案:獲取到服務器時間以後,js用
setTimeout
做倒計時,不依賴本地時間。
// 接口獲取服務器當前時間
let currTime = serverTime
setTimeout(() => {
// 當前時間 = 服務器時間 循環累加
currTime = currTime + 1000
// 倒計時時間 = 截止時間 - 當前時間
const countDown = endTime - currTime
// 顯示倒計時信息
setCountDownInfo(countDown)
}, 1000)
優點:解決了依賴本地時間的弊端
缺點:前邊提過,js定時器有偏差;頁面後臺運行,定時器會變慢。需要一定的頻率,獲取服務器當前時間矯正這些偏差
思考一:校準時間的頻率
無論上述哪種方案,無論矯正js
定時器偏差還是更新時間差值防止修改本地時間,頻率的設定都很重要,可以根據場景區分
1. 倒計時的重要性:
- 直接控制秒殺或購買按鈕的倒計時,準確性要求最高,所以需要頻率相對高;
- 其次就是浮標、活動入口按鈕、落地頁等提示類倒計時,不涉及主要業務邏輯,準確性要求低,可以頻率設定相對低;
2. 距離倒計時結束的時長
- 距離還很長時,更新頻率高,沒必要而且很浪費服務器資源。
- 倒計時馬上結束時,更新頻率很低,精度就會很差。
所以通常倒計時越接近結束時,準確性要求越高。可以根據倒計時時長,來動態設定更新頻率,倒計時距離結束越近,更新的頻率越高,並且設定頻率最高閾值;反之同理。
比如倒計時爲5分鐘時,30s更新一次:(5 * 60 * 1000) / 30 = 當前時間差(ms) / 當前頻率(s)
,就可以獲取動態的更新頻率
3. 當然頻率具體的數值,還要根據接口穩定性和用戶數量決定
思考二: 使用setTimeout
還是setInterval
試想:
- 如果
setInterval
定時器間隔時間總是小於操作執行時間時,事件隊列中就會排起隊 :本次循環事件未執行完,下次循環的事件已經添加進隊列中 - 而且 當使用
setInterval
時,僅當沒有該定時器的任何其他代碼實例時,纔將定時器代碼添加到隊列中
所以:
- 丟幀: 那麼下次循環結束時,因爲隊列中已經有它的一個實例,就不會向隊列中添加事件了,所以這次事件執行就會丟失。
- 而且當前的事件執行完畢後,就會馬上執行隊列中已經添加的事件,這兩次事件執行的時間間隔就會變小甚至無間隔
總結:
一定時間間隔重複做某些操作時,setInterval
可能會出現丟幀和操作之間無時間間隔的情況。所以儘量使用setTimeout,它可以保證事件在相同時間間隔內執行,並且不丟失事件。
參考這篇文章
思考三: 爲什麼setTimeout
時間間隔會有偏差
setTimeout
只是負責準時的把事件添加進事件隊列中,但是如果js
線程在忙於執行其他任務 (處理用戶交互、js
代碼執行等),那麼這個事件就需要等待線程空閒了才能被執行。
如果setTimeout
遞歸實現倒計時,上述等待js
線程產生的延遲就會在不停的循環中被疊加累計,延遲偏差就會越來越大,計時就會越來越不準。