前端開發中一個不可避免的場景就是寫倒計時,沒有接觸這個場景以前筆者一直覺得這玩意只要 setInterval 一下就能夠了,最多就是有 Event Loop 致使微小偏差的坑,直到去年年初的時候寫登錄註冊遇到了才發覺這裏仍是有很多講究的,這邊文章主要是記錄筆者是如何解決倒計時這種場景的實際開發中遇到的幾個問題。本文會帶來倆個部分同窗陌生的概念:Event Loop 和 requestAnimationFrame 涉及到篇幅問題會以後單獨寫文章,可是請相信筆者,即便你不懂這倆東西看完以後再去查資料也不晚。前端
結尾會附上源代碼和一個基於 react hooks 的實現。react
本文涉及的源碼地址:https://github.com/MonchiLin/...git
目前已經實現的功能以下:github
基於該庫實現的 react hooks 版本:https://github.com/MonchiLin/...typescript
你們都知道 JS 的併發模型是 Event Loop,用起來很簡單方便,可是在倒計時這種場景中會致使定時器(setInterval/setTimeout)沒法精確的計算時間,正常狀況下這點程度也不須要特別重視,致使時間錯誤的另外一個問題是在一些瀏覽器中( Chrome/Edge )標籤頁在後臺時定時器是不會被執行的,這就會形成一個很大的問題,看下面的例子:npm
小紅使用手機號登陸xx網站,發送驗證碼以後小紅髮現手機號填錯了,因而在等待再次發送驗證碼的時間內她順手打開了別的網站看了一會,這時回來發現倒計時仍然在繼續。後端
下面代碼爲如何矯正倒計時:瀏覽器
/** * 矯正時間 */ rectifyTime() { // 注: this.infoForRectification.startTime 爲本次倒計時的開始的時間點 // 注: this.infoForRectification.endTime 爲本次倒計時的結束時時間點 // this.infoForRectification.endTime = 本次倒計時的開始的時間點 + 須要倒計時的時間 // 這裏樓主的代碼已經 // 倒計時開始後通過了多久 = 當前時間 - 倒計時開始的時間 const now = new Date().getTime() - this.infoForRectification.startTime // 完成倒計時總需時間 = 倒計時的結束時時間點 - 倒計時開始的時間點 const total = this.infoForRectification.endTime - this.infoForRectification.startTime // 指望的當前剩餘時間 = 倒計時開始後通過了多久 - 完成倒計時總需時間 (step 先無視) const timeOfAnticipation = this.countdownConfig.step * (total - now) console.log("指望的當前剩餘時間 =>", timeOfAnticipation / 1000, "s") console.log("實際當前剩餘時間 =>", this.currentTime, "s") // 誤差 = 當前的倒計時 - 指望的當前剩餘時間 / 1000 (由於指望的剩餘時間是時間戳) const offset = this.currentTime - timeOfAnticipation / 1000 console.log("偏差 =>", offset, "s") // 處理離開屏幕過久的狀況, 早就已經完成了倒計時,在調用函數的地方進行處理,如果返回 false 則認爲已經倒計時結束 if (offset > this.currentTime) { return false } else if (offset >= this.config.precision / 1000) { // this.config.precision:精度 // 若是偏差已經大於允許的誤差則矯正一次當前的倒計時 this.currentTime -= offset } return true }
解決了大問題以後咱們不妨更進一步來優化一下 Event Loop 致使的時間誤差, requestAnimationFrame 能夠實現更加優秀的倒計時解決方案,它最大的特色就是瀏覽器會保證在每次刷新的屏幕的時候調用一次,那麼瀏覽器多久刷新一次屏幕呢?取決你屏幕的刷新率,通常在 60 Hz 以上,假設瀏覽器一秒刷新 60 次,那麼每次調用的間隔就是 1000 / 60 = 0.16 ms 這意味着咱們使用 RAF(requestAnimationFrame)能夠減小更多的偏差,下面附上 RAF 實現 setInterval 的代碼:閉包
this.requestInterval = (fn, delay) => { // 記錄開始時間 let start = new Date().getTime() // 建立一個對象保存 raf 的 timer 用於清除 raf const handle: Handle = { timer: null }; // 建立一個閉包函數 const loop = () => { // 每次儲存 timer, 注意看,這裏遞歸調用了 loop,這就是 raf 的用法 handle.timer = requestAnimationFrame(loop); // loop 本次被調用的時間 const current = new Date().getTime() // 計算距離上次調用 loop 過了多久 = 本次調用時間 - 起始時間 const delta = current - start; // 若是 delta >= delay 就意味着已經通過 delay 的時間,將再次調用 fn if (delta >= delay) { fn.call(); // 從新記錄開始時間 start = new Date().getTime(); } } handle.timer = requestAnimationFrame(loop); return handle; }
如果要解決實際問題就一定繞不開須要偏離思路的髒代碼,這部分代碼並無什麼特點,主要就是爲了處理邊界行爲,筆者再三考慮後仍是以爲移除這部分文章,若是有小夥伴須要我講解能夠在告訴我,最好的方式固然仍是直接看源代碼,對於一些的小夥伴來講看源碼要比看別人講解快太多了。併發
什麼,你說你還要處理刷新網頁後從本地讀取這種狀況,Oh no!想必聰明的你讀完這篇文章後本身造一個倒計時的輪子也不在話下,或者由後端的小夥伴配合一下,例如返回 「驗證碼頻繁」。
另外,若是有新的功能需求也能夠告訴我。