事件循环是JS面试的"照妖镜",看似简单,一做题就翻车。今天从原理到实战,一次性讲透。
前言
如果说原型链、闭包、this是JS的"三大基石",那事件循环就是JS的"灵魂机制"。
为什么这么说?因为JavaScript是单线程语言,但它又要处理异步操作(网络请求、定时器、用户交互等),那怎么办?答案就是事件循环。
我之前面试的时候,事件循环相关的题几乎每次都考,而且每次都能给我整出新花样。后来我把这块彻底搞明白了,发现其实就那么回事——理解了执行顺序的规则,什么题都不怕。
一、先搞清楚几个基本概念
1.1 为什么需要事件循环?
JavaScript是单线程的,一次只能做一件事。如果遇到耗时的操作(比如发请求、读文件),整个页面就卡住了,用户疯狂点按钮没反应——这体验谁受得了?
所以JS的设计是:把耗时操作交给浏览器(或Node.js)的后台线程去处理,处理完了再通过回调函数通知主线程。
而管理"什么时候执行哪个回调"的机制,就是事件循环(Event Loop)。
1.2 执行栈(Call Stack)
JS代码执行的时候,会有一个调用栈,遵循"后进先出"的原则。
function foo() {
console.log('foo');
bar();
}
function bar() {
console.log('bar');
}
foo();
// 执行顺序:foo入栈 → 输出foo → bar入栈 → 输出bar → bar出栈 → foo出栈
这个很好理解,就像叠盘子,最后放上去的最先拿下来。
1.3 任务队列(Task Queue)
浏览器在执行代码的过程中,会遇到各种异步任务。这些任务不会立即执行,而是被放到不同的队列里,等执行栈空了再按规则取出来执行。
这里的关键是:队列不止一个。
二、核心:宏任务与微任务
2.1 两类任务
事件循环中有两种任务:
| 类型 | 常见API | 通俗理解 |
|---|---|---|
| 宏任务(Macro Task) | setTimeout、setInterval、I/O、UI渲染、requestAnimationFrame | 大事,慢慢来 |
| 微任务(Micro Task) | Promise.then/catch/finally、MutationObserver、process.nextTick(Node) | 小事,赶紧办 |
2.2 事件循环的执行流程
这是最核心的部分,一定要理解:
关键规则:每次执行完一个宏任务(包括第一个同步代码块),都会先把所有微任务清空,再去执行下一个宏任务。
用大白话说就是:每干完一件大事,先把积攒的小事全部处理完,再干下一件大事。
2.3 一个简单的例子热热身
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
输出顺序:1 → 4 → 3 → 2
解析:
- 执行同步代码:输出
1,把 setTimeout 回调放入宏任务队列,把 Promise.then 放入微任务队列,输出4 - 同步代码执行完毕,检查微任务队列:输出
3 - 微任务清空,取下一个宏任务:输出
2
记住这个顺序:同步 → 微任务 → 宏任务。
三、Promise 相关的坑
3.1 Promise 的执行时机
Promise的构造函数是同步执行的,但.then()里的回调是微任务。
console.log('1');
new Promise((resolve) => {
console.log('2'); // 同步执行!
resolve();
}).then(() => {
console.log('3'); // 微任务
});
console.log('4');
输出:1 → 2 → 4 → 3
这个很多人会搞错,以为Promise整体都是异步的。构造函数里的代码是同步的,只有.then/catch/finally里的回调才是微任务。
3.2 then 链式调用
new Promise((resolve) => {
console.log('1');
resolve();
})
.then(() => {
console.log('2');
})
.then(() => {
console.log('3');
});
console.log('4');
输出:1 → 4 → 2 → 3
每个.then()都会返回一个新的Promise,所以每个.then()的回调都会作为一个微任务入队。它们按顺序执行。
3.3 then 里 return 一个 Promise
Promise.resolve()
.then(() => {
console.log('1');
return Promise.resolve('2');
})
.then((res) => {
console.log(res);
});
输出:1 → 2
注意:如果.then()里return一个Promise,下一个.then()会等这个Promise resolve之后再执行。这个过程中会产生两个微任务(规范要求至少两个),但在实际开发中,我们通常只需要关心顺序,不需要关心具体产生了几个微任务。
四、async/await 的本质
4.1 async/await 就是 Promise 的语法糖
async函数返回一个Promise,await会"暂停"函数执行,等待Promise resolve。
async function foo() {
console.log('1');
await Promise.resolve();
console.log('2');
}
console.log('3');
foo();
console.log('4');
输出:3 → 1 → 4 → 2
解析:
- 输出
3 - 调用
foo(),输出1 - 遇到
await,await后面的代码相当于.then()里的回调,放入微任务队列 - 继续执行同步代码,输出
4 - 微任务队列:输出
2
关键理解:await后面的代码 = .then()里的回调 = 微任务。
4.2 await 连续多个
async function foo() {
console.log('1');
await console.log('2'); // 注意这里
console.log('3');
await console.log('4');
console.log('5');
}
console.log('6');
foo();
console.log('7');
输出:6 → 1 → 2 → 7 → 3 → 4 → 5
解析:
- 同步代码:输出
6 - 进入
foo():输出1 await console.log('2'):先同步执行console.log('2')(输出2),然后await undefined,后面的代码放入微任务- 继续同步代码:输出
7 - 微任务:输出
3,然后await console.log('4')同步输出4,后面的代码再放入微任务 - 微任务:输出
5
注意:await右边的表达式是同步执行的,只有await后面的代码才是微任务。
五、特殊 API 的执行时机
5.1 requestAnimationFrame
requestAnimationFrame(简称rAF)在宏任务中的执行时机比较特殊。在大多数浏览器中,rAF的回调会在微任务执行完毕后、下一次重绘前执行。
但不同浏览器实现可能有差异,面试中一般不会考得太深,知道它是宏任务就行。
5.2 process.nextTick(Node.js)
在Node.js中,process.nextTick的优先级高于Promise微任务。它会在当前微任务队列清空前执行。
// Node.js 环境
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
console.log('sync');
输出:sync → nextTick → promise
这个是Node.js特有的,浏览器里没有process.nextTick。
5.3 MutationObserver
MutationObserver是微任务,用于监听DOM变化。它在Promise微任务之后执行(在Node.js中对应Promise.then之后)。
六、12 道经典输出题
建议先自己想答案,再看解析。每道题都覆盖了不同的考点。
🟢 第1题:基础热身
console.log('start');
setTimeout(() => {
console.log('timeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise');
});
console.log('end');
点击查看答案
输出:start → end → promise → timeout
考点:同步代码优先 → 微任务(Promise.then)→ 宏任务(setTimeout)
🟢 第2题:Promise构造函数是同步的
console.log('1');
new Promise((resolve) => {
console.log('2');
resolve();
console.log('3');
}).then(() => {
console.log('4');
});
console.log('5');
点击查看答案
输出:1 → 2 → 3 → 5 → 4
考点:Promise构造函数里的代码是同步执行的,只有.then()的回调是微任务。resolve()调用后,构造函数里的代码还会继续执行。
🟡 第3题:then链式调用
setTimeout(() => {
console.log('A');
}, 0);
new Promise((resolve) => {
console.log('B');
resolve();
}).then(() => {
console.log('C');
}).then(() => {
console.log('D');
});
console.log('E');
点击查看答案
输出:B → E → C → D → A
考点:多个.then()按顺序入微任务队列。同步代码执行完后,依次清空微任务,最后执行宏任务。
🟡 第4题:async/await 基础
async function foo() {
console.log('1');
await Promise.resolve();
console.log('2');
}
console.log('3');
foo();
console.log('4');
点击查看答案
输出:3 → 1 → 4 → 2
考点:await后面的代码相当于.then()回调,是微任务。foo()调用时,先同步执行到await,然后让出执行权。
🟡 第5题:await 右侧表达式是同步的
async function foo() {
console.log('1');
await console.log('2');
console.log('3');
}
console.log('4');
foo();
console.log('5');
点击查看答案
输出:4 → 1 → 2 → 5 → 3
考点:await右边的console.log('2')是同步执行的!只有await之后的代码(console.log('3'))才是微任务。这是很多人容易搞混的地方。
🟡 第6题:宏任务中嵌套微任务
setTimeout(() => {
console.log('1');
Promise.resolve().then(() => {
console.log('2');
});
}, 0);
setTimeout(() => {
console.log('3');
}, 0);
console.log('4');
点击查看答案
输出:4 → 1 → 2 → 3
考点:第一个setTimeout的回调执行时,产生了微任务。执行完这个宏任务后,会先清空微任务队列,再执行下一个宏任务。所以顺序是1 → 2 → 3,而不是1 → 3 → 2。
🟠 第7题:微任务中产生微任务
Promise.resolve()
.then(() => {
console.log('1');
return Promise.resolve();
})
.then(() => {
console.log('2');
})
.then(() => {
console.log('3');
});
点击查看答案
输出:1 → 2 → 3
考点:微任务执行过程中产生的新的微任务,会在当前轮次继续执行(而不是等到下一轮)。所以三个.then()按顺序执行。
🟠 第8题:async函数中的多个await
async function foo() {
console.log('1');
await Promise.resolve();
console.log('2');
await Promise.resolve();
console.log('3');
}
console.log('4');
foo();
console.log('5');
点击查看答案
输出:4 → 1 → 5 → 2 → 3
考点:第一个await让出执行权,console.log('2')是微任务。第二个await又让出执行权,console.log('3')是下一个微任务。每个await都会把后面的代码推入微任务队列。
🟠 第9题:经典综合题
console.log('start');
setTimeout(() => {
console.log('timeout1');
Promise.resolve().then(() => {
console.log('promise1');
});
}, 0);
setTimeout(() => {
console.log('timeout2');
}, 0);
Promise.resolve()
.then(() => {
console.log('promise2');
})
.then(() => {
console.log('promise3');
});
console.log('end');
点击查看答案
输出:start → end → promise2 → promise3 → timeout1 → promise1 → timeout2
解析:
- 同步代码:
start → end - 微任务队列:
promise2 → promise3(第二个then是第一个then执行后产生的) - 第一个宏任务(timeout1):输出
timeout1,产生微任务promise1 - 清空微任务:输出
promise1 - 第二个宏任务(timeout2):输出
timeout2
考点:宏任务执行后产生的微任务会在下一个宏任务之前清空。
🔴 第10题:async + setTimeout 混合
async function foo() {
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
await Promise.resolve();
console.log('3');
setTimeout(() => {
console.log('4');
}, 0);
console.log('5');
}
console.log('6');
foo();
console.log('7');
点击查看答案
输出:6 → 1 → 7 → 3 → 5 → 2 → 4
解析:
- 同步:
6 - 进入
foo():1,setTimeout回调入宏任务队列 await让出执行权- 同步:
7 - 微任务:
3,第二个setTimeout入宏任务队列,5 - 宏任务:
2,然后4
考点:await之前的代码是同步的,await之后的代码是微任务。setTimeout不管在哪,回调都是宏任务。
🔴 第11题:终极综合题
console.log('1');
async function async1() {
console.log('2');
await async2();
console.log('3');
}
async function async2() {
console.log('4');
}
setTimeout(() => {
console.log('5');
}, 0);
async1();
new Promise((resolve) => {
console.log('6');
resolve();
}).then(() => {
console.log('7');
});
console.log('8');
点击查看答案
输出:1 → 2 → 4 → 6 → 8 → 3 → 7 → 5
解析:
- 同步:
1 - 调用
async1():2,调用async2():4,await让出执行权 - Promise构造函数:
6,resolve,then回调入微任务队列 - 同步:
8 - 微任务队列:先执行await后面的
3,再执行then回调7 - 宏任务:
5
考点:async函数调用时,await之前的代码是同步执行的。async2()也是同步调用的(只是返回Promise,但函数体本身同步执行)。微任务按入队顺序执行。
🔴 第12题:地狱难度(面试真题)
setTimeout(() => {
console.log('A');
Promise.resolve().then(() => {
console.log('B');
});
}, 0);
new Promise((resolve) => {
console.log('C');
resolve();
console.log('D');
}).then(() => {
console.log('E');
setTimeout(() => {
console.log('F');
}, 0);
}).then(() => {
console.log('G');
});
console.log('H');
Promise.resolve().then(() => {
console.log('I');
});
点击查看答案
输出:C → D → H → E → I → G → A → B → F
解析:
- 同步代码:setTimeout回调入宏任务队列
- 同步代码:Promise构造函数
C → D,resolve,then回调入微任务队列 - 同步代码:
H - 同步代码:Promise.resolve().then入微任务队列
- 微任务队列(当前有两个:E的then、I的then):
- 执行E的then:输出
E,setTimeout入宏任务队列,返回的Promise resolve后,下一个then入微任务队列 - 执行I的then:输出
I - 执行G的then:输出
G
- 执行E的then:输出
- 宏任务队列(当前有两个:A的setTimeout、F的setTimeout):
- 执行A的setTimeout:输出
A,Promise.then入微任务队列 - 清空微任务:输出
B - 执行F的setTimeout:输出
F
- 执行A的setTimeout:输出
考点:微任务执行过程中产生的微任务会在当前轮次继续执行。宏任务执行后产生的微任务在下一个宏任务之前清空。
七、一张图总结事件循环
八、速记口诀
为了方便记忆,我总结了几条口诀:
- 同步先行:同步代码永远最先执行
- 微任务优先:每次宏任务执行完,先把微任务全部清空
- 宏任务排队:宏任务按入队顺序,一个一个来
- await是分界线:await前面的同步,后面的微任务
- Promise构造同步:构造函数里的代码是同步的
- 微任务套微任务:微任务执行中产生的新微任务,当前轮次就执行
写在最后
事件循环的题目看似千变万化,但核心规则就那几条。把宏任务和微任务的执行顺序搞清楚,再复杂的题也能拆解出来。
建议把上面12道题自己手写一遍,每道题都画出执行过程,比看十遍文章都有用。
有问题欢迎评论区交流,觉得有帮助的话点个赞呗 👋