await 是执行栈的物理切出而非暂停,引擎立即求值右侧表达式,若为 pending Promise 则弹出当前 async 函数上下文,后续代码转为微任务回调;续体通过闭包捕获上下文,在新栈帧中执行。

await 不是暂停,而是执行栈的“物理切出”
当 JavaScript 引擎遇到 await 表达式时,并不会在当前调用栈上“挂起”函数或保留执行帧。它立即求值右侧表达式(如 fetch() 或 Promise.resolve()),若结果是一个 pending Promise,引擎就主动弹出当前 async 函数的执行上下文——这导致调用栈真实地、物理地断裂:栈顶帧被移除,控制权交还给上层宏任务(比如事件处理函数或 script 标签同步代码)。
此时,async 函数内部后续语句(包括 await 后面的变量赋值、return、throw)尚未执行,也不保留在栈中,而是被编译器转化为一个微任务回调,等待注册到微任务队列。
上下文还原靠的是“续体(continuation)”而非栈恢复
所谓“异步还原”,并非把旧栈帧重新压回去。引擎为每个 await 生成一个闭包形式的续体函数,它捕获了 await 前的局部变量、this 绑定、词法环境等关键上下文信息。当该 Promise settle 后,这个续体作为微任务被取出执行——它是在全新的执行栈帧中启动的,但通过闭包复用了原 async 函数的运行时状态。
这意味着:
- 两次执行(await 前与续体执行)共享同一词法作用域,但属于两个独立的调用栈生命周期
- 调试器看到的“断点连续”是工具层模拟,底层并无栈帧延续
- 若 await 后修改了外部可变对象(如数组 push),这些变更对续体可见;但若依赖
arguments或caller等动态栈信息,则会丢失
微任务队列是上下文切换的唯一协调者
await 的调度完全依赖微任务队列的清空机制。一次宏任务结束时,引擎不是只执行一个微任务,而是批量、顺序、不可中断地执行全部已排队微任务——包括由多个 await 注册的续体、Promise.then() 回调、queueMicrotask() 等。
这种设计保证了:
- await 后代码总在当前宏任务之后、下一个宏任务之前执行(比
setTimeout早) - 连续 await 形成链式微任务:第一个续体执行完,才触发第二个 await 的 Promise 构造与监听,再注册第二个续体
- 没有“中间插入”可能:哪怕你在 await 后立刻调用
queueMicrotask(() => console.log('x')),它和 await 续体同属本轮微任务,按注册顺序执行
典型断裂-还原场景验证
看这段代码:
console.log('1');
async function f() {
console.log('2');
await Promise.resolve();
console.log('3');
}
f();
console.log('4');
执行栈变化如下:
- 同步阶段:1 → 2 → 4 入栈并执行,f 的上下文在
await处弹出,栈清空 - 微任务阶段:引擎取
await注册的续体,新建栈帧,执行console.log('3') - 整个过程无栈帧复用,“2”和“3”处于两个完全隔离的调用栈周期
这就是 await 语义的物理本质:用微任务队列做上下文搬运工,用闭包续体代替栈帧延续。


















