【js篇】深入剖析:setTimeout、Promise 与 async/await的执行机制差异

81 阅读4分钟

在 JavaScript 中,setTimeoutPromiseasync/await 都涉及异步操作,但它们的执行时机、任务类型和底层机制有本质区别。理解这些差异,是掌握 事件循环(Event Loop) 和异步编程的关键。

本文将从任务队列、执行顺序、返回值等维度,彻底讲清三者的区别。


一、核心概念回顾

方法类型执行时机
setTimeout宏任务(Macrotask)加入宏任务队列,等待当前所有微任务执行完后执行
Promise.then微任务(Microtask)加入微任务队列,当前宏任务结束后立即执行
async/await语法糖(基于 Promise)await 后的异步操作本质是 Promise,触发微任务

二、setTimeout:宏任务的代表

✅ 基本用法

console.log('Start');
setTimeout(() => {
  console.log('Timeout');
}, 0);
console.log('End');

输出顺序

Start
End
Timeout

✅ 关键特性

  • 任务类型:宏任务(Macrotask);
  • 执行时机:即使延迟为 0,也会等到当前所有同步代码和微任务执行完毕后才执行;
  • 队列:放入 macrotask queue
  • 用途:延迟执行、避免阻塞、模拟异步。

⚠️ 注意:setTimeout(fn, 0) 并不表示“立即执行”,而是“尽快执行”,至少需要等待一个事件循环周期。


三、Promise:微任务的核心

✅ 基本用法

console.log('Start');
Promise.resolve().then(() => {
  console.log('Promise');
});
console.log('End');

输出顺序

Start
End
Promise

✅ 关键特性

  • Promise 构造函数是同步执行的
console.log('Start');
new Promise(resolve => {
  console.log('Promise constructor'); // 立即执行
  resolve();
}).then(() => {
  console.log('Then');
});
console.log('End');

输出

Start
Promise constructor
End
Then
  • .then() 回调是微任务
    • Promise 状态变为 fulfilledrejected 时,.then() 回调被加入 microtask queue
    • 在当前宏任务结束后,立即执行所有微任务,再进入下一个宏任务。

Promise 的任务入队时机

状态.then() 回调入队时机
resolved / rejected立即加入当前事件循环的 microtask queue
pending等待状态改变后,再加入 future microtask queue
const p1 = Promise.resolve();
p1.then(() => console.log('p1 then')); // 立即入队

const p2 = new Promise(() => {}); // pending
p2.then(() => console.log('p2 then')); // 不入队,直到 resolve/reject

setTimeout(() => {
  console.log('Timeout');
}, 0);

即使 p2.then 先定义,它也不会立即入队,直到 p2resolve


四、3)async/await:基于 Promise 的语法糖

✅ 基本用法

async function func() {
  console.log('Start');
  await Promise.resolve();
  console.log('After await');
}

func();
console.log('End');

输出顺序

Start
End
After await

✅ 执行机制解析

  1. async 函数返回一个 Promise 对象;
  2. 函数体同步执行,遇到 await 时:
    • 如果 await 后是一个 Promise,函数暂停执行,让出执行权
    • Promise.then() 回调(即 await 后的代码)被加入 microtask queue
  3. 当前宏任务继续执行;
  4. 宏任务结束后,执行 microtask queue 中的所有任务,包括 await 后的代码。

await 的本质

await 相当于:

// 等价转换
await somePromise;

// 相当于
somePromise.then(() => {
  // 继续执行后续代码
});

所以 await 后的代码本质上是一个 微任务


五、经典面试题:执行顺序大乱斗

console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

Promise.resolve().then(() => {
  console.log('3');
});

async function async1() {
  console.log('4');
  await async2();
  console.log('5');
}

async function async2() {
  console.log('6');
}

async1();

console.log('7');

正确输出顺序

1
4
6
7
3
5
2

🔍 逐步解析

步骤执行内容输出
1同步代码1
2async1() 调用4
3await async2() 执行 async26
4await 遇到 Promise.resolve()async2 无返回值,等价于 Promise.resolve(undefined)暂停,console.log('5') 加入 microtask queue
5继续同步代码7
6当前宏任务结束,执行 microtask queue:
Promise.then()3
await 后续代码 → 5
3, 5
7下一个宏任务:setTimeout 回调2

六、三者核心区别对比表

特性setTimeoutPromise.thenasync/await
任务类型宏任务(Macrotask)微任务(Microtask)微任务(基于 Promise)
执行时机下一个事件循环周期当前宏任务结束后立即执行Promise.then
await 后代码--加入 microtask queue
返回值number(定时器ID)PromisePromise
错误处理回调内处理.catch()try/catch(async)try/catch
是否阻塞不阻塞不阻塞语法上阻塞(实际是暂停)

七、关键结论

  1. Promise 构造函数是同步的.then() 是微任务;
  2. setTimeout 即使延迟为 0,也是宏任务,晚于所有微任务执行;
  3. async/awaitPromise 的语法糖await 让函数暂停,后续代码作为微任务执行;
  4. 微任务优先于宏任务:一个事件循环中,先执行所有微任务,再执行下一个宏任务。

💡 结语

setTimeout 是‘下一轮’,Promise 是‘本轮末’,async/await 是‘语法糖+微任务’。”

掌握这三者的执行机制,你就能轻松应对任何异步执行顺序的面试题。

记住:

  • 宏任务setTimeoutsetIntervalI/OUI渲染
  • 微任务Promise.thenMutationObserverqueueMicrotask
  • 事件循环:宏任务 → 微任务 → 宏任务 → ...