js 活動倒數計時詳解

Hhy_9288發表於2018-04-20

背景

前端頁面倒數計時功能在很多場景中會用到,如運營活動開始倒數計時和活動結束倒數計時,又如購物網站的秒殺倒數計時,搶購倒數計時,還有我們手Q春節搶紅包倒數計時等等……. 最近的話費代付專案中,也涉及倒數計時功能,但在開發過程中遇到一些麻煩和坑點,下面和大家分享一下最後是如何解決的。

坑點

手Q春節搶明星紅包活動,就有產品吐槽兩個手機在不同時間點開啟同一個活動顯示的開搶倒數計時不一樣,誤差大的甚至相差幾分鐘,導致某些使用者在活動顯示還未開始紅包就已被搶完了。為什麼誤差會這麼大呢?

對於這個問題,前臺開發同學一般會猜測是這個原因:倒數計時讀取了客戶端時間造成的,因為客戶端時間和服務端時間有誤差,應該讀取伺服器時間。

是的,倒數計時不應該讀取客戶端時間,客戶端時間使用者可以隨時調整,會造成不一致,應讀取伺服器返回時間。但實踐證明,做了這一步還未夠,頁面執行時間長了,新開的頁面和原開啟頁面還是存在誤差。

京東團購也存在這個問題:

a68e938e274c0524112776c05ae39f571425366912

造成誤差的原因主要有幾種可能:

1. 沒有考慮js凍結執行耗費時間;(特別是移動端容易出現,下滑頁面時倒數計時不動了)

2. 沒有考慮頁面渲染和函式執行累積時間;(京東的誤差貌似屬於這種)

3. 其他程式碼邏輯問題(這種情況就複雜了,這裡不討論);

計時器原理

倒數計時功能離不開setTimeout或setInterval這兩個函式,要用好這兩個函式必先了解好Javascript直譯器的工作原理,前端界大牛John.Resig (jQuery作者) 有篇文章很好講解了Javascript直譯器工作原理 — 《How JavaScript Timers Work》

前端開發同學都知道,javascript是單執行緒的(web worker除外),更好理解的解釋是javascript直譯器是單執行緒工作,它不能在處理一個ajax的callback的同時去處理click event的callback,而是必須按照先後佇列順序執行。

1425041883_52_w1024_h768

這圖包含資訊量很大,這裡按照自己的理解描述一下:

這圖從上往下看,垂直方向是時間,以ms為單位,藍色模組是執行程式碼所佔的時間段,如第一個程式碼模組執行js佔用了約18ms, 第二個模組執行js佔用了約11ms,其他模組類似。由於js是單執行緒執行,同一時間只能執行一個js程式碼(同一時間其他非同步事件執行會被阻塞 ) , 當非同步事件發生時,它會進入程式碼執行佇列,執行執行緒空閒時依照佇列順序依次執行程式碼。

第一個模組初始化了兩個定時器,一個10ms延遲的setTimeout和10ms的setInterval。這些定時器可能會在我們第一個程式碼塊執行結束之前就觸發,這取決於定時器在第一個程式碼塊中啟動的位置和時間。注意,定時器雖然觸發了,但是並不會立即執行,它只是把需要延遲執行的函式按時間先後加入了執行佇列,線上程的某一個空閒的時間點,這個函式就能夠得到執行。

按照第一個模組事件觸發的順序(Mouse Click Occurs -. 10ms Timer Fires),第一個模組程式碼執行結束後,按照佇列中等待的先後順序執行事件,先執行Mouse Click CallBack再執行Timer。在執行Mouse Click CallBack模組時,Interval第一次觸發未執行加入佇列。在執行Timer模組時,Interval第二次觸發未執行加入佇列。待Mouse Click CallBack和Timer模組都執行完畢後,再依次執行佇列中已觸發的Interval事件。後面模組由於沒有阻塞的事件了,所以按照既定10ms執行Interval事件。

倒數計時問題

如果上面Javascript計時器原理理解了,就很好明白倒數計時功能存在問題的隱患。

先看一段測試程式碼:

目測程式碼就知道執行結果,定時器每秒執行一次,每次輸出應該是0 。

實際輸出:

1425095227_64_w308_h208

結論:由於程式碼執行佔用時間和其他事件阻塞原因,導致有些事件執行延遲了幾ms,但影響很微。

下面加一段阻塞程式碼看看:

實際輸出:

1425095555_30_w290_h240

結論:由於加了很佔執行緒的阻塞事件,導致定時器事件每次執行延遲越來越嚴重。

由於實際專案中,執行計時器的同時,會有很多其他非同步阻塞事件,會導致倒數計時功能不精確。

解決思路

這裡先分析一下從獲取伺服器時間到前端顯示倒數計時的過程:

1. 客戶端http請求伺服器時間;

2. 伺服器響應完成;

3. 伺服器通過網路傳輸時間資料到客戶端;

4. 客戶端根據活動開始時間和伺服器時間差做倒數計時顯示;

1425113045_11_w380_h210

伺服器響應完成的時間其實就是伺服器時間,但經過網路傳輸這一步,就會產生誤差了,誤差大小視網路環境而異,這部分時間前端也沒有什麼好辦法計算出來,一般是幾十ms以內,大的可能有幾百ms。

可以得出:當前伺服器時間 = 伺服器系統返回時間 + 網路傳輸時間 + 前端渲染時間 + 常量(可選),這裡重點是說要考慮前端渲染的時間,避免不同瀏覽器渲染快慢差異造成明顯的時間不同步,這是第一點。(網路傳輸時間忽略或加個常量唄)

獲得伺服器時間後,前端進入倒數計時計算和計時器顯示,這步就要考慮js程式碼凍結和執行緒阻塞造成計時器延時問題了,我的思路是通過引入計數器,判斷計時器延遲執行的時間來調整,儘量讓誤差縮小,不同瀏覽器不同時間段開啟頁面倒數計時誤差可控制在1s以內

關鍵實現程式碼如下:

執行結果:

1425117036_60_w420_h263

結論:由於執行緒阻塞延遲問題,做了setTimeout執行時間的誤差修正,保證setTimeout執行時間一致。若凍結時間特別長的,還要做特殊處理。

轉載:https://www.xuanfengge.com/js-realizes-precise-countdown.html

參考:https://blog.csdn.net/hhy_9288/article/details/79455390


相關文章