第 4 课:await 的执行机制
本课目标:
- 解释 async 函数会同步运行到它的第一个
await。- 解释
await之后的代码作为微任务运行,而不是同步运行。- 正确判断
await只挂起它自己的函数,从不阻塞线程。
await 读起来像「在这里等」,整个程序会冻结吗?
await 这个词是给直觉挖的陷阱。它看起来像「停下一切,原地等待」,于是人们想象整个程序冻结在那一行,直到 promise 落定。如果真是那样,一个慢 await 就会锁死页面。事实不是:UI 继续响应,其他代码继续运行。所以 await 不可能表示「冻结线程」。它实际做的事是你模型里最后缺的一块,而且复用的正是第 3 课你已经掌握的机器:它把它后面的一切变成一个微任务。
讲解
先看第一个 await 之前运行的部分。async 函数在那之前并不特殊:它同步运行,就在现在,就在调用栈上,和任何函数一样1。一个完全没有 await 的 async 函数会整个同步运行;「async」要到第一个 await 才开始起作用1。
再看 await 本身。它暂停自己所在 async 函数的执行,直到所等的 promise 落定2。但「暂停」不等于「阻塞线程」。控制权退出这个函数,回到调用者,调用者继续运行程序的其余部分;当所等的值就绪时,一个继续运行这个被暂停函数的微任务会被安排出来2。换句话说,await 把你的函数切成两半:await 之前的一切现在运行,之后的一切变成一个稍后运行的微任务,效果和把函数剩余部分装进 .then() 一模一样。
这就是 await 从不冻结任何东西的原因。await 表达式从不阻塞主线程,它只推迟真正依赖结果的那部分代码,也就是 await 之后的行2。就算 promise 已经解决,这一点也成立:函数仍会暂停到下一个 tick,调用者在此期间恢复运行2。所以 await 之后的代码永远被推迟成微任务,从不原地运行。
对着规则走一遍。console.log("start") 打印 start。然后 greet() 被调用,同步运行到它的 await:打印 A。在 await 处,greet 挂起,console.log("B") 变成一个微任务,控制权回到调用者。回到主脚本,console.log("end") 打印 end。现在栈空了,事件循环清空微任务队列,续接部分运行,打印 B。顺序是 start、A、end、B:A 是同步的(在 await 之前),B 则在微任务队列里等过。
因为 await 之后的代码就是一个被安排的微任务,第 3 课的一切原样适用:await 的续接部分会在下一个 setTimeout 任务之前清空,也会和其他微任务按安排顺序交错。await 没有增加新规则,它只是把你的函数喂进了你已经理解的微任务队列。
完整示例(跟着做)
两个 async 调用加一条同步打印,看每个函数的两半各落在哪里:
在每个函数的 await 处把它切开。first() 同步运行到自己的 await:打印 first: before await,然后挂起,安排续接部分(微任务 1)。控制权回来,second() 同步运行到自己的 await:打印 second: before await,然后挂起,安排续接部分(微任务 2)。接着 console.log("synchronous end") 打印。现在栈空,微任务队列按安排顺序清空:first: after await(微任务 1),然后 second: after await(微任务 2)。顺序是:first: before await、second: before await、synchronous end、first: after await、second: after await。每个「before await」都是同步的;每个「after await」都是微任务。
你来试试(填空示例)
预测输出。给每一行标「同步」(await 之前)或「微任务」(await 之后)。把顺序填出来:
答案:顺序是 3、1、4、2。走一遍:console.log("3") 同步打印 3。run() 同步运行到它的 await,打印 1,然后挂起,把 console.log("2") 安排成微任务;控制权回到调用者。console.log("4") 同步打印 4。栈空了,微任务运行,打印 2。run() 里的两条打印落在 await 的两侧:1 是同步的,2 是微任务,所以调用者的同步 4 插在了它们中间。
小结 + 下节课预告
async 函数同步运行到它的第一个 await;在 await 处,它只挂起自己,把控制权交回调用者,并把函数剩余部分安排成微任务。await 从不阻塞线程,它只推迟它后面的行,效果等同把它们包进 .then()。机制到此收齐。你现在握有这套模型的每条规则:栈运行同步代码,然后微任务队列彻底清空,然后运行一个任务,循环往复,而 await 只是把续接部分送进微任务队列。第 5 课把这些规则变成一套可重复的流程,用它推算一段真正纠缠的代码片段的输出,然后在真实控制台里证明你的判断。