JS定时器不可靠的原因及解决方案
2022-04-16 17:30:35 来源:易采站长站 作者:
目录
前言定时器工作原理setInterval调用被废弃事件循环导致定时器不可靠的原因延迟执行时间有最大值解决方案总结前言
在工作中应用定时器的场景非常多,但你会发现有时候定时器好像并没有按照我们的预期去执行,比如我们常遇到的setTimeout(()=>{},0)它有时候并不是按我们预期的立马就执行。想要知道为什么会这样,我们首先需要了解javascript计时器的工作原理。
定时器工作原理
为了理解计时器的内部工作原理,我们首先需要了解一个非常重要的概念:计时器设定的延时是没有保证的。因为所有在浏览器中执行的javaScript单线程异步事件(比如鼠标点击事件和计时器)都只有在它有空的时候才执行。
这么说可能不是很清晰,我们来看下面这张图
算第二个定时器的时间更短,但中间那个for循环的执行时间远远超过了这两个定时器设定的时间。
setTimeout 设置的回调任务是 按照顺序添加到延迟队列里面的,当执行完一个任务之后,ProcessDelayTask 函数会根据发起时间和延迟时间来计算出到期的任务,然后 依次执行 这些到期的任务。
在执行完前面的任务之后,上面例子的两个 setTimeout 都到期了,那么按照顺序执行就是打印 1 和 2。所以在这个场景下,setTimeout 就显得不那么可靠了。
延迟执行时间有最大值
包括 IE, Chrome, Safari, Firefox 在内的浏览器其内部以32位带符号整数存储延时。这就会导致如果一个延时(delay)大于 2147483647 毫秒 (大约24.8 天)时就会溢出,导致定时器将会被立即执行。(MDN)
setTimeout 的第二个参数设置为 0 (未设置、小于 0、大于 2147483647 时都默认为 0)的时候,意味着马上执行,或者尽快执行。
setTimeout(function () { console.log("你猜它什么时候打印?")}, 2147483648);把这段代码放到浏览器控制台执行,你会发现它会立马打印出 你猜它什么时候打印?
最小延时>=4ms(嵌套使用定时器)
在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是4ms,这通常是由于函数嵌套导致(嵌套层级达到一定深度),或者是由于已经执行的setInterval的回调函数阻塞导致的。
setTimeout 的第二个参数设置为 0 (未设置、小于 0、大于 2147483647 时都默认为 0)的时候,意味着马上执行,或者尽快执行。如果延迟时间小于 0,则会把延迟时间设置为 0。如果定时器嵌套 5 次以上并且延迟时间小于 4ms,则会把延迟时间设置为 4ms。function cb() { f(); setTimeout(cb, 0); }setTimeout(cb, 0);在Chrome 和 Firefox中, 定时器的第5次调用被阻塞了;在Safari是在第6次;Edge是在第3次。所以后面的定时器都最少被延迟了4ms
未被激活的tabs的定时最小延迟>=1000ms
浏览器为了优化后台tab的加载损耗(以及降低耗电量),在未被激活的tab中定时器的最小延时限制为1S(1000ms)。
let num = 100;function setTime() { // 当前秒执行的计时 console.log(`当前秒数:${new Date().getSeconds()} - 执行次数:${100-num}`); num ? num-- && setTimeout(() => setTime(), 50) : "";}setTime();这里我在39秒时切到了其他标签页,我们会发现它后面的执行间隔都是1秒执行一次,并不是我们设定的50ms。

setInterval的处理时长不能比设定的间隔长
setInterval的处理时长不能比设定的间隔长,否则setInterval将会没有间隔的重复执行
但是对这个问题,很多情况下,我们并不能清晰的把控处理程序所消耗的时长,为了能够按照一定的间隔周期性的触发定时器,我们可以使用setTimeout来代替setInterval执行。
setTimeout(function fn(){ // todo setTimeout(fn,10) // 执行完处理程序的内容后,在末尾再间隔10毫秒来调用该程序,这样就能保证一定是10毫秒的周期调用,这里时间按自己的需求来写},10)解决方案
方法一:requestAnimationFrame
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行,理想状态下回调函数执行次数通常是每秒60次(也就是我们所说的60fsp),也就是每16.7ms 执行一次,但是并不一定保证为 16.7 ms。
const t = Date.now()function mySetTimeout (cb, delay) { let startTime = Date.now() loop() function loop () { if (Date.now() - startTime >= delay) { cb(); return; } requestAnimationFrame(loop) }}mySetTimeout(()=>console.log('mySetTimeout' ,Date.now()-t),2000) //2005setTimeout(()=>console.log('SetTimeout' ,Date.now()-t),2000) // 2002这种方案看起来像是增加了误差,这是因为requestAnimationFrame每16.7ms 执行一次,因此它不适用于间隔很小的定时器修正。
方法二: Web Worker
Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。此外,他们可以使用XMLHttpRequest执行 I/O (尽管responseXML和channel属性总是为空)。一旦创建, 一个worker 可以将消息发送到创建它的JavaScript代码, 通过将消息发布到该代码指定的事件处理程序(反之亦然)。
Web Worker 的作用就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程不会被阻塞或拖慢。
// index.jslet count = 0;//耗时任务setInterval(function(){ let i = 0; while(i++ < 100000000);}, 0);// worker let worker = new Worker('./worker.js')// worker.jslet startTime = new Date().getTime();let count = 0;setInterval(function(){ count++; console.log(count + ' --- ' + (new Date().getTime() - (startTime + count * 1000)));}, 1000);
这种方案体验整体上来说还是比较好的,既能较大程度修正计时器也不影响主进程任务
总结
由于js的单线程特性,所以会有事件排队、先进先出、setInterval调用被废弃、定时器无法保证准时执行回调函数以及出现setInterval的连续执行。













闽公网安备 35020302000061号