5、Promise 为什么比 setTimeout 先执行?一次搞懂 JS 执行顺序

134 阅读2分钟

在面试里,有一道经典问题:

👉 为什么 Promise.then 的回调一定比 setTimeout(fn, 0) 先执行?

很多人答不出来,只会模糊地说“事件循环”或者“异步机制”,其实根本没搞清楚底层原理。今天我们来彻底拆解。


1. JavaScript 执行模型:事件循环

JS 引擎是常驻内存的,它等待宿主环境(浏览器/Node)传递任务。 这些任务进入一个“事件循环”,循环的本质就是:

while (true) {
  r = wait();
  execute(r);
}

每一次执行就是一个 宏观任务(Macro Task)


2. 宏观任务 vs 微观任务

JS 执行任务分两类:

  • 宏观任务:宿主环境发起,比如 setTimeoutsetInterval、DOM 事件。
  • 微观任务:JS 引擎发起,比如 Promise.thenMutationObserverqueueMicrotask

执行规则: 👉 每个宏观任务执行完,必须先清空当前的微观任务队列,才能进入下一个宏观任务


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

为什么?

  1. "a":同步任务,立即执行。
  2. "b":同步任务,立即执行。
  3. "c":Promise.then → 微任务 → 当前宏观任务结束前执行。
  4. "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. 实战练习:红绿灯

最后留一个思考题:如何用 Promiseasync/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 组合技巧是什么? 欢迎留言分享,看看谁的姿势最骚!