背景

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

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