在 JavaScript 中,setTimeout、Promise 和 async/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状态变为fulfilled或rejected时,.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 先定义,它也不会立即入队,直到 p2 被 resolve。
四、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
✅ 执行机制解析
async函数返回一个Promise对象;- 函数体同步执行,遇到
await时:- 如果
await后是一个Promise,函数暂停执行,让出执行权; - 该
Promise的.then()回调(即await后的代码)被加入 microtask queue;
- 如果
- 当前宏任务继续执行;
- 宏任务结束后,执行 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 |
| 2 | async1() 调用 | 4 |
| 3 | await async2() 执行 async2 | 6 |
| 4 | await 遇到 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 |
六、三者核心区别对比表
| 特性 | setTimeout | Promise.then | async/await |
|---|---|---|---|
| 任务类型 | 宏任务(Macrotask) | 微任务(Microtask) | 微任务(基于 Promise) |
| 执行时机 | 下一个事件循环周期 | 当前宏任务结束后立即执行 | 同 Promise.then |
await 后代码 | - | - | 加入 microtask queue |
| 返回值 | number(定时器ID) | Promise | Promise |
| 错误处理 | 回调内处理 | .catch() 或 try/catch(async) | try/catch |
| 是否阻塞 | 不阻塞 | 不阻塞 | 语法上阻塞(实际是暂停) |
七、关键结论
Promise构造函数是同步的,.then()是微任务;setTimeout即使延迟为 0,也是宏任务,晚于所有微任务执行;async/await是Promise的语法糖,await让函数暂停,后续代码作为微任务执行;- 微任务优先于宏任务:一个事件循环中,先执行所有微任务,再执行下一个宏任务。
💡 结语
“
setTimeout是‘下一轮’,Promise是‘本轮末’,async/await是‘语法糖+微任务’。”
掌握这三者的执行机制,你就能轻松应对任何异步执行顺序的面试题。
记住:
- 宏任务:
setTimeout、setInterval、I/O、UI渲染; - 微任务:
Promise.then、MutationObserver、queueMicrotask; - 事件循环:宏任务 → 微任务 → 宏任务 → ...