情景:后端返回一个时间差,前端将其格式化为xx时xx分xx秒后展示在页面上,然后每隔1s刷新一下倒计时。写的时候发现有不少问题,遂研究记录一下。
export const getFormatTime = (durTime: number) => {
let remain = durTime;
const hour = Math.floor(remain / (60 * 60 * 1000));
remain = remain % 3600000;
const minute = Math.floor(remain / (60 * 1000));
remain = remain % 60000;
const second = Math.floor(remain / 1000);
return {
hour,
minute,
second,
};
};
这里很简单就不赘述了。
// 假设剩余时间为1小时1分10秒
let time = 3670000;
setInterval(() => {
time = time - 1000;
const data = getFormatTime(time);
// 在这更新页面数据
}, 1000)
但我印象中setInterval是有问题的,就去了解了一下,有如下几个问题:
1、setInterval会无视回调函数里的错误,而不会中止。
let count = 1;
setInterval(function () {
count++;
console.log(count);
if (count % 3 === 0) throw new Error('setInterval报错');
}, 1000)
这会让一些bug难以被排查出来。
2、setInterval会在任何情况下定时执行,即使我们不希望如此。比如每隔3s轮询一次,但是用户网络很差可能隔了十几秒才拿到上一次请求的结果,这就会导致请求积压。
3、setInterval内部代码的执行时间大于延迟时间时,当次回调会被跳过。
let startTime = new Date().getTime();
setInterval(() => {
while (new Date().getTime() < startTime + 2000) {}
startTime = new Date().getTime()
console.log(startTime);
}, 1000);
执行上面代码会发现,我们定的1s执行一次,实际2s才会打印一次。该问题是由setInterval的作用机制导致的,setInterval会在延迟时间过后将回调push到任务队列排队执行,但如果任务队列中已有该任务则不会push。
let time = 3670000;
const fn = () => {
setTimeout(() => {
time = time - 1000;
const data = getFormatTime(time);
console.log(data); // 拿到data更新页面数据
fn() // 新开定时器
}, 1000)
}
fn()
但由于setTimeout是最小延迟时间,这样写会导致误差延迟越来越大,倒计时越来越不准。这就需要每次设置的延迟时间动态调整,以达到消除误差的目的,代码如下:
let time = 3670000;
const startTime = new Date().getTime();
let count = 0;
const fn = (nextTime) => {
setTimeout(() => {
time = time - 1000;
const data = getFormatTime(time);
console.log(data); // 拿到data更新页面数据
count++
const offset = new Date().getTime() - (startTime + count * 1000); // 误差时间
fn(1000 - offset)
}, nextTime)
}
fn(1000)
看起来没有什么问题,但实际上不论是setInterval还是setTimeout都存在一个问题,当页面被切换到后台的时候,由于浏览器的优化策略,定时器的执行会被延迟,在大约5分钟后延迟到1分钟左右执行一次。
以下方案均是在这两个方案的基础上,解决了存在的问题。
let totalTime = 3670000; // 总时间
let leftTime = totalTime // 剩余时间
let startTime = new Date().getTime(); // 计时器的开始时间
let count = 0; // 计时器的执行次数
let timer = 0; // 定时器id
const fn = (nextTime) => {
timer = setTimeout(() => {
leftTime = leftTime - 1000;
const data = getFormatTime(leftTime);
console.log(data); // 拿到data更新页面数据
count++
const offset = new Date().getTime() - (startTime + count * 1000); // 误差时间
fn(1000 - offset)
}, nextTime)
}
fn(1000)
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
clearTimeout(timer)
} else {
totalTime = totalTime - (new Date().getTime() - startTime)
if (totalTime < 0) {
return;
}
leftTime = totalTime
startTime = new Date().getTime()
count = 0
fn(1000)
}
})
该方案通过监听页面的可见性变化,在hidden时清除定时器,在visible的时候更新总时间并重新开启定时器,以达到清除误差的作用。
首先安装real-interval依赖:
yarn add real-interval
代码如下:
const time = 3670000;
new Interval((pass) => {
if (time - pass * 1000 < 0) {
return false;
}
const data = getFormatTime(time - pass * 1000);
console.log(data);
}, 1000);
其中pass表示已经过了多少个1000ms,return false表示中止定时器。这种方案的优点是代码简单,缺点是会引入新的库。
Web Worker是运行在后台的JavaScript,不会影响页面的性能。浏览器的优化策略不会影响worker脚本,也就是说在worker脚本里的定时器一定会定期执行。
主线程:
import worker_script from './work';
// 创建worker线程,并监听消息
const worker = new Worker(worker_script);
worker.onmessage = (event) => {
console.log('主线程接收到了:' + event.data);
};
worker线程:
const workercode = () => {
const getFormatTime = (durTime: number) => {
let remain = durTime;
const hour = Math.floor(remain / (60 * 60 * 1000));
remain = remain % 3600000;
const minute = Math.floor(remain / (60 * 1000));
remain = remain % 60000;
const second = Math.floor(remain / 1000);
return {
hour,
minute,
second,
};
};
let time = 3670000;
setInterval(() => {
time = time - 1000;
if (time < 0) {
close(); // 关闭worker线程
return;
}
const data = getFormatTime(time);
postMessage(JSON.stringify(data)); // 向主线程发送消息
}, 1000);
};
let code = workercode.toString();
code = code.substring(code.indexOf('{') + 1, code.lastIndexOf('}'));
const blob = new Blob([code], { type: 'application/javascript' });
const worker_script = URL.createObjectURL(blob);
export default worker_script;
这种方案代码较多,但也没有啥学习成本,很容易理解。感兴趣的同学可以看一下这篇文章:web worker 使用教程
上面给出的5种方案其实在特定的场景下都是可以使用的,具体问题具体分析。就我个人而言,还是更喜欢方案3和方案5,这两种方案在无需引入额外依赖的情况下,较为完美的解决了定时器的各种问题。