背景
项目里有个秒杀倒计时功能模块。
页面切换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 笔记: 如何写个倒计时
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。