共计 2633 个字符,预计需要花费 7 分钟才能阅读完成。
背景
我的项目里有个秒杀倒计时功能模块。
页面切换 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 笔记: 如何写个倒计时
正文完
发表至: javascript
2020-09-23