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

相关推荐
轻口味1 小时前
命名空间与模块化概述
开发语言·前端·javascript
吉大一菜鸡1 小时前
FPGA学习(基于小梅哥Xilinx FPGA)学习笔记
笔记·学习·fpga开发
前端小小王2 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发2 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
CCSBRIDGE3 小时前
Magento2项目部署笔记
笔记
亦枫Leonlew4 小时前
微积分复习笔记 Calculus Volume 2 - 5.1 Sequences
笔记·数学·微积分
爱码小白5 小时前
网络编程(王铭东老师)笔记
服务器·网络·笔记
真滴book理喻5 小时前
Vue(四)
前端·javascript·vue.js
程序员_三木5 小时前
Three.js入门-Raycaster鼠标拾取详解与应用
开发语言·javascript·计算机外设·webgl·three.js
LuH11245 小时前
【论文阅读笔记】Learning to sample
论文阅读·笔记·图形渲染·点云