关于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 笔记: 如何写个倒计时

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理