1

背景

项目里有个秒杀倒计时功能模块。
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。所以用次数表示时间是不准确。

解决方案

综合考虑下最终采用【方案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 笔记: 如何写个倒计时


普拉斯强
2.7k 声望53 粉丝

Coder