当前位置: 首页 > Web前端 > HTML

JS定时器执行不可靠的原因及解决方法

时间:2023-03-28 13:57:35 HTML

前言工作中使用定时器的场景有很多,但是你会发现有时候定时器似乎并没有按照我们预期的那样执行,比如我们经常遇到的setTimeout(()=>{},0)它有时不会像我们预期的那样立即执行。要理解为什么会这样,我们首先需要了解Javascript定时器是如何工作的。定时器的工作原理为了理解定时器的内部工作原理,我们首先需要了解一个非常重要的概念:定时器设置的延迟时间是无法保证的。因为所有在浏览器中执行的JavaScript单线程异步事件(比如鼠标点击事件和定时器)都是在空闲的时候执行的。这么说可能不是很清楚,让我们看下图图中有很多信息需要消化,但是充分理解它会让你更好地理解异步JavaScript执行是如何工作的。该图是一维的:垂直方向是(挂钟)时间(以毫秒为单位)。蓝色框表示正在执行的JavaScript部分。例如,第一个JavaScript块的执行时间约为18毫秒,鼠标单击块的执行时间约为11毫秒,依此类推。由于JavaScript一次只能执行一段代码(由于其单线程特性),因此每段代码都会“阻塞”其他异步事件的进行。这意味着当异步事件发生时(例如鼠标单击、计时器触发或XMLHttpRequest完成),它将排队等待稍后执行。首先,在第一个JavaScript块中,启动了两个计时器:10ms的setTimeout和10ms的setInterval。由于计时器启动的时间和地点,它实际上在我们实际完成第一个代码块之前触发,但请注意它不会立即执行(由于线程的缘故,它不能这样做)。相反,延迟函数会在下一个可用时刻排队等待执行。此外,在第一个JavaScript块中,我们看到发生了鼠标单击。与这个异步事件相关的JavaScript回调(我们永远不知道用户什么时候会执行一个动作,所以它被认为是异步的)不能立即执行,所以,就像初始计时器一样,它会排队等候稍后执行。在JavaScript的初始块执行完毕后,浏览器立即提出问题:什么正在等待执行?在这种情况下,鼠标单击处理程序和计时器回调都在等待。然后浏览器选择一个(鼠标点击回调)并立即执行。计时器将等到下一个可能的时间才能执行。执行单击事件时,将放弃setInterval调用。在20毫秒时,第二个setInterval也到期。因为此时点击事件已经占用了线程,所以setInterval还是执行不了,而因为此时队列中已经有setInterval在排队等待执行,所以这次调用setInterval会被丢弃。浏览器不会多次将相同的setInterval处理程序添加到挂起的执行队列。事实上,我们可以看到,当第三个间隔回调被触发时,间隔本身正在执行。这向我们展示了一个重要的事实:intervals不关心当前正在执行什么,它们会被无差别地排队,即使这意味着回调之间的时间间隔会被牺牲掉。setTimeout/setInterval不能保证回调函数会按时执行最后,第二次interval回调执行完后,我们可以看到JavaScript引擎已经没有什么可以执行的了。这意味着浏览器现在正在等待新的异步事件发生。当间隔再次触发时,我们将获得50毫秒的值。但这一次,没有什么能阻止它的执行,所以它立即开火。OK,总的来说,JS定时器之所以不可靠,是因为JavaScript是单线程的,一次只能执行一个任务,而setTimeout()的第二个参数(延迟时间)只是告诉JavaScript要等多久当前任务到队列。如果队列为空,则添加的代码会立即执行;如果队列不为空,则等待前面的代码执行完毕,再执行定时器任务。它必须等待主线程任务执行完毕后才能开始执行,无论是否到了我们设定的时间,这里我们可以进一步了解Javascript的事件循环。JavaScript中所有的任务都分为同步任务和异步任务。同步任务,顾名思义,就是立即执行的任务。一般直接进入主线程执行。.而我们的异步任务进入任务队列,等待主线程中的任务执行完毕后再执行。任务队列是一个事件队列,表示相关的异步任务可以进入执行栈。主线程读取任务队列来读取其中的事件。队列是一种先进先出的数据结构。上面我们提到异步任务可以分为宏任务和微任务,所以任务队列也可以分为宏任务队列和微任务队列。UI渲染等;MicrotaskQueue:针对小任务,常见的有Promise、Process.nextTick;同步任务直接放入主线程执行,异步任务(点击事件、定时器、ajax等)挂在后台执行,等待一个I/O事件完成或者action事件触发。系统在后台执行异步任务。如果一个异步任务事件(或行为事件被触发),该任务将被添加到任务队列中,每个任务将被相应的回调函数处理。这里,异步任务分为宏任务和微任务。宏任务进入宏任务队列,微任务进入微任务队列。执行任务队列中的任务具体在执行栈中完成。当主线程中的所有任务执行完毕后,读取微任务队列。如果有microtasks,就会全部执行,然后读取上面的macrotaskqueue。这个过程会不断重复,也就是我们常说的事件循环(Event-Loop)。这里更详细的可以参考我之前的文章探究JavaScript执行机制导致定时器不可靠的原因太长会导致异步代码的执行延迟。setTimeout(()=>{console.log(1);},20);for(leti=0;i<90000000;i++){}setTimeout(()=>{console.log(2);},0);这应该按预期先打印2然后打印1,但事实并非如此。即使第二个定时器更短,中间for循环的执行时间也远远长于两个定时器设定的时间。setTimeout设置的回调任务按顺序加入延迟队列。任务执行完成后,ProcessDelayTask函数会根据启动时间和延迟时间计算出到期任务,然后依次执行这些到期任务。前面的任务执行完后,上面例子中的两个setTimeout已经过期了,所以按顺序执行就是打印1和2。所以在这种场景下,setTimeout就不是那么靠谱了。延迟执行时间有一个最大值。包括IE、Chrome、Safari和Firefox在内的浏览器以32位有符号整数存储延迟。如果延迟大于2147483647毫秒(约24.8天),这将导致溢出,导致立即执行计时器。(MDN)当setTimeout的第二个参数设置为0时(不设置时默认为0,小于0,大于2147483647),表示立即执行,或者尽快执行。setTimeout(function(){console.log("你猜什么时候打印?")},2147483648);把这段代码放到浏览器控制台执行,你会发现猜对了就会马上打印出来Print?最小延迟>=4ms(嵌套定时器)在浏览器中,setTimeout()/setInterval()的每次定时器调用的最小间隔为4ms,这通常是函数嵌套(嵌套层数达到一定深度)造成的,或者它是因为已经执行过的setInterval的回调函数被阻塞了。当setTimeout的第二个参数设置为0时(不设置默认为0,小于0,大于2147483647),表示立即执行,或者尽快执行。如果延迟时间小于0,则将延迟时间设置为0。如果定时器嵌套5次以上且延迟时间小于4ms,则将延迟时间设置为4ms。函数cb(){f();设置超时(cb,0);}setTimeout(cb,0);在Chrome和Firefox中,定时器的第5次调用被阻止;在Safari中是第6位;Edge是第3次。因此,后面的定时器至少会延迟4ms。非活动选项卡的最小时间延迟为>=1000毫秒。为了优化后台选项卡的加载损耗(并降低功耗),浏览器在非活动选项卡中设置了计时器。最小延迟限制为1S(1000ms)。letnum=100;functionsetTime(){//计时在当前秒执行console.log(`Currentseconds:${newDate().getSeconds()}-Executiontimes:${100-num}`);数?num--&&setTimeout(()=>setTime(),50):"";}setTime();这里我在39秒切换到其他tab,我们会发现后面的执行间隔是1秒执行一次,而不是我们设置的50ms。setInterval的处理时间不能长于设置的时间间隔。setInterval的处理时间不能长于设置的时间间隔。否则,setInterval会无间隔地重复执行。但是对于这个问题,在很多情况下,我们并不能清楚地控制处理过程。消耗的时间,为了按照一定的间隔周期性的触发定时器,我们可以使用setTimeout代替setInterval执行。setTimeout(functionfn(){//todosetTimeout(fn,10)//执行完处理程序内容后,最后每隔10毫秒调用程序,这样可以保证周期性调用10毫秒,这里的时间根据自己的需要写},10)解决方法1:requestAnimationFramewindow.requestAnimationFrame()告诉浏览器你要执行一个动画,让浏览器调用指定的回调函数来更新动画在下一次重绘之前。该方法需要传入一个回调函数作为参数,在浏览器下次重绘之前执行。理想情况下,回调函数的执行次数通常是每秒60次(也就是我们所说的60fsp),也就是每16.7ms执行一次,但不一定保证是16.7ms。constt=Date.now()functionmySetTimeout(cb,delay){letstartTime=Date.now()loop()functionloop(){if(Date.now()-startTime>=delay){cb();}返回;}requestAnimationFrame(loop)}}mySetTimeout(()=>console.log('mySetTimeout',Date.now()-t),2000)//2005setTimeout(()=>console.log('SetTimeout',Date.now()-t),2000)//2002这个方案好像增加了错误,因为requestAnimationFrame每16.7ms执行一次,所以不适合间隔小的timer校正。方法2:WebWorkerWebWorker为Web内容提供了一种在后台线程中运行脚本的简便方法。线程可以在不干扰用户界面的情况下执行任务。此外,它们可以使用XMLHttpRequest执行I/O(尽管responseXML和通道属性始终为空)。创建后,worker可以将消息发送到创建它的JavaScript代码,方法是将消息发布到该代码指定的事件处理程序(反之亦然)。WebWorker的作用是为JavaScript创建一个多线程环境,让主线程创建Worker线程,并将一些任务分配给后者运行。主线程运行的同时,Worker线程在后台运行,互不干扰。等到Worker线程完成计算任务,再将结果返回给主线程。这样做的好处是,一些计算密集型或高延迟的任务由Worker线程承担,不会阻塞或拖慢主线程。//index.jsletcount=0;//耗时任务setInterval(function(){leti=0;while(i++<100000000);},0);//workerletworker=newWorker('./worker.js')//worker.jsletstartTime=newDate().getTime();letcount=0;setInterval(function(){count++;console.log(count+'---'+(newDate().getTime()-(startTime+count*1000)));},1000);这个方案整体体验还是比较好的,不仅可以很大程度的修正定时器,也不会因为js单线程的特性影响到主进程任务汇总,所以会有事件排队,先进先出-out,setInterval调用会被丢弃,定时器不能保证回调函数的及时执行,会出现连续执行setInterval的情况。推荐阅读Reflow&Repaint(回流重绘)简介,以及如何优化?\这些浏览器面试题,看你能答对几个?\这次带你彻底理解前端本地存储\面试官:说说前端路由和后端路由的区别\JavaScript原型和原型链\Java深入作用域和闭包\this指向还有call,apply,bind觉得文章不错可以点个赞哦^_^另外,欢迎关注留言交流~关注公众号,获取更多精选文章~