我们来深入剖析这个极具迷惑性的现象——async/await 到底是不是“同步写法”?
很多人会说:
“
async/await是 Promise 的语法糖,它让异步代码像同步代码一样书写。”
但真相是, “像同步” ≠ 同步。async/await 只是一场语法幻觉,它在执行机制上仍然根植于 事件循环与微任务队列的异步调度中。
下面我们就来揭开 async/await 的“欺骗性同步”的面具,透彻理解它背后的 微任务机制 和 执行顺序差异。
第一眼假象:“写起来像同步,运行起来也同步吧?”
你可能会写过这样的代码:
async function foo() {
console.log("start");
await Promise.resolve();
console.log("end");
}
foo();
console.log("outside");
初学者往往以为输出会是:
start
end
outside
但实际上,输出是:
start
outside
end
为什么?因为 await 不是“阻塞当前线程”的同步操作,而是把“之后的代码”包进了一个 微任务(microtask) ,等当前所有同步代码执行完,再排队执行。
async/await 的真实行为:语法糖 + 微任务
要彻底理解 async/await 的机制,先明确两点:
- async 函数返回的是 Promise
- await 会中断当前 async 函数的执行,把“剩余代码”封装为微任务,放入 microtask queue
这意味着:
await somePromise;
// 相当于
somePromise.then(() => {
// 剩下的代码在微任务中运行
});
换句话说,await 后的语句不是“顺着执行”,而是“挂起执行” 。
案例拆解:你以为的同步,其实只是安排好了异步
例子 1:顺序混乱的初体验
console.log("A");
setTimeout(() => {
console.log("B");
}, 0);
(async () => {
console.log("C");
await null;
console.log("D");
})();
console.log("E");
输出结果是:
A
C
E
D
B
解释:
A:同步输出setTimeout注册了宏任务(下一轮执行)C:立即执行 async 函数内 await 之前的同步语句await null:产生一个微任务(相当于Promise.resolve().then(...))E:当前宏任务执行完D:await 后的语句作为微任务执行B:下一轮宏任务才执行
await 后面的代码其实被“丢”到了微任务队列里,直到本轮执行结束之后才执行。
微任务:这才是 async/await 真正的幕后黑手
事件循环中,执行顺序如下:
- 执行一个宏任务(比如当前
script脚本) - 在这个宏任务期间注册的所有微任务全部执行(包括
.then()、await之后的部分) - 渲染视图
- 开始下一轮宏任务
所以 await 的运行过程如下:
async function foo() {
console.log("1");
await bar(); // bar 返回一个 Promise
console.log("2"); // 被封装为微任务
}
等价于:
function foo() {
console.log("1");
bar().then(() => {
console.log("2"); // 放进微任务队列
});
}
所以 await 并没有阻塞函数继续执行,而是 “挂起”,把剩下的代码交给微任务队列。
为何这会带来“欺骗性的同步感”?
因为我们写的是:
const data = await fetchData();
doSomething(data);
这种写法读起来就像是:
const data = fetchData();
doSomething(data);
但执行起来:
fetchData()被异步调度doSomething()不是马上执行,而是变成微任务,在 fetch 完成后才执行
这就像你下楼买咖啡,留了张纸条告诉同事:“我回来再继续做报告”,然后立刻去楼下了。看起来你很“同步”,实际上你溜得飞快。
更深陷阱:forEach + async = 无限痛苦
很多人以为可以这样写:
[1, 2, 3].forEach(async (item) => {
await someAsyncCall(item);
});
但实际情况是:forEach 不会等待 async 函数的执行完成,也不会顺序执行。
想要顺序执行异步任务,正确写法应是:
for (const item of [1, 2, 3]) {
await someAsyncCall(item); // 保证按顺序
}
原因就是:await 的语义并不影响原生同步 API 的行为,比如 forEach 不认 async 回调。
更诡异的异步陷阱:两个 await,谁先执行?
async function foo() {
await console.log("A");
console.log("B");
}
async function bar() {
await console.log("C");
console.log("D");
}
foo();
bar();
输出:
A
C
B
D
这说明 await 后面的语句是被作为微任务注册的,多个 await 并不会“等待前一个 await 执行完”!
每一个 await,都是独立的暂停点。它们的“恢复”顺序,取决于微任务注册的顺序。
总结回顾:async/await 的“假面舞会”
| 误解 | 真相 |
|---|---|
await 会阻塞函数继续执行 | ❌ 不会,它只是暂停,余下部分变成微任务 |
async/await = 同步执行 | ❌ 只是“看起来像同步”,实际上全部是异步调度 |
| 多个 await 会按顺序执行 | ✅ 但仅在 for...of、while 这种显式结构中,forEach 无效 |
await 后的语句立即执行 | ❌ 不会,它们要等当前所有同步代码执行完,才在微任务队列中执行 |
深度建议:写 async/await,别只图省事
在实际开发中,建议:
- 知道微任务机制:别指望
await后代码立即生效,尤其涉及 DOM 或状态变更。 - 避免在 forEach/map/filter 中使用 await:这类结构无法处理 async 正确的顺序逻辑。
- 调试中用
console.log检查顺序:验证你的异步是否真的按预期执行。 - 需要真正同步顺序控制时,用显式循环:如
for...of加await。 续加深分析。