如何寫一個靠譜的前端倒計時庫

  • [時間]: 2020/3/16
  • [keyword]: 前端,npm,開發,Typescript,倒計時
前端開發中一個不可避免的場景就是寫倒計時,沒有接觸這個場景以前筆者一直覺得這玩意只要 setInterval 一下就能夠了,最多就是有 Event Loop 致使微小偏差的坑,直到去年年初的時候寫登錄註冊遇到了才發覺這裏仍是有很多講究的,這邊文章主要是記錄筆者是如何解決倒計時這種場景的實際開發中遇到的幾個問題。

本文會帶來倆個部分同窗陌生的概念:Event Loop 和 requestAnimationFrame 涉及到篇幅問題會以後單獨寫文章,可是請相信筆者,即便你不懂這倆東西看完以後再去查資料也不晚。前端

結尾會附上源代碼和一個基於 react hooks 的實現。react

本文涉及的源碼地址:https://github.com/MonchiLin/...git

目前已經實現的功能以下:github

  • [x] 靈活的參數配置
  • [x] 基於 raf 的定時器實現
  • [x] 暫停倒計時/恢復倒計時

基於該庫實現的 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!想必聰明的你讀完這篇文章後本身造一個倒計時的輪子也不在話下,或者由後端的小夥伴配合一下,例如返回 「驗證碼頻繁」。

另外,若是有新的功能需求也能夠告訴我。

相關文章
相關標籤/搜索