浏览器中的事件循环机制【看完就懂】
什么是事件循环机制
相信大家看过很多类似下面这样的代码:
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
事件是宏任务
还是微任务
年后有时间在复盘总结这两个问题吧。
最后提前祝大家在新的一年好运哦~
近期文章
写在最后
如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者
文章公众号
首发,关注 不知名宝藏程序媛
第一时间获取最新的文章
笔芯❤️~