乐趣区

关于javascript:如何写个倒计时

背景

我的项目里有个秒杀倒计时功能模块。

页面切换 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 笔记: 如何写个倒计时

退出移动版