博客 / 詳情

返回

寫個秒殺倒計時方法

背景

項目裏有個秒殺倒計時功能模塊。
image
頁面切換Tab後,一段時間再回來發現明顯慢了。擼代碼吧:

// ...
CountDown.prototype.count = function() {
  var self = this;
  this.clear();
  this.timeout = setTimeout(function(){
    // 計數減1
    if(--self.currCount <= 0) {
       // ...
    } else {
        // ...
        self.count();
    }
  }, this.options.step)
}

內部通過setTimeout實現的,並且通過計數方式記錄剩餘時間。

問題分析

  1. 頁面切換Tab後,再回來發現明顯慢了。
    這個是瀏覽器的Timer throttling策略
  2. 用“次數”表示時間是不準確的。
    setTimeout(fn, delay),並不是表示delay時間後一定執行fn,而是表示 最早delay 後執行fn。所以用次數表示時間是不準確。

解決方案

  • 方案1:阻止瀏覽器Timer throttling
    How to prevent the setInterval / setTimeout slow down on TAB change裏提到可以用web Worker處理瀏覽器Timer throttling,並且還有現成的npm庫HackTimer可以使用。
  • 方案2:用户切回瀏覽器TAB時更正倒計時。
    不使用“計數方式”計算時間,通過計算setTimeout(fn, delay)fn函數兩次執行間隔來計算剩餘時間。

綜合考慮下最終採用【方案2】解決問題。

方案實施

  1. getTimerParts.js 剩餘時間格式化方法
const units = ['ms', 's', 'm', 'h', 'd'];
const divider = [1, 1000, 1000 * 60, 1000 * 60 * 60, 1000 * 60 * 60 * 24];
const unitMod = [1000, 60, 60, 24];

/**
 * 返回值格式:
 * {
 *  d: xxx,
 *  h: xxx,
 *  m: xxx,
 *  s: xxx,
 *  ms: xxx
 * }
 */
export default  function getTimerParts(time, lastUnit = 'd') {
    const lastUnitIndex = units.indexOf(lastUnit);
    const timerParts = units.reduce((timerParts, unit, index) => {        
        timerParts[unit] = index > lastUnitIndex
            ? 0
            : index === lastUnitIndex
            ? Math.floor(time / divider[index])
            : Math.floor(time / divider[index]) % unitMod[index];

        return timerParts;
    }, {});

    return timerParts;
}
  1. countDown.js 倒計時構造函數
import getTimerParts from './getTimerParts'

function now() {
    return window.performance 
        ? window.performance.now() 
        : Date.now();
}

export default function CountDown({ initialTime, step, onChange, onStart }) {
    this.initialTime = initialTime || 0;
    this.time = this.initialTime;
    this.currentInternalTime = now();
    this.step = step || 1000;
    this.onChange = onChange || (() => {});
    this.onStart = onStart || (() => {});
}

CountDown.prototype.start = function() {
    this.stop();
    this.onStart(getTimerParts(this.time));
    // 記錄首次執行時間
    this.currentInternalTime = now();
    this.loop();   
}

CountDown.prototype.loop = function() {
    // 開啓倒計時
    this.timer = setTimeout(() => {
        // 通過執行時間差計算剩餘時間
        const currentInternalTime = now();
        const delta = currentInternalTime - this.currentInternalTime;        
        this.time = this.time - delta;
        if(this.time < 0) {
            this.time = 0;
        }
        // 記錄本次執行的時間點
        this.currentInternalTime = currentInternalTime;
        this.onChange(getTimerParts(this.time));
        if(this.time === 0 ) {
            this.stop();
        } else {
            this.loop();
        }        
    }, this.step);
}

CountDown.prototype.stop = function() {
    if(this.timer) {
        clearTimeout(this.timer);
    }
}

簡單的引用方:

import CountDown from '../../src/lib/timer'
import { useEffect, useState } from 'react'

export default function TimerPage() {
  const [timeParts, setTimeParts] = useState(null)
  
  useEffect(() => {
      const countDown = new CountDown({ 
        initialTime: 10000, 
        onStart: setTimeParts,
        onChange: setTimeParts
    });
    countDown.start();
  }, [])

  return (
    <div>
      <p>
      {
        timeParts && `${timeParts.d}天 ${timeParts.h}:${timeParts.m}:${timeParts.s}`
      }
      </p>
    </div>
  )
}

遺留問題:

  1. setTimer(fn, delay)兩次執行fn時間間隔大於delay的,如果執行間隔比較大的話會會造成倒計時跳級。

參考

整理自gitHub 筆記: 如何寫個倒計時

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.