背景
我的项目里有个秒杀倒计时功能模块。
页面切换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
实现的,并且通过计数形式记录剩余时间。
问题剖析
- 页面切换Tab后,再回来发现显著慢了。
这个是浏览器的Timer throttling
策略 - 用“次数”示意工夫是不精确的。
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】解决问题。
计划施行
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;}
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> )}
遗留问题:
setTimer(fn, delay)
两次执行fn
工夫距离大于delay
的,如果执行距离比拟大的话会会造成倒计时跳级。
参考
整顿自gitHub 笔记: 如何写个倒计时