JavaScript 一面:事件循环、宏任务、微任务与 async/await
一面里考事件循环,常见思路是给一段混合了
setTimeout/Promise/async/await的代码,让你写出打印顺序。这一篇试着把那套模型讲清楚。
事件循环的基本模型
可以先用一个够用的抽象来记忆浏览器里的事件循环:
- 执行上下文中同步代码先跑完;
- 异步任务的回调被分发到不同的队列里:
- 宏任务队列(macrotask queue):
setTimeout、setInterval、setImmediate(Node)、I/O、UI 渲染等; - 微任务队列(microtask queue):
Promise.then/catch/finally、MutationObserver、queueMicrotask 等。
- 宏任务队列(macrotask queue):
一个完整的“循环”大致是:
- 从宏任务队列取出一个任务,执行其中的同步代码;
- 执行完当前宏任务后,清空当前产生的所有微任务队列;
- 执行 UI 渲染(浏览器环境中);
- 回到第 1 步,处理下一个宏任务。
在一面里,如果能用两三句话把这几个点讲清楚,已经足够展示你对事件循环的理解。
Promise 与微任务:为什么总是“先 Promise 再 Timeout”?
一个经典的面试例子:
1console.log(1);
2
3setTimeout(() => {
4 console.log(2);
5}, 0);
6
7Promise.resolve().then(() => {
8 console.log(3);
9});
10
11console.log(4);
打印顺序是:1, 4, 3, 2。
分析过程可以这样讲:
- 同步代码先执行:输出 1;注册一个宏任务(
setTimeout回调);注册一个微任务(then回调);输出 4; - 当前宏任务(主脚本)结束,开始执行本轮微任务队列:输出 3;
- 微任务清空后,事件循环进入下一轮,从宏任务队列取出
setTimeout的回调:输出 2。
关键点:同一轮事件循环中,永远是先清空微任务,再执行下一个宏任务。
async/await 底层仍然是 Promise + 微任务
async/await 本质只是对 Promise 的语法糖。
一个典型考题:
1async function foo() {
2 console.log("foo start");
3 await bar();
4 console.log("foo end");
5}
6
7async function bar() {
8 console.log("bar");
9}
10
11console.log("script start");
12foo();
13console.log("script end");
大多数运行环境下打印顺序是:
1script start
2foo start
3bar
4script end
5foo end
可以这样解释:
foo()被调用时,先同步执行到await bar():- 输出
foo start; - 调用
bar(),输出bar; await把后面的逻辑(console.log("foo end"))包装成一个微任务;
- 输出
- 回到主脚本,输出
script end; - 本轮宏任务结束,开始执行微任务队列:输出
foo end。
在一面回答时,如果你能明确指出“await 之后的代码会被放到一个微任务里”,已经足够展示扎实理解。
一个稍微复杂一点的例子
面试常见进阶题,会混合几个异步源:
1console.log("start");
2
3setTimeout(() => {
4 console.log("timeout");
5}, 0);
6
7Promise.resolve()
8 .then(() => {
9 console.log("then1");
10 })
11 .then(() => {
12 console.log("then2");
13 });
14
15(async () => {
16 console.log("async start");
17 await null;
18 console.log("async end");
19})();
20
21console.log("end");
分析顺序可以拆开讲:
同步部分:
- 输出
start; - 注册一个宏任务(
setTimeout); - 注册第一个
then1微任务; - 进入自执行
async函数,输出async start,await null把后续console.log("async end")作为微任务; - 输出
end。
- 输出
微任务队列执行顺序:
- 先执行
then1:输出then1,并注册下一个微任务then2; - 然后执行
async对应的微任务:输出async end; - 最后执行
then2:输出then2。
- 先执行
微任务清空后,下一轮取宏任务:
- 执行
setTimeout回调,输出timeout。
- 执行
所以整体输出顺序为:
1start
2async start
3end
4then1
5async end
6then2
7timeout
在面试场景里,如果你能边写边口头说明“这个是宏任务,这个是微任务,现在清空哪个队列”,会比只给出最终顺序更加分。
几个一面经常会问到的小问题
可以预先准备的几点:
process.nextTick和 Promise 微任务的顺序(Node 环境)- 如果面试主要是浏览器端开发,可以简单说“Node 的事件循环阶段更复杂,这里不展开”。
forEach+ async 的坑forEach不会等待async回调完成,await里面的逻辑只会排队进微任务;- 如果想控制异步顺序,应该用
for...of循环配合await。
setTimeout(fn, 0)是否真的“立即执行”?- 不会,它只是将回调排队到宏任务队列里,至少要等当前宏任务和所有微任务跑完。
这些问题不一定每次都被问到,但提前想清楚,面对变体题目时会从容很多。
常见面试题与参考答案
题 1:基础版输出顺序题
1console.log(1);
2
3setTimeout(() => {
4 console.log(2);
5}, 0);
6
7Promise.resolve().then(() => {
8 console.log(3);
9});
10
11console.log(4);
参考答案:
- 输出顺序:
1, 4, 3, 2。 - 分析:
- 同步代码先执行,输出
1和4,注册一个宏任务(setTimeout)和一个微任务(then); - 当前宏任务结束后,清空微任务队列,输出
3; - 然后进入下一轮事件循环,从宏任务队列取出
setTimeout回调,输出2。
- 同步代码先执行,输出
答题时可以边写边口头说明“先宏任务里的同步,再微任务,再下一个宏任务”。
题 2:混合 then 链和 async 的输出顺序
1console.log("start");
2
3setTimeout(() => {
4 console.log("timeout");
5}, 0);
6
7Promise.resolve()
8 .then(() => {
9 console.log("then1");
10 })
11 .then(() => {
12 console.log("then2");
13 });
14
15(async () => {
16 console.log("async start");
17 await null;
18 console.log("async end");
19})();
20
21console.log("end");
参考答案:
- 输出顺序:
1start
2async start
3end
4then1
5async end
6then2
7timeout
- 简要分析:
- 同步阶段依次输出
start、async start、end,并注册:- 一个宏任务(
setTimeout); then1对应的微任务;await null对应的微任务(async end那一行);
- 一个宏任务(
- 清空微任务队列时:
- 先执行
then1,输出then1,并注册下一个微任务then2; - 再执行
async end; - 最后执行
then2;
- 先执行
- 微任务清空后,取下一个宏任务执行,输出
timeout。
- 同步阶段依次输出
题 3:async/await 与普通 Promise then 的对比
有些一面题会让你“把一段 then 链改成 async/await”,或者问“这两种写法在事件循环里的差别”。
可以用类似下面的回答方式:
async/await相比手写 then 链更易读,但底层仍然是:await前的部分在当前宏任务里同步执行;await之后的逻辑被放进一个微任务队列,在当前宏任务结束后执行;
- 事件循环顺序不变,都是“当前宏任务 → 清空微任务 → 下一个宏任务”,只是
async/await把这个流程写得更接近同步代码。
小结:一面时可以怎么讲?
可以尝试用下面这样的结构回答事件循环相关问题:
- 先用一句话说明宏任务/微任务和事件循环的基本模型;
- 用一个
console + setTimeout + Promise的小例子说明“为什么是先 Promise 再 Timeout”; - 再补充
async/await只是 Promise 的语法糖,await之后的代码会进微任务队列; - 如果还有时间,可以提一句 Node 事件循环更复杂,但面试重点多半在浏览器模型。
相比死记硬背“哪些输出顺序”,面试官更希望看到你能用自己的话把这套机制讲清楚,并在新题目里正确运用这套模型。