背景
項目裏有個秒殺倒計時功能模塊。
頁面切換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 筆記: 如何寫個倒計時