面试官:说说你对事件循环的理解
huihuiha opened this issue · comments
一、是什么
JavaScript
在设计之初便是单线程,即指程序运行时,只有一个线程存在,同一时间只能做一件事
为什么要这么设计,跟JavaScript
的应用场景有关
JavaScript
初期作为一门浏览器脚本语言,通常用于操作 DOM
,如果是多线程,一个线程进行了删除 DOM
,另一个添加 DOM
,此时浏览器该如何处理?
为了解决单线程运行阻塞问题,JavaScript
用到了计算机系统的一种运行机制,这种机制就叫做事件循环(Event Loop)
事件循环(Event Loop)
在JavaScript
中,所有的任务都可以分为
-
同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行
-
异步任务:异步执行的任务,比如
ajax
网络请求,setTimeout
定时函数等
同步任务与异步任务的运行流程图如下:
从上面我们可以看到,同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。上述过程的不断重复就是事件循环
二、宏任务与微任务
如果将任务划分为同步任务和异步任务并不是那么的准确,举个例子:
console.log(1)
setTimeout(()=>{
console.log(2)
}, 0)
new Promise((resolve, reject)=>{
console.log('new Promise')
resolve()
}).then(()=>{
console.log('then')
})
console.log(3)
如果按照上面流程图来分析代码,我们会得到下面的执行步骤:
console.log(1)
,同步任务,主线程中执行setTimeout()
,异步任务,放到Event Table
,0 毫秒后console.log(2)
回调推入Event Queue
中new Promise
,同步任务,主线程直接执行.then
,异步任务,放到Event Table
console.log(3)
,同步任务,主线程执行
所以按照分析,它的结果应该是 1
=> 'new Promise'
=> 3
=> 2
=> 'then'
但是实际结果是:1
=>'new Promise'
=> 3
=> 'then'
=> 2
出现分歧的原因在于异步任务执行顺序,事件队列其实是一个“先进先出”的数据结构,排在前面的事件会优先被主线程读取
例子中 setTimeout
回调事件是先进入队列中的,按理说应该先于 .then
中的执行,但是结果却偏偏相反
原因在于异步任务还可以细分为微任务与宏任务
微任务
一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前
常见的微任务有:
-
Promise.then
-
MutaionObserver
-
Object.observe(已废弃;Proxy 对象替代)
-
process.nextTick(Node.js)
宏任务
宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合
常见的宏任务有:
- script (可以理解为外层同步代码)
- setTimeout/setInterval
- UI rendering/UI事件
- postMessage、MessageChannel
- setImmediate、I/O(Node.js)
这时候,事件循环,宏任务,微任务的关系如图所示
按照这个流程,它的执行机制是:
- 执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中
- 当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完
回到上面的题目
console.log(1)
setTimeout(()=>{
console.log(2)
}, 0)
new Promise((resolve, reject)=>{
console.log('new Promise')
resolve()
}).then(()=>{
console.log('then')
})
console.log(3)
流程如下
// 遇到 console.log(1) ,直接打印 1
// 遇到定时器,属于新的宏任务,留着后面执行
// 遇到 new Promise,这个是直接执行的,打印 'new Promise'
// .then 属于微任务,放入微任务队列,后面再执行
// 遇到 console.log(3) 直接打印 3
// 好了本轮宏任务执行完毕,现在去微任务列表查看是否有微任务,发现 .then 的回调,执行它,打印 'then'
// 当一次宏任务执行完,再去执行新的宏任务,这里就剩一个定时器的宏任务了,执行它,打印 2
三、async与await
async
是异步的意思,await
则可以理解为等待
放到一起可以理解async
就是用来声明一个异步方法,而 await
是用来等待异步方法执行
async
async
函数返回一个promise
对象,下面两种方法是等效的
function f() {
return Promise.resolve('TEST');
}
// asyncF is equivalent to f!
async function asyncF() {
return 'TEST';
}
await
正常情况下,await
命令后面是一个 Promise
对象,返回该对象的结果。如果不是 Promise
对象,就直接返回对应的值
async function f(){
// 等同于
// return 123
return await 123
}
f().then(v => console.log(v)) // 123
不管await
后面跟着的是什么,await
都会阻塞后面的代码
async function fn1 (){
console.log(1)
await fn2()
console.log(2) // 阻塞
}
async function fn2 (){
console.log('fn2')
}
fn1()
console.log(3)
上面的例子中,await
会阻塞下面的代码(即加入微任务队列),先执行 async
外面的同步代码,同步代码执行完,再回到 async
函数中,再执行之前阻塞的代码
所以上述输出结果为:1
,fn2
,3
,2
四、流程分析
通过对上面的了解,我们对JavaScript
对各种场景的执行顺序有了大致的了解
这里直接上代码:
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('settimeout')
})
async1()
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')
分析过程:
- 执行整段代码,遇到
console.log('script start')
直接打印结果,输出script start
- 遇到定时器了,它是宏任务,先放着不执行
- 遇到
async1()
,执行async1
函数,先打印async1 start
,下面遇到await
怎么办?先执行async2
,打印async2
,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码 - 跳到
new Promise
这里,直接执行,打印promise1
,下面遇到.then()
,它是微任务,放到微任务列表等待执行 - 最后一行直接打印
script end
,现在同步代码执行完了,开始执行微任务,即await
下面的代码,打印async1 end
- 继续执行下一个微任务,即执行
then
的回调,打印promise2
- 上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印
settimeout
所以最后的结果是:script start
、async1 start
、async2
、promise1
、script end
、async1 end
、promise2
、settimeout
所以整个同步代码的执行也是宏任务?
所以整个同步代码的执行也是宏任务?
这个理解能力建议去搬砖吧,别污染了这个行业。
console.log(1)
setTimeout(()=>{
console.log(2)
}, 0)
new Promise((resolve, reject)=>{
console.log('new Promise')
resolve()
}).then(()=>{
console.log('then')
})
console.log(3)
按照博主所说宏任务先执行,然后轮到微任务(该例子附近有提到)。那么这个例子里,应该是2比then要更早打印,而事实是then比2早,应该是微任务先于宏任务吧,我看其他博主也是这么说的
所以整个同步代码的执行也是宏任务?
这个理解能力建议去搬砖吧,别污染了这个行业。
嚯,好大的戾气
console.log(1) setTimeout(()=>{ console.log(2) }, 0) new Promise((resolve, reject)=>{ console.log('new Promise') resolve() }).then(()=>{ console.log('then') }) console.log(3)
按照博主所说宏任务先执行,然后轮到微任务(该例子附近有提到)。那么这个例子里,应该是2比then要更早打印,而事实是then比2早,应该是微任务先于宏任务吧,我看其他博主也是这么说的
这个我理解的应该是:如果把整段脚本(script)看作是一个宏任务,这个时候是先宏后微,因为肯定要先开始执行整段脚本,如果同在任务队列里面的话,顺序是先微后宏;
console.log(1) setTimeout(()=>{ console.log(2) }, 0) new Promise((resolve, reject)=>{ console.log('new Promise') resolve() }).then(()=>{ console.log('then') }) console.log(3)
按照博主所说宏任务先执行,然后轮到微任务(该例子附近有提到)。那么这个例子里,应该是2比then要更早打印,而事实是then比2早,应该是微任务先于宏任务吧,我看其他博主也是这么说的
这个我理解的应该是:如果把整段脚本(script)看作是一个宏任务,这个时候是先宏后微,因为肯定要先开始执行整段脚本,如果同在任务队列里面的话,顺序是先微后宏;
感谢解答
关于 async
与await
的例子有个令我疑惑的地方, 通过async
声名的异步函数, 执行的时候不应该在前面都加上await
吗?
不然日常使用会带来没必要的困扰吧
async function fn1 (){
console.log(1)
await fn2()
console.log(2)
}
async function fn2 (){
console.log('fn2')
}
await fn1()
console.log(3)
result:
1
fn2
2
3
所以整个同步代码的执行也是宏任务?
同步 异步(异步细分为微任务和宏任务 看题不仔细哦)
所以整个同步代码的执行也是宏任务?
同步 异步(异步细分为微任务和宏任务 看题不仔细哦)
其实这个问题问的非常好,整个同步代码确实是作为一个宏任务先开始执行的,等执行完成之后将继续执行微任务队列中的全部任务,之后再执行一个宏任务->全部微任务,如此循环
所以整个同步代码的执行也是宏任务?
这个理解能力建议去搬砖吧,别污染了这个行业。
你他么真是个小丑,整个script的执行就是当宏任务来,他有说错吗?🤣
所以整个同步代码的执行也是宏任务?
这个理解能力建议去搬砖吧,别污染了这个行业。
你他么真是个小丑,整个script的执行就是当宏任务来,他有说错吗?🤣
那请问下我说的那个是不是就错误了呢(同步 异步(异步细分为微任务和宏任务 看题不仔细哦))
所以整个同步代码的执行也是宏任务?
这个理解能力建议去搬砖吧,别污染了这个行业。
你他么真是个小丑,整个script的执行就是当宏任务来,他有说错吗?🤣
那请问下我说的那个是不是就错误了呢(同步 异步(异步细分为微任务和宏任务 看题不仔细哦))
异步任务划分没有错啊,宏任务分类中不就是有个外部script吗,浏览器以异步的方式加载script,就是作为单宏任务来执行,如果这个script中全部时同步代码,他说的并没有错。
所以整个同步代码的执行也是宏任务?
这个理解能力建议去搬砖吧,别污染了这个行业。
你他么真是个小丑,整个script的执行就是当宏任务来,他有说错吗?🤣
那请问下我说的那个是不是就错误了呢(同步 异步(异步细分为微任务和宏任务 看题不仔细哦))
我说的不够仔细,可以看看GPT的回答:
在浏览器中,每个script标签的加载和解析都会被视为一个宏任务。当脚本被加载时,其中的代码会按照顺序执行,这些代码都属于同一个宏任务。
但是需要注意的是,如果在这个script中有异步操作(比如使用setTimeout或Promise),那么这些异步操作会被视作微任务,并且会在当前宏任务执行完毕后立即执行。因此,在一个script标签中,可能既包含宏任务代码,也包含微任务代码。
总之,可以说整个文件的同步代码执行是一个宏任务,但如果存在异步操作,则包含这些异步操作的代码块会被视为微任务。
所以整个同步代码的执行也是宏任务?
这个理解能力建议去搬砖吧,别污染了这个行业。
你他么真是个小丑,整个script的执行就是当宏任务来,他有说错吗?🤣
那请问下我说的那个是不是就错误了呢(同步 异步(异步细分为微任务和宏任务 看题不仔细哦))
我说的不够仔细,可以看看GPT的回答:
在浏览器中,每个script标签的加载和解析都会被视为一个宏任务。当脚本被加载时,其中的代码会按照顺序执行,这些代码都属于同一个宏任务。
但是需要注意的是,如果在这个script中有异步操作(比如使用setTimeout或Promise),那么这些异步操作会被视作微任务,并且会在当前宏任务执行完毕后立即执行。因此,在一个script标签中,可能既包含宏任务代码,也包含微任务代码。
总之,可以说整个文件的同步代码执行是一个宏任务,但如果存在异步操作,则包含这些异步操作的代码块会被视为微任务。
谢谢解答。
为什么你们的所有解释都没有贴浏览器的源码?分什么宏任务,微任务。不应该以代码中命名的变量么?非要翻译成中文?
为什么你们的所有解释都没有贴浏览器的源码?分什么宏任务,微任务。不应该以代码中命名的变量么?非要翻译成中文?
英文本身就是microtask和macrotask啊....