在面试里,有一道经典问题:
👉 为什么 Promise.then 的回调一定比 setTimeout(fn, 0) 先执行?
很多人答不出来,只会模糊地说“事件循环”或者“异步机制”,其实根本没搞清楚底层原理。今天我们来彻底拆解。
1. JavaScript 执行模型:事件循环
JS 引擎是常驻内存的,它等待宿主环境(浏览器/Node)传递任务。 这些任务进入一个“事件循环”,循环的本质就是:
while (true) {
r = wait();
execute(r);
}
每一次执行就是一个 宏观任务(Macro Task)。
2. 宏观任务 vs 微观任务
JS 执行任务分两类:
- 宏观任务:宿主环境发起,比如
setTimeout、setInterval、DOM 事件。 - 微观任务:JS 引擎发起,比如
Promise.then、MutationObserver、queueMicrotask。
执行规则: 👉 每个宏观任务执行完,必须先清空当前的微观任务队列,才能进入下一个宏观任务。
3. 实验一:Promise vs setTimeout
看代码:
setTimeout(() => console.log("d"), 0);
new Promise(resolve => {
console.log("a");
resolve();
}).then(() => console.log("c"));
console.log("b");
输出顺序是:
a
b
c
d
为什么?
"a":同步任务,立即执行。"b":同步任务,立即执行。"c":Promise.then → 微任务 → 当前宏观任务结束前执行。"d":setTimeout → 新的宏观任务 → 下一个事件循环才执行。
4. 实验二:耗时 Promise + setTimeout
setTimeout(() => console.log("d"), 0);
new Promise(resolve => {
resolve();
}).then(() => {
let begin = Date.now();
while (Date.now() - begin < 1000); // 模拟耗时
console.log("c1");
new Promise(r => r()).then(() => console.log("c2"));
});
输出:
c1
c2
d
即使中间“卡”了一秒,新的微任务 c2 也会 插队 到 d 前面。
结论:Promise(微任务)永远比 setTimeout(宏任务)更快。
5. async/await:Promise 的语法糖
async/await 本质还是基于 Promise 的:
function sleep(t) {
return new Promise(r => setTimeout(r, t));
}
async function foo() {
console.log("a");
await sleep(2000);
console.log("b");
}
foo();
await 会把后续逻辑放进微任务队列,因此它和 Promise.then 的执行顺序一致。
6. 实战练习:红绿灯
最后留一个思考题:如何用 Promise 或 async/await 实现红绿灯循环?
需求:
- 绿灯 3 秒 → 黄灯 1 秒 → 红灯 2 秒 → 循环
思路(伪代码):
async function trafficLight() {
while (true) {
await change("green", 3000);
await change("yellow", 1000);
await change("red", 2000);
}
}
是不是比回调地狱优雅多了?
总结
Promise.then→ 微任务;setTimeout→ 宏任务。- 每个宏任务执行完,必须先清空微任务。
- 所以 Promise 一定比 setTimeout 快。
- async/await 只是语法糖,本质仍是微任务调度。
理解了这一点,不仅能秒答面试题,还能写出更可控的异步逻辑。
互动问题:
👉 你用过最骚的 Promise + setTimeout 组合技巧是什么?
欢迎留言分享,看看谁的姿势最骚!