从头认识JavaScript的事件循环模型

1. JS的运⾏机制

介绍

众所周知JavaScript是⼀⻔单线程的语⾔,所以在JavaScript的世界中默认的情况下同⼀个时间节点只能做⼀件事情,这样的设定就造成了JavaScript这⻔语⾔的⼀些局限性,⽐如在我们的⻚⾯中加载⼀些远程数据时,如果按照单线程同步的⽅式运⾏,⼀旦有HTTP请求向服务器发送,就会出现等待数据返回之前⽹⻚假死的效果出现。因为JavaScript在同⼀个时间只能做⼀件事,这就导致了⻚⾯渲染和事件的执⾏,在这个过程中⽆法进⾏。显然在实际的开发中我们并没有遇⻅过这种情况。

关于同步和异步

基于以上的描述,我们知道在JavaScript的世界中,应该存在⼀种解决⽅案,来处理单线程造成的诟病。这就是同步【阻塞】和异步【⾮阻塞】执⾏模式的出现。

同步(阻塞):

同步的意思是JavaScript会严格按照单线程(从上到下、从左到右的⽅式)执⾏代码逻辑,进⾏代码的解释和运⾏,所以在运⾏代码时,不会出现先运⾏4、5⾏的代码,再回头运⾏1、3⾏的代码这种情况。⽐如下列操作。

var a = 1
var b = 2
var c = a + b
//这个例⼦c⼀定是3,不会出现先执⾏第三⾏然后在执⾏第⼆⾏和第⼀⾏的情况
console.log(c)

接下来通过下列的案例升级⼀下代码的运⾏场景:

var a = 1
var b = 2
var d1 = new Date().getTime()
var d2 = new Date().getTime()
while(d2-d1<2000){
d2 = new Date().getTime()
}
//这段代码在输出结果之前⽹⻚会进⼊⼀个类似假死(等待)的状态
console.log(a+b)

当我们按照顺序执⾏上⾯代码时,我们的代码在解释执⾏到第4⾏时,还是正常的速度执⾏,但是在下⼀⾏就会进⼊⼀个持续的循环中。d2和d1在⾏级间的时间差仅仅是毫秒内的差别,所以在执⾏到while循环的时候d2-d1的值⼀定⽐2000⼩,那么这个循环会执⾏到什么时候呢?由于每次循环时,d2都会获取⼀次当前的时间发⽣变化,直到d2-d1==2000等情况,这时也就是正好过了2秒的时间,我们的程序才能跳出循环,进⽽再输出a+b的结果。那么这段程序的实际执⾏时间⾄少是2秒以上。这就导致了程序阻塞的出现,这也是为什么将同步的代码运⾏机制叫做阻塞式运⾏的原因。

阻塞式运⾏的代码,在遇到消耗时间的代码⽚段时,之后的代码都必须等待耗时的代码运⾏完毕,才能得到执⾏资源,这就是单线程同步的特点。

异步(⾮阻塞):

在上⾯的阐述中,我们明⽩了单线程同步模型中的问题所在,接下来引⼊单线程异步模型的介绍。异步的意思就是和同步对⽴,所以异步模式的代码是不会按照默认顺序执⾏的。JavaScript执⾏引擎在⼯作时,仍然是按照从上到下从左到右的⽅式解释和运⾏代码。在解释时,如果遇到异步模式的代码,引擎会将当前的任务“挂起”并略过。也就是先不执⾏这段代码,继续向下运⾏⾮异步模式的代码,那么什么时候来执⾏同步代码呢?直到同步代码全部执⾏完毕后,程序会将之前“挂起”的异步代码按照“特定的顺序”来进⾏执⾏,所以异步代码并不会【阻塞】同步代码的运⾏,并且异步代码并不是代表进⼊新的线程同时执⾏,⽽是等待同步代码执⾏完毕再进⾏⼯作。

我们阅读下⾯的代码分析:

var a = 1
var b = 2
setTimeout(function(){
console.log('输出了⼀些内容')
},2000)
//会直接输出3并且等待2秒左右的时间在输出function内部的内容
console.log(a+b)

这段代码的setTimeout定时任务规定了2秒之后执⾏⼀些内容,在运⾏当前程序执⾏到setTimeout时,并不会直接执⾏内部的回调函数,⽽是会先将内部的函数在另外⼀个位置(具体是什么位置下⾯会介绍)保存起来,然后继续执⾏下⾯的console.log进⾏输出,输出之后代码执⾏完毕,然后等待⼤概2秒左右,之前保存的函数再执⾏。⾮阻塞式运⾏的代码,程序运⾏到该代码⽚段时,执⾏引擎会将程序保存到⼀个暂存区,等待所有同步代码全部执⾏完毕后,⾮阻塞式的代码会按照特定的执⾏顺序,分步执⾏。这就是单线程异步的特点。

通俗的讲:
通俗的讲,同步和异步的关系是这样的:

同步的例⼦】:⽐如我们去面包店买面包,买面包的这个流程就是同步的。每个⼈必须按照来的时间,先后进⾏排队,而店员更具你们的顺序进行售卖面包,第一个人没买完,第二个人就需等待,这个就是⼀个阻塞流程。如果排队过程中第⼀个⼈在买面包时出了问题,这样更换时间就会追加到这个⼈身上,直到他顺利的买完,第⼆个⼈才能轮到。如果在这过程中如果面包买完了或者收银系统出现了问题,其他人就没办法继续下去。

异步的例⼦】:还是结合⽣活中,当我们进餐馆吃饭时,这个场景就属于⼀个完美的异步流程场景。每⼀桌来的客⼈会按照他们来的顺序进⾏点单,假设只有⼀个服务员的情况,点单必须按照先后顺序,但是服务员不需要等第⼀桌客⼈点好的菜出锅上菜,就可以直接去收集第⼆桌第三桌客⼈的需求。这样可能在⼗分钟之内,服务员就将所有桌的客⼈点菜的菜单统计出来,并且发送给了后厨。之后的菜也不会按照点餐顾客的课桌顺序,因为后厨收集到菜单之后可能有1,2,3桌的客⼈都点了锅包⾁,那么他可能会先⼀次出三份锅包⾁,这样锅包⾁在上菜的时候1,2,3桌的客⼈都能得到,并且其他的菜也会乱序的逐⼀上菜,这个过程就是异步的。如果按照同步的模式点餐,默认在饭店点菜就会出现饭店在第⼀桌客⼈上满菜之前第⼆桌之后的客⼈就只能等待连单都不能点的状态。

总结:

JavaScript的运⾏顺序就是完全单线程的异步模型:同步在前,异步在后。所有的异步任务都要等待当前的同步任务执⾏完毕之后才能执⾏。请看下⾯的案例:

var a = 1
var b = 2
var d1 = new Date().getTime()
var d2 = new Date().getTime()
setTimeout(function(){
console.log('我是⼀个异步任务')
},1000)
while(d2-d1<2000){
d2 = new Date().getTime()
}
//这段代码在输出3之前会进⼊假死状态,'我是⼀个异步任务'⼀定会在3之后输出
console.log(a+b)

观察上⾯的程序我们实际运⾏之后就会感受到单线程异步模型的执⾏顺序了,并且这⾥我们会发现setTimeout设置的时间是1000毫秒但是在while的阻塞2000毫秒的循环之后并没有等待1秒⽽是直接输出了我是⼀个异步任务,这是因为setTimout的时间计算是从setTimeout()这个函数执⾏时开始计算的。

JS的线程组成

上⾯我们通过⼏个简单的例⼦⼤概了解了⼀下JS的运⾏顺序,那么为什么是这个顺序,这个顺序的执⾏原理是什么

样的,我们应该如何更好更深的探究真相呢?这⾥需要介绍⼀下浏览器中⼀个Tab⻚⾯的实际线程组成。

在了解线程组成前要了解⼀点,虽然浏览器是单线程执⾏JavaScript代码的,但是浏览器实际是以多个线程协助操

作来实现单线程异步模型的,具体线程组成如下:

  1. GUI渲染线程

  2. JavaScript引擎线程

  3. 事件触发线程

  4. 定时器触发线程

  5. http请求线程

  6. 其他线程

按照真实的浏览器线程组成分析,我们会发现实际上运⾏JavaScript的线程其实并不是⼀个,但是为什么说

JavaScript是⼀⻔单线程的语⾔呢?因为这些线程中实际参与代码执⾏的线程并不是所有线程,⽐如GUI渲染线程为

什么单独存在,这个是防⽌我们在html⽹⻚渲染⼀半的时候突然执⾏了⼀段阻塞式的JS代码⽽导致⽹⻚卡在⼀半停

住这种效果。在JavaScript代码运⾏的过程中实际执⾏程序时同时只存在⼀个活动线程,这⾥实现同步异步就是靠

多线程切换的形式来进⾏实现的。

所以我们通常分析时,将上⾯的细分线程归纳为下列两条线程:

  1. 【主线程】:这个线程⽤了执⾏⻚⾯的渲染,JavaScript代码的运⾏,事件的触发等等

  2. 【⼯作线程】:这个线程是在幕后⼯作的,⽤来处理异步任务的执⾏来实现⾮阻塞的运⾏模式

2. JavaScript的运⾏模型

image.png

上图是JavaScript运⾏时的⼀个⼯作流程和内存划分的简要描述,我们根据图中可以得知主线程就是我们JavaScript执⾏代码的线程,主线程代码在运⾏时,会按照同步和异步代码将其分成两个去处,如果是同步代码执⾏,就会直接将该任务放在⼀个叫做“函数执⾏栈”的空间进⾏执⾏,执⾏栈是典型的【栈结构】(先进后出),程序在运⾏的时候会将同步代码按顺序⼊栈,将异步代码放到【⼯作线程】中暂时挂起,【⼯作线程】中保存的是定时任务函数、JS的交互事件、JS的⽹络请求等耗时操作。当【主线程】将代码块筛选完毕后,进⼊执⾏栈的函数会按照从外到内的顺序依次运⾏,运⾏中涉及到的对象数据是在堆内存中进⾏保存和管理的。当执⾏栈内的任务全部执⾏完毕后,执⾏栈就会清空。执⾏栈清空后,“事件循环”就会⼯作,“事件循环”会检测【任务队列】中是否有要执⾏的任务,那么这个任务队列的任务来源就是⼯作线程,程序运⾏期间,⼯作线程会把到期的定时任务、返回数据的http 任务等【异步任务】按照先后顺序插⼊到【任务队列】中,等执⾏栈清空后,事件循环会访问任务队列,将任务队列中存在的任务,按顺序(先进先出)放在执⾏栈中继续执⾏,直到任务队列清空。

从代码⽚段开始分析

functiontask1(){
    console.log('第⼀个任务')
}
functiontask2(){
    console.log('第⼆个任务')
}

functiontask3(){
    console.log('第三个任务')
}
functiontask4(){
    console.log('第四个任务')
}
task1()
setTimeout(task2,1000)
setTimeout(task3,500)
task4()

刚才的⽂字阅读可能在⼤脑中很难形成⼀个带动画的图形界⾯来帮助我们分析JavaScript的实际运⾏思路,接下来
我们将这段代码肢解之后详细的研究⼀下。

按照字⾯分析:

按照字⾯分析,我们创建了四个函数代表4个任务,函数本身都是同步代码。在执⾏的时候会按照1,2,3,4进⾏解析,解析过程中我们发现任务2和任务3被setTimeout进⾏了定时托管,这样就只能先运⾏任务1和任务4了。当
任务1和任务4运⾏完毕之后500毫秒后运⾏任务3,1000毫⽶后运⾏任务2。
那么他们在实际运⾏时⼜是经历了怎么样的流程来运⾏的呢?⼤概的流程我们以图解的形式分析⼀下。

图解分析:

image.png

如上图,在上述代码刚开始运⾏的时候我们的主线程即将⼯作,按照顺序从上到下进⾏解释执⾏,此时执⾏栈、⼯作线程、任务队列都是空的,事件循环也没有⼯作。接下来我们分析下⼀个阶段程序做了什么事情。

image.png
结合上图可以看出程序在主线程执⾏之后就将任务1、4和任务2、3分别放进了两个⽅向,任务1和任务4都是⽴即
执⾏任务所以会按照1->4的顺序进栈出栈(这⾥由于任务1和2是平⾏任务所以会先执⾏任务1的进出栈再执⾏任务
4的进出栈),⽽任务2和任务3由于是异步任务就会进⼊⼯作线程挂起并开始计时,并不影响主线程运⾏,此时的
任务队列还是空置的。

image.png

我们发现同步任务的执⾏速度是⻜快的,这样⼀下执⾏栈已经空了,⽽任务2和任务3还没有到时间,这样我们的事
件循环就会开始⼯作等待任务队列中的任务进⼊,接下来就是执⾏异步任务的时候了。

image.png

我们发现任务队列并不是⼀下⼦就会将任务2和任务三⼀起放进去,⽽是哪个计时器到时间了哪个放进去,这样我
们的事件循环就会发现队列中的任务,并且将任务拿到执⾏栈中进⾏消费,此时会输出任务3的内容。

image.png

到这就是最后⼀次执⾏,当执⾏完毕后⼯作线程中没有计时任务,任务队列的任务清空程序到此执⾏完毕。

总结

我们通过图解之后脑⼦⾥就会更清晰的能搞懂异步任务的执⾏⽅式了,这⾥采⽤最简单的任务模型进⾏描绘复杂的任务在内存中的分配和⾛向是⾮常复杂的,我们有了这次的经验之后就可以通过观察代码在⼤脑中先模拟⼀次执⾏,这样可以更清晰的理解JS的运⾏机制。

关于执⾏栈

执⾏栈是⼀个栈的数据结构,当我们运⾏单层函数时,执⾏栈执⾏的函数进栈后,会出栈销毁然后下⼀个进栈下⼀

个出栈,当有函数嵌套调⽤的时候栈中就会堆积栈帧,⽐如我们查看下⾯的例⼦:

functiontask1(){
console.log('task1执⾏')
task2()
console.log('task2执⾏完毕')
}
functiontask2(){
console.log('task2执⾏')
task3()
console.log('task3执⾏完毕')
}
functiontask3(){
console.log('task3执⾏')
}
task1()
console.log('task1执⾏完毕')

我们根据字⾯阅读就能很简单的分析出输出的结果会是

/*
task1执⾏
task2执⾏
task3执⾏
task3执⾏完毕
task2执⾏完毕
task1执⾏完毕
*/

那么这种嵌套函数在执⾏栈中的操作流程是什么样的呢?

image.png
第⼀次执⾏的时候调⽤task1函数执⾏到console.log的时候先进⾏输出,接下来会遇到task2函数的调⽤会出现下⾯的情况:

image.png

执⾏到此时检测到task2中还有调⽤task3的函数,那么就会继续进⼊task3中执⾏,如下图:

image.png
在执⾏完task3中的输出之后task3内部没有其他代码,那么task3函数就算执⾏完毕那么就会发⽣出栈⼯作。

image.png
此时我们会发现task3出栈之后程序运⾏⼜会回到task2的函数中继续他的执⾏。接下来会发⽣相同的事情。

image.png
再之后就剩下task1⾃⼰了,他在task2销毁之后输出task2执⾏完毕后他也会随着出栈⽽销毁。

image.png
当task1执⾏完毕之后它随着销毁最后⼀⾏输出,就会进⼊执⾏栈执⾏并销毁,销毁之后执⾏栈和主线程清空。这个过程就会出现123321的这个顺序,⽽且我们在打印输出时,也能通过打印的顺序来理解⼊栈和出栈的顺序和流程。

关于递归

关于上⾯的执⾏栈执⾏逻辑清楚后,我们就顺便学习⼀下递归函数,递归函数是项⽬开发时经常涉及到的场景。我们经常会在未知深度的树形结构,或其他合适的场景中使⽤递归。那么递归在⾯试中也会经常被问到⻛险问题,如果了解了执⾏栈的执⾏逻辑后,递归函数就可以看成是在⼀个函数中嵌套n层执⾏,那么在执⾏过程中会触发⼤量的栈帧堆积,如果处理的数据过⼤,会导致执⾏栈的⾼度不够放置新的栈帧,⽽造成栈溢出的错误。所以我们在做海量数据递归的时候⼀定要注意这个问题。

关于执⾏栈的深度:

执⾏栈的深度根据不同的浏览器和JS引擎有着不同的区别,我们这⾥就Chrome浏览器为例⼦来尝试⼀下递归的溢出:

var i=0;
functiontask(){
i++
console.log(`递归了${i}次`)
task()
}
task()

我们发现在递归了11378次之后会提示超过栈深度的错误,也就是我们⽆法在Chrome或者其他浏览器做太深层的递归操作。
image.png

如何跨越递归限制

vari=0;
functiontask(){
i++
console.log(`递归了${i}次`)
//使⽤异步任务来阻⽌递归的溢出
setTimeout(function(){
task()
},0)
}
task()

我们发现只是做了⼀个⼩⼩的改造,这样就不会出现溢出的错误了。这是为什么呢?

图解原因

这个是因为我们这⾥使⽤了异步任务去调⽤递归中的函数,那么这个函数在执⾏的时候就不只使⽤栈进⾏执⾏了。

先看没有异步流程时候的执⾏图例:

image.png
再看有了异步任务的递归:

image.png
有了异步任务之后我们的递归就不会叠加栈帧了,因为放⼊⼯作线程之后该函数就结束了,可以出栈销毁,那么在执⾏栈中就永远都是只有⼀个任务在运⾏,这样就防⽌了栈帧的⽆限叠加,从⽽解决了⽆限递归的问题,不过异步递归的过程是⽆法保证运⾏速度的,在实际的⼯作场景中,如果考虑性能问题,还需要使⽤while循环等解决⽅案,来保证运⾏效率的问题,在实际⼯作场景中,尽量避免递归循环,因为递归循环就算控制在有限栈帧的叠加,其性能也远远不及指针循环。

3.宏任务和微任务

在明确了事件循环模型以及JavaScript的执⾏流程后,我们认识了⼀个叫做任务队列的容器,他的数据结构式队列的结构。所有除同步任务外的代码都会在⼯作线程中,按照他到达的时间节点有序的进⼊任务队列,⽽且任务队列中的异步任务⼜分为【宏任务】和【微任务】。

举个例⼦:

在了解【宏任务】和【微任务】前,还是哪⽣活中的实际场景举个例⼦:

⽐如在去银⾏办理业务时,每个⼈都需要在进⼊银⾏时找到取票机进⾏取票,这个操作会把来办理业务的⼈按照取票的顺序排成⼀个有序的队列。假设银⾏只开通了⼀个办事窗⼝,窗⼝的⼯作⼈员会按照排队的顺序进⾏叫号,到达号码的⼈就可以前往窗⼝办理业务,在第⼀个⼈办理业务的过程中,第⼆个以后的⼈都需要进⾏等待。这个场景与JavaScript的异步任务队列执⾏场景是⼀模⼀样的,如果把每个办业务的⼈当作JavaScript中的每⼀个异步的任务,那么取号就相当于将异步任务放⼊任务队列。银⾏的窗⼝就相当于【函数执⾏栈】,在叫号时代表将当前队列的第⼀个任务放⼊【函数执⾏栈】运⾏。这时可能每个⼈在窗⼝办理的业务内容各不相同,⽐如第⼀个⼈仅仅进⾏开卡的操作,这样银⾏⼯作⼈员就会为其执⾏开卡流程,这就相当于执⾏异步任务内部的代码。如果第⼀个⼈的银⾏卡开通完毕,银⾏的⼯作⼈员不会⽴即叫第⼆个⼈过来,⽽是会询问第⼀个⼈,“您是否需要为刚才开通的卡办理⼀些增值业务,⽐如做个活期储蓄。”,这时相当于在原始开卡的业务流程中临时追加了⼀个新的任务,按照JavaScript的执⾏顺序,这个⼈的新任务应该回到取票机拿取⼀张新的号码,并且在队尾重新排队,这样⼯作的话办事效率就会急剧下降。所以银⾏实际的做法是在叫下⼀个⼈办理业务前,如果前⾯的⼈临时有新的业务要办理,⼯作⼈员会继续为其办理业务,直到这个⼈的所有事情都办理完毕。从取卡到办理追加业务完成的这个过程,就是微任务的实际体现。在JavaScript运⾏环境中,包括主线程代码在内,可以理解为所有的任务内部都存在⼀个微任务队列,在每下⼀个宏任务执⾏前,事件循环系统都会先检测当前的代码块中是否包含已经注册的微任务,并将队列中的微任务优先执⾏完毕,进⽽执⾏下⼀个宏任务。所以实际的任务队列的结构是这样的,如图:

image.png

1、宏任务与微任务的介绍

由上述内容得知JavaScript中存在两种异步任务,⼀种是宏任务⼀种是微任务,他们的特点如下:

宏任务:

宏任务是JavaScript中最原始的异步任务,包括setTimeout、setInterVal、AJAX等,在代码执⾏环境中按照同步代码的顺序,逐个进⼊⼯作线程挂起,再按照异步任务到达的时间节点,逐个进⼊异步任务队列,最终按照队列中的顺序进⼊函数执⾏栈进⾏执⾏。

微任务:

微任务是随着ECMA标准升级提出的新的异步任务,微任务在异步任务队列的基础上增加了【微任务】的概念,每⼀个宏任务执⾏前,程序会先检测中是否有当次事件循环未执⾏的微任务,优先清空本次的微任务后,再执⾏下⼀个宏任务,每⼀个宏任务内部可注册当次任务的微任务队列,再下⼀个宏任务执⾏前运⾏,微任务也是按照进⼊队列的顺序执⾏的。

总结:

在JavaScript的运⾏环境中,代码的执⾏流程是这样的:

1. 默认的同步代码按照顺序从上到下,从左到右运⾏,运⾏过程中注册本次的微任务和后续的宏任务:
2. 执⾏本次同步代码中注册的微任务,并向任务队列注册微任务中包含的宏任务和微任务
3. 将下⼀个宏任务开始前的所有微任务执⾏完毕
4. 执⾏最先进⼊队列的宏任务,并注册当次的微任务和后续的宏任务,宏任务会按照当前任务队列的队尾继续向下排列。

常⻅的宏任务和微任务划分

宏任务

#浏览器
I/O
setTimeout
setInterval
setImmediate×
requestAnimationFrame
Node
×

有些地方会列出来UI Rendering,说这个也是宏任务,可是在读了HTMNL规范文档以后,发现这很显然是和微任务平行的一个操作步骤,requestAnimationFrame姑且也算是宏任务吧,requestAnimationFrame在MDN定以为,下次页面重绘钱所执行的操作,而重绘也是作为宏任务的一个步骤来存在的,且该步骤晚于微任务的执行。

微任务

#浏览器
process.nextTick×
MutationObserver
Promise.then catch finally
Node
×

示例1

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');
复制代码

结果:

script start -->  script end-->  promise1-->  promise2-->  setTimeout
复制代码

对应的处理过程则是:

  1. 执行 console.log (输出 script start)
  2. 遇到 setTimeout 加入外部队列
  3. 遇到两个 Promise 的 then 加入内部队列
  4. 遇到 console.log 直接执行(输出 script end)
  5. 内部队列中的任务挨个执行完 (输出 promise1 和 promise2)
  6. 外部队列中的任务执行 (输出 setTimeout)

下面是setTimeout和requestAnimationFrame两个宏任务,这⾥的运⾏结果是多种情况。如果三个宏任务都为setTimeout的话会按照代码编写的顺序执⾏宏任务,⽽中间包含了⼀个requestAnimationFrame,这⾥就要学习⼀下他们的执⾏时机了。setTimeout是在程序运⾏到setTimeout时⽴即注册⼀个宏任务,所以两个setTimeout的顺序⼀定是固定的timer1和timer2会按照顺序输出。⽽requestAnimationFrame是请求下⼀次重绘事件,所以他的执⾏频率要参考浏览器的刷新率。
参考如下代码:

leti=0;
letd=newDate().getTime()

letd1=newDate().getTime()
functionloop(){
d1=newDate().getTime()
i++
//当间隔时间超过1秒时执⾏

if((d1-d)>=1000){
d=d1
console.log(i)
i=0
console.log('经过了1秒')
}
requestAnimationFrame(loop)
}
loop()

该代码在浏览器运⾏时,控制台会每间隔1秒进⾏⼀次输出,输出的i就是loop函数执⾏的次数,如下图:

image.png
这个输出意味着requestAnimationFrame函数的执⾏频率是每秒钟60次左右,他是按照浏览器的刷新率来进⾏执⾏的,也就是当屏幕刷新⼀次时该函数就会触发⼀次,相当于运⾏间隔是16毫秒左右。

继续参考下列代码:

leti=0;

letd=newDate().getTime()

letd1=newDate().getTime()
functionloop(){
d1=newDate().getTime()
i++
if((d1-d)>=1000){
d=d1
console.log(i)
i=0
console.log('经过了1秒')
}
setTimeout(loop,0)
}
loop()

该代码结构与上⾯的案例类似,循环是采⽤setTimeout进⾏控制的,所以参考运⾏结果,如图:

image.png
根据运⾏结果得知,setTimeout(fn,0)的执⾏频率是每秒执⾏200次左右,所以他的间隔是5毫秒左右。

由于这两个异步的宏任务出发时机和执⾏频率不同,会导致三个宏任务的触发结果不同,如果我们打开⽹⻚时,恰
好赶上5毫秒内执⾏了⽹⻚的重绘事件,requestAnimationFrame在⼯作线程中就会到达触发时机优先进⼊任务队
列,所以此时会输出:UI update->timer1->timer2。
⽽当打开⽹⻚时上⼀次的重绘刚结束,下⼀次重绘的触发是16毫秒后,此时setTimeout注册的两个任务在⼯作线程中就会优先到达触发时机,这时输出的结果是:timer1->timer2->UI update。

所以此案例的运⾏结果如下2图所示:

image.png

image.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值