浏览器中的事件循环机制【看完就懂】
什么是事件循环机制
相信大家看过很多类似下面这样的代码:
function printNumber(){
console.log('printNumber');
}
setTimeout(function(){
console.log('setTimeout 1000')
}, 1000);
setTimeout(function(){
console.log('setTimeout 0')
});
printNumber();
new Promise((resolve, reject) => {
console.log('new promise');
}).then(function(){
console.log('promise resolve');
})
然后让我们说出这段代码的输出结果,那这段代码的输出结果其实就是由事件循环机制决定的。
我们都知道JS引擎线程是专门用来解析JavaScript脚本的,所有的JavaScript代码都由这一个线程来解析。然而这个JS引擎是单线程的,也就意味着JavaScript程序在执行时,前面的必须处理好,后面的才会执行。
但是JavaScript中除了一些顺序执行的逻辑代码,还有很多异步任务,比如Ajax请求、定时器等。如果JS引擎在单线程解析JavaScript时遇到了一个Ajax请求,那就必须等Ajax请求返回结果才能继续执行后续的代码,很显然这样的行为是非常低效的。
那为了解决这样的问题,事件循环机制这样的技术就显得尤为重要:
"JS引擎"在顺序执行"JavaScript"代码时,如果遇到"同步代码"立即执行;
如果遇到一些"异步任务"就会将这个"异步任务"交给对应的模块处理,然后继续执行后续代码;
当一个"异步任务"到达触发条件时就将该"异步任务"的回调放入"任务队列"中;
当"JS引擎"空闲以后,就会从"任务队列"读取和执行异步任务;
补充内容:
1.
JS引擎线程也被称为执行JS代码的主线程,后续如果出现主线程这样的描述,指的就是JS引擎线程。
2.任务队列属于数据结构中的队列,特性是先进先出。
3. 有关JavaScript中同步任务和异步任务的分类下面一节会介绍。
4. 本文只讨论浏览器环境下的事件循环机制,后续的描述和代码演示均基于浏览器环境(Node中的事件循环机制不做分析)。
JavaScript任务的分类
前面我们简单介绍过事件循环机制执行JS代码的顺序,那首先我们需要知道在JavaScript那些代码是同步任务,那些是异步任务。
接下来我们对JavaScript中的任务做一个分类:
这个分类很重要哦 不同的类型的任务执行顺序不同~
任务的执行顺序
接着事件循环机制中JS引擎对这些任务的执行顺序描述如下:
-
步骤一: 从
<script>代码开始,到</script>代码结束,按顺序执行所有的代码。 -
步骤二: 在
步骤一顺序执行代码的过程中,如果遇到同步任务,立即执行,然后继续执行后续代码;如果遇到异步任务,将异步任务交给对应的模块处理(事件交给事件处理线程,ajax交给异步HTTP请求线程),当异步任务到达触发条件以后将异步任务的回调函数推入任务队列(宏任务推入宏任务队列,微任务推入微任务队列)。 -
步骤三:
步骤一结束后,说明同步代码执行完毕。此时读取并执行微任务队列中保存的所有的微任务。 -
步骤四:
步骤三完成后读取并执行宏任务队列中的宏任务,每执行完一个宏任务就去查看微任务队列中是否有新增的微任务,如果存在则重复步骤三;如果不存在,继续执行下一个宏任务,直到。
一定要看的补充说明 !!!
1.步骤四中描述的
新增的微任务和步骤三中描述的微任务是一样的,因为异步任务只有满足条件以后才会被推入任务队列,步骤三在执行时,不一定所有的微任务都到达触发条件而被推入任务队列;2.所谓的
到达触发条件指的是下面这几种情况:
① 定时器:定时器设置的时间到达,才会将定时器的回调函数推入任务队列中
② DOM事件:DOM绑定的事件被触发以后,才会将事件的回调函数推入任务队列中
③ 异步请求:异步请求返回结果以后,才会将异步请求的回调函数推入任务队列中
④ 异步任务之间的互相嵌套:比如宏任务A嵌套微任务X,当宏任务A对应的回调函数代码没有被执行到的时候,很显然根本不存在微任务X;只有宏任务A对应的回调函数代码被执行以后,JS引擎才会解析到微任务X,此时依然是将该微任务X交给对应的线程去处理,当微任务X满足前面描述的①、②、③的条件,才会将微任务X对应的回调推入任务队列,等待JS引擎去执行。3.所有的
异步任务都在JS引擎遇到</script>以后才会开始执行。4.
宏任务对应的英文描述为task,微任务对应的英文描述为micro task;宏任务队列描述为task quene,微任务队列描述为micro task quene。不过很多文章也会将宏任务描述为macro task,这个没多大关系。只是有些文章会将micro task描述为微任务队列,就有些误导人了,本文为了避免描述上产生的问题,均用中文文字描述。
实践一波吧
到此事件循环机制的核心内容就讲完了,核心内容主要就两点:JavaScript任务分类和任务执行顺序。只要牢牢掌握这两点,就能解决大部分问题。
那接下来我们就来实践一下。
示例一
console.log('script start');
function printNumber(){
console.log('同步任务执行:printNumber');
}
setTimeout(function(){
console.log('宏任务执行:setTimeout 1000ms')
}, 1000);
printNumber();
new Promise((resolve, reject) => {
console.log('同步任务执行:new promise');
resolve();
}).then(function(){
console.log('微任务执行:promise resolve');
})
console.log('script end');
这段代码是文章开头贴出来的代码,相对来说比较简单,接下来就分析一下这段代码的执行顺序以及输出结果。
-
1.首先
js引擎从上到下开始执行代码 -
2.遇到
console.log直接打印:script start -
3.遇到
函数声明 -
4.遇到宏任务
setTimeout,交给定时器线程去处理(定时器线程会在1000ms后将setTimeout的回调函数:function(){ console.log('宏任务执行:setTimeout 1000ms') }推入宏任务队列,等待JS引擎去执行),之后JS引擎继续执行后续代码 -
5.遇到函数调用:
printNumber,立即执行并打印:同步任务执行:printNumber -
6.遇到
new Promise,new Promise构造传入的内容立即执行,所以打印:console.log('同步任务执行:new promise'); -
7.遇到
resolve执行promise.then,promise.then属于微任务,因此将promise.then的回调函数:function(){ console.log('微任务执行:promise resolve'); }推入微任务队列 -
8.再次遇到
console.log直接打印script end -
9.
步骤8完成,即说明同步任务执行完毕。此时就开始读取并执行微任务队列中所有的微任务。 在本例中就是执行步骤7中的promise.then,即打印:微任务执行:promise resolve。 -
10.本例中只有一个
微任务,因此步骤9完成以后开始执行宏任务,也就是步骤4中setTimeout的回调,即打印:宏任务执行:setTimeout 1000ms
注意:
setTimeout定时器设置的时间实际是推入任务队列的时间
经过以上的分析,得出来的打印顺序如下:
script start
同步任务执行:printNumber
同步任务执行:new promise
script end
微任务执行:promise resolve
宏任务执行:setTimeout 1000ms
最后在浏览器中验证一下:
示例二
接下来我们来看看下面这个稍微复杂一些的案例:
console.log('script start');
setTimeout(function(){
console.log('宏任务执行:setTimeout1 2000ms')
}, 2000);
setTimeout(function(){
console.log('宏任务执行:setTimeout2 0ms')
}, 0);
new Promise((resolve, reject) => {
console.log('同步代码执行: new Promise');
setTimeout(function(){
console.log('宏任务执行:setTimeout3 1000ms')
resolve();
}, 1000);
}).then(function(){
console.log('微任务执行:promise resolve')
});
console.log('script end');
分析执行过程:
-
1.
js引擎从上到下开始执行代码 -
2.遇到
console.log直接打印:script start -
3.遇到
宏任务setTimeout,交给定时器线程处理(定时器线程会在2000ms后将setTimeout的回调函数:function(){ console.log('宏任务执行:setTimeout1 2000s') }推入宏任务队列,等待JS引擎去执行),JS引擎继续执行后续代码 -
4.再次遇到
宏任务setTimeout,交给定时器线程处理(定时器线程会在0ms后将setTimeout的回调函数:function(){ console.log('宏任务执行:setTimeout2 0s') }推入宏任务队列,等待JS引擎去执行),JS引擎继续执行后续代码 -
5.遇到
new Promise,new Promise构造传入的内容立即执行,所以打印:console.log('同步任务执行:new promise');; -
6.接着发现
new Promise的构造函数存在一个宏任务setTimeout,所以依然是交给定时器线程处理(定时器线程会在1000ms后将改setTimeout的回调函数:function(){ console.log('宏任务执行:setTimeout3 1000ms') }推入宏任务队列,等待JS引擎去执行),JS引擎继续执行后续代码 -
7.遇到
console.log直接打印:script end -
8.
步骤7完成,即说明同步任务执行完毕。在这个过程中,没有产生微任务,所以微任务队列为空;同时在这个过程中产生了三个宏任务:setTimeout,按照定时器设置的时间,这三个宏任务推入宏任务队列的顺序为:setTimeout2 0ms、setTimeout3 1000ms、setTimeout1 2000ms,所以后续执行宏任务时先推入队列的任务先执行。(最先推入任务队列的称为队首的任务,任务执行完成后,就会从队首中移除,下一个任务就会称为队首任务) -
9.根据
步骤8的分析,执行完同步代码以后,本应该先执行微任务队列中的所有的微任务,但是因为并没有微任务存在,所以开始执行宏任务队列中队首的任务,即setTimeout2 0ms,所以会打印:宏任务执行:setTimeout2 0ms -
10.
步骤9结束以后,也就是执行完一个宏任务了;接下依然是执行微任务队列中的所有微任务,但是此时依然因为没有微任务存在,所以执行宏任务队列中的队首的那个任务,即setTimeout3 1000ms,所以会打印:宏任务执行:setTimeout2 0ms; 接着发现定时器setTimeout的回调函数中调用了resolve,因此产生了一个微任务:promise.then,该微任务会被推入微任务队列。 -
11.
步骤10结束以后,也是执行完一个宏任务了;接下还是执行微任务队列中的所有微任务,此时微任务队列中有一个微任务,是步骤9在执行的过程中产生的(这就是我们在前面说的任务之间的嵌套,只有外层任务的回调被执行后,内层的任务才会存在),所以执行该微任务,打印:微任务执行:promise resolve -
步骤10完成后,即执行完一个微任务;接着继续执行宏任务队列中队首的那个任务,即打印:setTimeout1 2000s
-
13.所有的
微任务、宏任务执行完毕,代码结束
最终的打印顺序:
script start
同步代码执行: new Promise
script end
宏任务执行:setTimeout2 0ms
宏任务执行:setTimeout3 1000ms
微任务执行:promise resolve
宏任务执行:setTimeout1 2000ms
浏览器在验证一下:
setImmediate和setTimeout 0
关于setImmediate的作用 MDN Web Docs 是这样介绍的:
从上面的描述我们可以获取到两个有用信息:
- 1.该方法是非标准的,目前只有最新版的
IE和Nodejs 0.10+支持 - 2.该方法提供的回调函数会在浏览器完成后面的其他语句后立即执行
关于第一点非常好理解,我自己也做过尝试,确实只有IE10以及更高的版本才能使用;
而第二点说的有点含糊,我个人理解为setImmediate的回调应该是在JS引擎执行完所有的同步代码以后立即执行的。
那不管如何理解,我们在IE浏览器中试试应该能得出更准确的结论。
以下所有的示例均在
IE11中进行测试
示例一
首先是一个最简单的示例:
console.log('script start');
setImmediate(function(){
console.log('宏任务执行:setImmediate');
})
console.log('script end');
这段代码的输出顺序如下:
从这个示例的结果可以看到setImmediate的回调函数确实是在同步代码执行完成后才执行的。这个结果能说明前面的理解是正确的吗?
先不要着急,我们在来看一个示例。
示例二
console.log('script start');
setImmediate(function(){
console.log('宏任务执行:setImmediate');
})
setTimeout(function(){
console.log('宏任务执行:setTimeout 0');
},0)
console.log('script end');
在这个示例中,我们写了一个setTimeout定时器,并且将时间设置为0。根据代码书写顺序,在将setImmediate推入宏任务队列以后,紧接着setTimeout的回调也会被推入宏任务队列,所以最终应该输出:
script start
script end
宏任务执行:setImmediate
宏任务执行:setTimeout 0
但是浏览器的输出结果并不是这样的:
示例三
console.log('script start');
setTimeout(function(){
setImmediate(function(){
console.log('宏任务执行:setImmediate');
})
setTimeout(function(){
console.log('宏任务执行:setTimeout 0');
},0)
}, 0)
console.log('script end');
在这个例子中,我们将setImmediate和setTimeout 0是嵌套在异步任务setTimeout的里面,并且外层的setTimeout设置的时间是0ms。
然而令人困惑的是这段代码在IE浏览器中的输出结果是不确定的:
以上是多次刷新页面的输出结果
经过以上三个示例,关于setImmediate和setTimeout 0两者的执行时机貌似得不出什么合适的结论,所以这个问题先不做总结,后续在研究吧~
setTimeout 0 和setTimeout 1
在学习这个的时候看到一个特别有意思的代码:
setTimeout(function(){
console.log('宏任务执行:setTimeout 1ms');
}, 1)
setTimeout(function(){
console.log('宏任务执行:setTimeout 0ms');
}, 0)
如果按照事件循环机制的说法,理论上以上的代码输出结果为:
script start
script end
宏任务执行:setTimeout 0
宏任务执行:setTimeout 1
这段代码在Firefox和IE中确实是前面我们推测出来的结果:
但是在Chrome中却是下面这样的结果:
Chrome浏览器的输出结果貌似有点违背前面总结的事件循环机制,但是实际上并没有,因为我们有一句非常重要的话:当异步任务到达触发条件以后将异步任务的回调函数推入任务队列。
所以对于setTimeout 1是在1ms后将回调函数推入任务队列,setTimeout 0则是立即将回调函数推入任务队列,然而setTimeout 1在setTimeout 0的前面,执行完setTimeout 1以后,当执行setTimeout 0的时候1ms的时间已经过去了,那这个时候setTimeout 1的回调函数就比setTimeout 0的回调函数先压入任务队列,所以就会出现Chrome中的打印结果。
Ajax和Dom事件的疑惑
前面我们在对JS中的任务分类时,对Ajax和Dom事件并没有进行归类,一个原因是发现很多文章并没有对这两个任务进行分类,也有很多文章对这两个任务的分类都不一致;另外一个原因就是我自己也没有找到一些合适的例子去证实。
不过关于事件循环机制 HTML Standard有关于 Event Loop 的介绍,不过介于全篇是纯英文的,简单看过之后只get到了下面的这些有效信息:
在经过翻译和解读以后,得出来下面这些信息。
每一个任务都有相关的任务源
function fn(){ }
setTimeout(fn, 1000)
在上面的例子中fn就称为是setTimeout的回调函数,setTimeout就称为该回调函数的任务源。
推入任务队列的是对应的回调函数,执行回调函数的时候可以称为在执行任务。所以在该示例中就可以说任务fn对应的任务源就是setTimeout。
浏览器会根据任务源去分类所有的任务
这个就是前面我们第二节中总结的JavaScript中任务的分类。
浏览器有一个用于鼠标和按键事件的任务队列
关于这个说法的完整翻译为:浏览器可以有一个用于鼠标和按键事件的任务队列(与用户交互任务源关联),另一个与所有其他任务源关联。然后,使用在事件循环处理模型的初始步骤中授予的自由度,它可以使键盘和鼠标事件优先于其他任务四分之三的时间,从而保持界面的响应性,但不会耗尽其他任务队列。
看完这段话,我会理解DOM事件是不是区别于前面我们说的宏任务、微任务?
总而言之呢,关于Ajax和Dom事件到底是属于微任务还是宏任务?以及它们两个和其他异步任务共同存在时的执行顺序,我自己还是存疑的,所以就不给出什么结论了,以免误导大家。当然如果大家有明确的结论或者示例,欢迎提出来~
总结
到此本篇文章就结束了,有关浏览器中的事件循环机制就我们总结的两个核心点:JavaScript任务分类和任务执行顺序。
只要牢牢掌握这两点,就能解决大部分问题。
然而本篇文章还遗留了两个问题:
- 浏览器中
setTimeout 0和setImmediate执行顺序 ajax和dom事件是宏任务还是微任务
年后有时间在复盘总结这两个问题吧。
最后提前祝大家在新的一年好运哦~
近期文章
写在最后
如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者
文章公众号首发,关注 不知名宝藏程序媛 第一时间获取最新的文章
笔芯❤️~

浙公网安备 33010602011771号