当前位置: 首页 > 科技观察

setTimeout的实现原理和使用注意

时间:2023-03-22 10:14:52 科技观察

setTimeout,它是一个定时器,用于指定某个函数执行后多少毫秒。setTimeout用法vartimeoutID=setTimeout(function[,delay,arg1,arg2,...]);vartimeoutID=setTimeout(function[,delay]);vartimeoutID=setTimeout(code[,delay]);第一个参数是function或者Executablestring(比如alert('test'),不推荐这种方式)第二个参数是延迟时间,单位为毫秒,可选,默认为0。第三个及以后的参数为入参的功能。setTimeout的返回值是一个数字,这个值就是timeoutID,可以用来取消定时器。setTimeout在浏览器中的实现浏览器渲染进程中所有运行在主线程上的任务都需要先加入到消息队列中,然后事件循环系统依次执行消息队列中的任务。Chrome中除了正常的消息队列之外,还有一个消息队列(我们可以称之为延迟队列)。这个队列维护了一个需要延迟的任务列表,包括定时器和一些需要延迟的内部Chromium。任务。所以当一个定时器被JavaScript创建时,渲染进程会将定时器的回调任务添加到延迟队列中。比如这样一段代码:functionfoo(){console.log("test")}vartimeoutID=setTimeout(foo,100);当通过JavaScript调用setTimeout设置回调函数时,渲染进程会创建一个回调任务,包括回调函数foo、当前启动时间、延迟执行时间等,其模拟代码如下:structDelayTask{int64id;CallBackFunctioncbf;intstart_time;intdelay_time;};DelayTasktimerTask;timerTask.cbf=foo;timerTask.start_time=getCurrentTime();//获取当前时间timerTask.delay_time=100;//设置延迟执行时间回调任务创建后,任务会被添加到延迟执行队列中。这个回调任务什么时候执行?浏览器中有一个函数专门用来处理延迟执行的任务。暂且称之为ProcessDelayTask。其主要逻辑如下:voidProcessTimerTask(){//从delayed_incoming_queue中获取过期的定时器任务//依次执行这些任务}TaskQueuetask_queue;voidProcessTask();boolkeep_running=true;voidMainTherad(){for(;;){//执行消息队列中的任务Tasktask=task_queue.takeTask();ProcessTask(task);//执行延迟队列中的任务ProcessDelayTask()if(!keep_running)//如果设置了退出标志,则直接退出线程loopbreak;}}其实当浏览器处理完消息队列中的一个任务后,就会开始执行ProcessDelayTask函数。ProcessDelayTask函数会根据启动时间和延迟时间计算到期任务,然后依次执行这些到期任务。完成应有的任务后,继续进行下一个循环过程。这样就实现了定时器,从这个过程可以很明显的看出,定时器不一定按时执行,延时执行。注意:如果当前任务执行时间过长,过期的定时器任务会延迟执行。使用setTimeout时,有很多因素会导致回调函数的执行时间比设置的期望值长,其中之一就是上面提到的,如果当前任务的处理时间过长,则设置的任务计时器将被延迟。比如在浏览器中执行这样一段代码,打印执行时间:functionbar(){console.log('bar')constendTime=Date.now()console.log('costtime',endTime-startTime)}functionfoo(){setTimeout(bar,0);for(leti=0;i<5000;i++){leti=5+8+8+8console.log(i)}}foo()的执行结果如图图中:从结果我们可以看到,执行foo函数耗时为365毫秒,也就是说setTimeout设置的任务延迟了365毫秒,setTimeout设置的回调延迟时间为0.2.setTimeout设置的回调函数中的this环境没有指向回调函数,比如这段代码:varname=1;varMyObj={name:2,test:1,showName:function(){console.log(this.name,this.test);}}setTimeout(MyObj.showName,1000)MyObj.showName()//这里先输出21//1s再输出1undefined。其实这里仔细分析一下,很容易理解这个this的方向。按照this的规定,如果是对象调用(obj.fn()),那么this指向的就是对象,所以MyObj.showName()输出的是MyObj中的值。在setTimeout中,入参是MyObj.showName,这里传入这个值,可以理解为:constfn=MyObj.showNamesetTimeout(fn,1000)这样,在setTimeout中,执行的时候,其实就是在window下执行fn,此时this指向的是window,而不是原来的函数。3、setTimeout存在嵌套调用问题如果setTimeout存在嵌套调用,调用超过5次后,系统会设置最小执行时间间隔为4毫秒。我们可以在浏览器中粗略测试一下,代码如下:0);}setTimeout(cb,0);执行结果:从结果可以看出,前5次调用的时间间隔比较小,嵌套调用的次数都在5次以上,后面每次调用的最小时间间隔为4毫秒(我作为结果的操作,间隔基本上是5ms,考虑到代码执行的计算误差)。这样做的原因是,在Chrome中,如果定时器被嵌套调用超过5次,系统会判断该函数方法被阻塞。如果调用定时器的间隔小于4毫秒,浏览器会将调用间隔设置为4毫秒。可以看看源码(https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/frame/dom_timer.cc)staticconstintkMaxTimerNestingLevel=5;//Chromiumusesaminimumtimerintervalof4ms.We'dliketogo//lower;然而,有一些编码不佳的网站//创建CPU-spinningloops。使用4ms防止CPU//spinningtoobusiness并在CPUspinning和//thesmallestpossibleintervaltimer.staticconstexprbase::TimeDeltakMinimumInterval=base::TimeDelta::FromMilliseconds(4)之间提供平衡;为JavaScript动画使用setTimeout不一定是个好主意。4.对于非活动页面,setTimeout执行的最小间隔为1000毫秒。如果标签不是当前活动标签,则定时器的最小时间间隔为1000毫秒。目的是优化后台页面的加载损耗,降低功耗。使用定时器时要注意这一点。5.延迟执行时间有最大值。Chrome、Safari和Firefox都使用32位来存储延迟值。32位最多只能存储2147483647毫秒,也就是说如果setTimeout设置的延迟值大于2147483647毫秒(约24.8天)就会溢出,导致定时器立即执行。如:letstartTime=Date.now()functionfoo(){constendTime=Date.now()console.log('costtime',endTime-startTime)console.log("test")}vartimerID=setTimeout(foo,2147483648);//会立即调用执行执行结果:运行后可以看到这段代码是立即执行的。但是如果你把延迟值修改成小于2147483647毫秒的东西,那么执行就没有问题了。