await 不是语法暂停,而是控制权移交事件循环导致的时序割裂;它将后续代码封装为微任务,在当前宏任务结束后立即执行,优先级高于 settimeout 等宏任务。

await 语句前后的代码看似连续,实则存在明确的物理执行断层——这不是语法上的“暂停”,而是控制权移交事件循环后、由微任务队列调度导致的**时序割裂**。理解这一点,关键在于看清 await 如何触发函数挂起、后续逻辑如何被注入微任务队列,以及它与同步代码、宏任务之间的调度优先级关系。
await 触发的函数挂起是“移交控制权”,不是“线程阻塞”
当 async 函数执行到 await 表达式时:
- 引擎立即求值 await 右侧表达式(如 Promise.resolve(1) 或 fetch()),得到一个 Promise 实例
- 若该 Promise 已处于 fulfilled/rejected 状态,后续代码会**立刻**被安排进微任务队列(不等待下一轮)
- 若 Promise 处于 pending 状态,当前 async 函数的执行上下文被挂起,控制权交还主线程,继续执行后续同步代码或已排队的其他任务
- await 后面的所有语句(包括 console.log、变量赋值、return 等)不再属于当前调用栈,而是被封装为一个续体(continuation),等待 Promise settle 后作为微任务入队
await 后续代码实际进入微任务队列,而非立即执行
这段代码清晰暴露了物理断层:
console.log('A');
async function foo() {
console.log('B');
await Promise.resolve();
console.log('C');
}
foo();
console.log('D');
输出顺序是 A → B → D → C,原因在于:
- 'A'、'B'、'D' 是同步代码,按序执行
- await Promise.resolve() 虽然立即完成,但其后续的 'C' 并不插在 'D' 前执行,而是被推入微任务队列
- 主线程执行完同步段(A/B/D)后,调用栈清空,事件循环立刻清空微任务队列 → 'C' 得以执行
- 这个“B 执行完 → D 执行 → C 才执行”的间隔,就是 await 造成的物理时序断层:它把本该紧随其后的逻辑,硬生生推迟到本轮宏任务结束之后
多个 await 形成微任务链,而非嵌套阻塞
连续 await 并不累积“暂停”,而是依次注册微任务:
async function chain() {
console.log(1);
await Promise.resolve();
console.log(2);
await Promise.resolve();
console.log(3);
}
执行过程是:
- 同步输出 1,第一个 await 将 console.log(2) 注册为微任务
- 同步部分结束,微任务队列执行 → 输出 2
- 此时第二个 await 触发,将 console.log(3) 注册为**下一个微任务**(注意:不是嵌套在前一个微任务里)
- 微任务队列再次清空 → 输出 3
- 整个过程没有“await 套 await”的堆栈延迟,只有微任务逐个出队的线性推进
与宏任务对比,断层位置一目了然
加入 setTimeout 后,更能定位断层所在:
console.log('start');
setTimeout(() => console.log('timeout'), 0);
async function test() {
console.log('async start');
await Promise.resolve();
console.log('async end');
}
test();
console.log('end');
输出:start → async start → end → async end → timeout
- 同步段:start / async start / end
- 微任务段:async end(await 后续)→ 立即执行
- 宏任务段:timeout → 等待下一轮事件循环
- 可见,await 后的代码和 setTimeout 的回调虽都“延迟”,但断层层级不同:前者卡在**同一轮事件循环的末尾**,后者卡在**下一轮事件循环的开头**
这个断层不是缺陷,而是 JavaScript 异步模型的精确体现:await 把代码切分,让出主线程,再靠微任务保证响应及时性。真正写代码时,意识到“await 后不是马上运行”,就能避开竞态、误判状态、错估执行时机等常见问题。











