面试官:说说浏览器中的事件循坏机制(event-loop)

在这之前,我们先来了解一下什么是事件循环机制,为什么有事件循环机制,以及后面将会配合题目带小伙伴们彻底搞明白事件循环机制,以后面试碰到这类题目再也不怕啦~

为什么有事件循环机制

因为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等待的任务完成后,后面紧接的代码块都将推入微任务队列

所以按照这个异步任务的划分,事件循环的执行过程可以分为以下几个步骤:

  1. 执行全局同步代码,进入到script标签,就属于执行了第一次宏任务,进入到了第一次事件循环
  2. 当执行栈遇到同步代码,立即执行;当遇到一些异步宏任务代码(setTimeout,AJAX网络请求等),在Web API操作完成后(定时器结束,网络请求完成等),它会根据执行快慢将相应的回调函数依次添加到宏任务队列中;而当遇到异步微任务代码(Promise的回调函数等),JS引擎内部则会直接添加到微任务队列中。
  3. 执行栈不会等待Web API的操作完成,会继续往下执行完所有同步代码,当执行栈为空,当前宏任务执行结束,立即执行微任务队列里面的所有微任务,若没有微任务,则继续下面步骤
  4. 事件循环的上述步骤执行完毕后,会检查是否需要重新渲染页面。如果需要,将执行重新渲染的操作。
  5. 执行宏任务,从宏任务队列中取出一个任务压入执行栈中进行执行。(这也叫下一轮事件循环的开启)
  6. 从步骤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'),先后打印结果then1then2

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 endpromise1promise2

7、微任务队列为空,且不需要重新渲染页面,执行宏任务,从宏任务队列中取出一个任务压入执行栈中进行执行(开启下一轮事件循环)。这里将宏任务队列的setTimeout事件的回调函数压入执行栈执行,碰到同步代码直接执行,打印结果setTimeout2 ,继续执行碰到setTimeout异步宏任务,将其交给web apis处理,继续往下,宏任务结束,执行栈为空,执行所有微任务,没有微任务,再从宏任务队列中取出一个任务压入执行栈中进行执行(开启下一轮事件循环)。此时任务队列为下图,因为后面加的定时器比第一个延时时间1000ms的定时器先执行完,所以先加入宏任务队列。重复上述步骤,最终会依次打印结果setTimeout22setTimeout1

做完这两道题目,小伙伴们有没有觉得事件循环机制很简单鸭~

最后给小伙伴们留一个题目,欢迎小伙伴们把自己的想法和答案打在评论区喔❤️❤️❤️~

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整体代码),将其压入执行栈中执行。执行宏任务期间,如果生成了微任务,这些微任务会被添加到微任务队列中,等待当前宏任务执行完毕后立即执行,直到将微任务队列里的所有微任务全部执行,才进行下一个宏任务的执行,然后一直重复之前的步骤,直到宏任务队列也全部执行完,执行栈进行等待事件状态。这就是所谓的事件循环机制。
如果觉得文章对您有所帮助的话,麻烦给小米露点点关注,点点赞咯😘,如果文章哪里有不对的地方,欢迎各位小伙伴评论喔~🌹🌹🌹

相关推荐
软件测试慧姐2 分钟前
面试中常问的软件测试面试题
面试·职场和发展
aygh18 分钟前
Java八股文复习指南
java·面试·八股文·后端开发
进击的尘埃20 分钟前
用了大半年 Claude Code,我总结了 12 个真正改变工作流的配置技巧
javascript
luanma15098020 分钟前
Laravel 8.X重磅特性全解析
前端·javascript·vue.js·php·lua
kyriewen37 分钟前
为什么我的代码在测试环境跑得好好的,一到用户电脑就崩?原来凶手躲在地址栏旁边
前端·javascript·chrome
busideyang39 分钟前
函数指针类型定义笔记
c语言·笔记·stm32·单片机·算法·嵌入式
蒸汽求职40 分钟前
【蒸汽教育求职干货】OPT只剩3个月还没找到工作,怎么办?——留学生IT求职的“紧急预案”
人工智能·经验分享·面试·职场和发展·美国求职
ETA841 分钟前
面试官:说说事件冒泡与委托?这是我见过最透彻的回答
前端·javascript
蒸汽求职43 分钟前
【蒸汽教育求职分享】美国IT面试的Behavioral Question:STAR法则人人都知道,但90%的人用错了
人工智能·面试·职场和发展·github·求职招聘·留学生求职
iPadiPhone1 小时前
万亿级流量的基石:Kafka 核心原理、大厂面试题解析与实战
分布式·后端·面试·kafka