项目中使用了setInterval做了一个每秒自增的定时器,突然有一天出现了时间倒着走的奇异现象。然后经过一波分析,总得来说就是setInterval在页面非活跃态的时候会被放慢,并不是按你设定的时间间隔去执行。我们时间计算公式是Math.abs(currentTime - startTime),currentTime(当前时间)正常来说肯定大于等于startTime(开始时间),setInterval的定时器就是让currentTime每秒自增,但是setInterval放慢,就导致currentTime比startTime小了,然后currentTime1s 1s增加,Math.abs(currentTime - startTime)的结果就1s 1s变小。
为了解决这个问题,想了两个方案。
方案一:后端会返回一个当前时间,组件初始加载的时候,使用这个时间初始化currentTime,并用currentTimeBackup备份currentTime,用localCurrentTime记录本地当前时间,之后currentTime的增长使用:
currentTime = currentTimeBackup + new Date().valueOf() - localCurrentTime
这样就不受setInterval被放慢的影响了。
方案二:方案二的设想是既然setInterval有问题,就想用setTimeout来模拟setInterval,当时还不知道setTimeout有一样的问题,然后设计了如下函数:
function setAccurateInterval(fn, interval) {
let count = 0;
const startTime = new Date().getTime();
function fixed() {
count++;
let offset = new Date().getTime() - (startTime + count * interval);
let nextTime = interval - offset;
if (nextTime < 0) nextTime = 0;
setTimeout(fixed, nextTime);
fn();
}
setTimeout(fixed, interval);
}
这个函数就造成了另一个现象,页面处于非活跃态一段时间后,再次被激活,时间飞速增长,一开始还以为是setTimeout任务被缓存了,查询了一大波资料,并不是,最后应该算是了解了本质。
结论:setTimeout任务并没有被缓存,浏览器的优化机制和代码本身问题造成这一现象
页面处于活跃态时,各数据如下:
offset甚小,nextTime基本就是1s,不存在问题。
切换页面,让定时器所在页面处于非活跃态时,各数据如下:
可以看到切换后的nowTime相较于前一个相差 1914ms,差不多2s了(这是因为这时JS主线程正在执行执行其他任务,等轮到这个宏任务[JS任务和事件循环可以参阅这篇文章]时就过去这么久了),而此时count只加了1,导致nextTime只剩85ms了。但这时浏览器存在优化机制,如果标签不是当前的激活标签,那么定时器最小的时间间隔是 1000 毫秒。所以这时表面上时间还是在1s 1s的加。
但是我还发现,非活跃标签页的setTimeout在执行大约300次后,会再次放慢,间隔会变成60s。(这个目前没有在网上找到佐证材料,如果万幸有人看到这篇记录,并且知道相关材料,欢迎留言,万分感谢)。这就造成offset会非常大。
这个时候nextTime就是0了,然后当页面再次处于激活状态,执行的都是setTimeout(()=>{…}, 0),就造成了时间飞速上涨的现象,这时两个定时任务的执行时间会比1s小很多(最快是4ms执行一次),count快速增长,回到理论上的正常值,然后就回到了最初的正常的状态。
以上就是对setTimeout和serInterval在使用时的一些总结。
再粘贴记录一下setTimeout的实现原理: