按照惯例,还是要先强调下本文绝密,因为含有字节真题 :)
缘起
最近在准备换工作,也大概看了下平时掌握不深的 JavaScript 事件循环机制。
但是在今天字节面的笔试中,还是扫到了我的知识盲区。
先看题,输出以下代码的打印结果:
async function async1() {
console.log('1');
await async2();
console.log('2');
}
async function async2() {
console.log('3');
}
console.log('4');
async1();
setTimeout(() => {
console.log('5');
}, 0)
new Promise((resolve, reject) => {
console.log('6');
resolve();
}).then(() => {
console.log('7');
})
console.log('8');
看到这里你可能已经蠢蠢欲动了。想必你内心已经有了你的答案,此时你不妨先去浏览器控制台打印一下,验证你的结果。
如果你全对,那么恭喜你,这块已经难不倒你了,你可以直接跳过本文;如果你像我一样,谨小慎微还是错了一两个地方,那么继续往下看是很有必要的。
「玩归玩,闹过闹,别拿学习开玩笑」。
我收回刚才的玩笑话,做对的同学也快回来吧!这道题目看起来平平无奇,但潜藏的知识点可不少。跟我一起深挖探究下,多学一点总归是好的,也看看你是否想得都对🤣。
事件循环的背景:同步 & 异步
众所周知,JavaScript是一种单线程语言,即同一时间只能做一件事情。因为它设计之初主要就是为了用来与用户互动,以及操作DOM的。
为了防止 协调事件、用户交互、脚本执行、UI 渲染和网络处理等行为造成 JS 单线程阻塞,于是将不少任务作为异步去执行处理。
我们前端经常会提两个概念:同步
和异步
。
同步任务
同步任务很好理解。如果一段代码,一执行马上就能得到期望结果,那么这个代码就是同步任务。
由于JS是单线程的,所以同步任务都是排队执行的,只有当一个同步任务执行完毕,才能执行下一个同步任务。
异步任务
如果一段代码,在执行时还不能够得到预期结果,而是需要在将来通过一定的手段拿到,那么这块代码就是异步任务。
上面已经提到,异步任务的设计是为了解决 JS 单线程阻塞的问题。
异步任务的实现:不进入JS单线程,而是放在任务队列中。若有多个异步任务则需要在任务队列中排队等待。任务队列类似于缓冲区,任务下一步会被移到执行栈然后JS线程执行调用栈的任务。
事件循环的基础:宏任务 & 微任务
异步任务中,有的是 setTimeout 这种耗时很久的,有的是 promise 这种耗时较短的。
当异步任务很多的时候,耗时久的就会阻塞后面所有的异步任务,包括一些很快可以执行完的也被阻塞。
于是 JS 引擎就将异步任务分类管理,划分成两个队列:宏任务队列 和 微任务队列。
宏任务有:
<script>
标签中的运行代码- setTimeout、setInterval的回调函数
- 事件触发的回调函数,例如
DOM Events
、I/O
、requestAnimationFrame
、Ajax、UI交互等
微任务有:
事件循环的原理:宏任务与微任务交替执行
为了既不阻塞JS单线程的执行,同时又保障JS执行的效率,前辈们设计了出了一套事件循环机制
:
先执行一个宏任务(document 下 script 标签中的所有同步代码),执行过程中如果产出新的宏/微任务,就将他们推入相应的任务队列,之后再执行微任务队列,再之后就执行宏任务队列,如此循环。以上不断重复的过程就叫做 Event Loop(事件循环) 。
回到开始的题目
有了上面的知识储备,我们再看开头的题目就不显得那么迷糊了。
一起来再仔细盘下执行过程:
- 所有的代码执行会被 js 引擎当成一个宏任务,但不会推到宏任务队列
- 首先打印 4,因为
async1
、async2
两个函数开始只是声明,没有调用 - 然后就是打印 1,看到 async 不要慌,async函数里的内容大都是同步执行的(除了 await 后面是作为 promise 来执行的)
- 然后就是打印 3,没得讲,调用函数 async2,里面没有 await
- 然后就是打印 6,别说是 2,打印 2 这个任务在 await 后面,是会被推进了微任务队列的
- 为啥是 6 解释下,Promise的函数体是同步执行的,只有 then、catch、finally 这些回调才是异步调用的(见上文)
- 此时的回调函数打印 7,被推进了微任务队列,异步执行
- 然后就是打印 8,毫无疑问是同步执行
- 同步任务至此已经全部完成,宏任务执行完了便开始执行微任务队列的微任务了
- 首先是打印 2,它最开始被推进微任务队列
- 其次是打印 7,初始化 promise 的时候将它推进微任务队列的(因为回调是异步执行的),这个时候微任务队列也已经执行完
- 最后是打印 5,根据前面所讲 setTimeout 是一个标准的宏任务,微任务队列执行完了就要执行宏任务队列
这里有几个重点需要关注下:
- 同步任务会立即执行,只有异步任务才会被加入到任务队列中进行事件循环(Event Loop)执行
- 宏任务和微任务都是异步任务下的产物,同步任务是立即执行的,所以它既不存在宏任务队列,也不存在微任务队列
- 很多人说微任务总是优先于宏任务执行,这句话不严谨。只有在事件循环中,微任务总是优先于宏任务执行(因为整个脚本的执行就是一个宏任务。只有在这个宏任务执行完毕后,JavaScript 引擎才会去执行微任务队列中的所有微任务)
- new Promise 中的代码是同步的,但是回调函数则是异步的(微任务)
async/await
是 JavaScript 中使用同步代码来处理异步的一种方式,它本身并不是宏任务或微任务。但async
函数将返回一个 Promise 对象,即 await 后面的代码会作为 Promise 的回调来处理。因此 await 后面的代码 会被当成微任务,加入到微任务队列中的- setTimeout() 的第2个参数是为了告诉 JavaScript 再过多长时间把当前任务添加到宏任务队列中
缘灭
因为下份工作不想太卷,所以本来也没打算冲刺字节的,甚至说是对字节这个面试毫无准备。
但不知道是哪位猎头不讲武德,偷偷摸摸给我投了。
我只能说,大意了,没有闪!
万万没想到简历还过了,我就当顺便练练手,为后续面试积累下经验吧!
看到这里,我奉劝各位还是忘了这道题目吧!
只要「真题穿肠过,知识心中留」就可以了。
我不想变成明天掘金首页字节真题泄密的猪脚,不想被字节请去喝茶,更不想去字节面第二轮。
掘友们,希望你们耗子尾汁,不要搞窝里斗!
原文链接:https://juejin.cn/post/7229372047414902839 作者:敲完代码再睡觉