在这之前,我们先来了解一下什么是事件循环机制,为什么有事件循环机制,以及后面将会配合题目带小伙伴们彻底搞明白事件循环机制,以后面试碰到这类题目再也不怕啦~
为什么有事件循环机制
因为JavaScript是一种单线程语言,意味着只能一次执行一个任务。当页面上发生多个事件时,如用户点击事件、网络请求完成事件等,这些事件将会形成一个任务队列。如果按照任务队列的顺序依次执行,时间较长的任务会阻塞后续的任务执行,导致页面无法响应用户的操作,甚至产生假死现象。所以,为了解决这一系列情况,便设计出了事件循环机制。
事件循环机制通过将任务分解为离散的事件并按照一定的顺序执行,使浏览器能够有效地处理多个事件,并在执行异步操作时不阻塞主线程的执行,解决了JavaScript单线程执行的限制,从而保证页面的响应性和用户体验。
浏览器中的事件循坏机制
下面这张图为事件循环机制大致模型图:
在浏览器中,除了JS引擎之外,还有Web APIs。这些Web APIs是由浏览器提供的功能接口,用于与浏览器环境进行交互,例如操作DOM、发起网络请求、定时器等。
当JS执行栈执行到同步任务会直接执行,而执行到相关异步任务(DOM操作、鼠标点击事件、滚轮事件、AJAX网络请求、setTimeout等),它将会把这些任务委托给相应的Web API进行处理,然后立即继续执行下面的代码,而不会等待Web API的操作完成。而一旦Web API操作完成(如定时器结束、网络请求完成等),它会将相应的回调函数添加到异步任务队列中,然后等待事件循环的处理。
其中,异步任务队列中的任务(也就是异步任务)主要可以分为两类:宏任务(Macrotask)和微任务(Microtask)。
宏任务(Macrotask)是一类较大粒度的任务,它们通常包括以下几种:
- script(js整体代码)
- AJAX网络请求
- setTimeout()
- setInterval()
- setImmediate()
- I/O
- UI-rendering
微任务(Microtask)是一类较小粒度的任务,执行时间较短。常见的微任务包括:
- Promise的回调函数,如.then(),.catch(),.finally()等
- MutationObserver
- Process.nextTick()
- 在async函数中使用await等待的任务完成后,后面紧接的代码块都将推入微任务队列
所以按照这个异步任务的划分,事件循环的执行过程可以分为以下几个步骤:
- 执行全局同步代码,进入到script标签,就属于执行了第一次宏任务,进入到了第一次事件循环
- 当执行栈遇到同步代码,立即执行;当遇到一些异步宏任务代码(setTimeout,AJAX网络请求等),在Web API操作完成后(定时器结束,网络请求完成等),它会根据执行快慢将相应的回调函数依次添加到宏任务队列中;而当遇到异步微任务代码(Promise的回调函数等),JS引擎内部则会直接添加到微任务队列中。
- 执行栈不会等待Web API的操作完成,会继续往下执行完所有同步代码,当执行栈为空,当前宏任务执行结束,立即执行微任务队列里面的所有微任务,若没有微任务,则继续下面步骤
- 事件循环的上述步骤执行完毕后,会检查是否需要重新渲染页面。如果需要,将执行重新渲染的操作。
- 执行宏任务,从宏任务队列中取出一个任务压入执行栈中进行执行。(这也叫下一轮事件循环的开启)
- 从步骤2开始,重复后续步骤,直到宏任务队列为空
这么说可能有点不好懂,直接来个流程图更加直观一点:
在每次事件循环时,事件循环会先执行一个宏任务,等该宏任务执行结束后,立即检查微任务队列并执行其中的所有微任务,直到微任务队列为空。然后再进行下一个宏任务的执行。这样的顺序保证了微任务在下一个宏任务之前执行,从而可以实现及时更新页面状态等需求。
看到这里相信大家对事件循环机制已经有一定的认知了。那现在我先拿一道简单的题目,给大家练练手:
js
console.log('start');
setTimeout(()=>{
console.log('setTimeout');
},0)
new Promise((resolve, reject) => {
console.log('Promise');
resolve()
}).then(() => {
console.log('then1');
}).then(() => {
console.log('then2');
})
js
//start Promise then1 then2 setTimeout
相信各位聪明的小伙伴们都已经做出来呢~我们通过上面事件循环的执行过程来分析一下:
1、执行栈碰到console.log('start')
为同步代码,立即执行,打印出start
2、继续往下执行,遇到setTimeout异步宏任务代码,js将它交给Web APIs处理,处理完成后Web APIs将setTimeout事件的回调函数添加到宏任务队列中。
3、js将setTimeout交给Web APIs后,立即继续往下执行,碰到new Promise(),是同步代码,立即执行里面的console.log('Promise')
,打印Promise ,然后继续执行resolve(),Promise状态为resolved,再继续执行Promise的.then()回调函数,这是异步微任务代码,直接将其回调函数添加到微任务队列中。代码继续执行,又是promise的.then()回调函数,将其回调函数添加到微任务队列中。
4、到这里执行栈为空,当前宏任务执行结束,立即检查微任务队列并执行其中的所有微任务,直到微任务队列为空。当前微任务队列有两个Promise.then()回调函数,按队列先进先出顺序,依次压入执行栈中执行,先后执行 console.log('then1')
,console.log('then2')
,先后打印结果then1 ,then2 。
5、微任务队列为空,且不需要重新渲染页面,执行宏任务,从宏任务队列中取出一个任务压入执行栈中进行执行(开启下一轮事件循环)。这里将宏任务队列的setTimeout事件的回调函数压入执行栈执行 console.log('setTimeout')
,打印结果setTimeout,该宏任务结束,立即检查微任务队列发现为空,继续下面步骤,宏任务队列也为空,此时事件循环进行事件等待状态。
经过这么一分析,我们的结果是不是很快就出来啦🥰~
在这里不知道有没有小伙伴和我一样有疑问,那个定时器我设置的不是0s吗,但是为什么它那么晚才执行出来呢,如果那个0s不是它应该被执行出来的时间,那这个时间的作用是什么呢?
不知道有多少小伙伴和我一样,以为定时器的计时是JS引擎干的,其实不是,当JS执行栈执行到定时器的时候,会将它无脑推给Web Apis,而从它被推给Web Apis的那一刻,是Web Apis开始计时。所以我们计时器参数填的那个时间,比如1s,其实是交给web APIs进行计时,然后达到1s才把计时器的回调函数推入宏任务队列,所以计时器这个1s其实是它从Web apis到宏任务队列所要的时间,实际执行它要的时间最少需要1s,得它前面的微任务和宏任务全部执行完,它才能执行。所以,当有多个定时器,网络请求,以及其它一些异步宏任务时,在Web Apis里面先执行完成的,会先被推入宏任务队列中。
如下图:
到这里是不是瞬间感觉悟了,那我们再来一道题目:
js
console.log('script start')
async function async1() {
await async2() //
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout1')
}, 1000)
setTimeout(function() {
console.log('setTimeout2')
setTimeout(function() {
console.log('setTimeout22')
}, 100)
}, 500)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
答案如下:
js
// script start
// async2 end
// Promise
// script end
// async1 end
// promise1
// promise2
// setTimeout2
// setTimeout22
// setTimeout1
不知道这题小伙伴们有没有都做出来呢?相信已经熟练掌握了的小伙伴们肯定很快就得出案啦。还是按上面事件循环过程的步骤,我们再来分析一遍:
1、同步代码console.log('script start')
直接打印结果script start。
2、代码继续执行,执行到async1()函数调用,立即执行,然后在函数里面又执行到await async2() , 执行async2()函数调用,console.log('async2 end')
打印结果async2 end ,async2()函数执行完成出栈,但是因为async2()函数前面加了await,所以在执行完async2()后,它后面紧接的代码块不再继续执行,而是都将推入微任务队列中,也就是console.log('async2 end')
被推入微任务队列中。
3、继续往下执行,执行栈碰到两个定时器,将它们的回调函数都加入宏任务队列中。
4、碰到new Promise(),是同步代码,立即执行里面的console.log('Promise')
,打印Promise,然后继续执行resolve(),Promise状态为resolved,后面紧跟着两个.then()回调函数,都将其添加到微任务队列中。
5、继续执行,碰到同步代码console.log('script end')
,立即执行打印结果script end
6、到这里,当前宏任务执行结束,执行栈为空,此时任务队列如下图,微任务队列要全部执行,依次执行先后打印结果async1 end 、promise1 、promise2、
7、微任务队列为空,且不需要重新渲染页面,执行宏任务,从宏任务队列中取出一个任务压入执行栈中进行执行(开启下一轮事件循环)。这里将宏任务队列的setTimeout事件的回调函数压入执行栈执行,碰到同步代码直接执行,打印结果setTimeout2 ,继续执行碰到setTimeout异步宏任务,将其交给web apis处理,继续往下,宏任务结束,执行栈为空,执行所有微任务,没有微任务,再从宏任务队列中取出一个任务压入执行栈中进行执行(开启下一轮事件循环)。此时任务队列为下图,因为后面加的定时器比第一个延时时间1000ms的定时器先执行完,所以先加入宏任务队列。重复上述步骤,最终会依次打印结果setTimeout22 、setTimeout1。
做完这两道题目,小伙伴们有没有觉得事件循环机制很简单鸭~
最后给小伙伴们留一个题目,欢迎小伙伴们把自己的想法和答案打在评论区喔❤️❤️❤️~
js
async function f1 () {
console.log('f1 start')
await f2()
console.log('f1 end') //微任务2
}
async function f2 () {
console.log('f2')
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout1')
}, 1000)
Promise.resolve().then(() => { //微任务1
console.log('promise1')
setTimeout(() => {
console.log('setTimeout')
}, 500)
})
f1()
let promise2 = new Promise((resolve) => {
resolve('promise2.then')
console.log('promise2')
})
promise2.then((res) => { //微任务3
console.log(res)
Promise.resolve().then(() => {
console.log('promise3')
})
})
console.log('script end')
总结
在事件循环中,当执行栈为空时,事件循环会从事件队列中取出一个宏任务(最开始就是
script
标签,js整体代码),将其压入执行栈中执行。执行宏任务期间,如果生成了微任务,这些微任务会被添加到微任务队列中,等待当前宏任务执行完毕后立即执行,直到将微任务队列里的所有微任务全部执行,才进行下一个宏任务的执行,然后一直重复之前的步骤,直到宏任务队列也全部执行完,执行栈进行等待事件状态。这就是所谓的事件循环机制。
如果觉得文章对您有所帮助的话,麻烦给小米露点点关注,点点赞咯😘,如果文章哪里有不对的地方,欢迎各位小伙伴评论喔~🌹🌹🌹