JavaScript async/await:事件循环与执行顺序详解 · 第 5 / 5 节

第 5 课:执行顺序的预测与验证

本课目标:

  • 运用四步流程预测任意混合异步代码片段的输出顺序。
  • 预测一段混合了同步打印、setTimeout.then()await 的代码片段的精确顺序。
  • 在真实的 Node 或浏览器控制台里运行代码验证预测,并用模型解释每一行。

先修条件:第 1-4 课(调用栈、任务队列、事件循环、微任务队列、await) | 上一篇 << 04

从「大概是这样」到「我确定,理由如下」

规则你现在都知道了:运行至完成、任务队列、事件循环、微任务队列和它先清空的优先级,还有 await 做的事。但「知道规则」和「在压力下把规则用到一段纠缠的代码上」是两种本事。面对六行混着定时器、promise 和 await 的代码,人很容易卡住。这一课给你一套固定流程,让你不必卡住,只需按步骤执行;然后在真实控制台里证明答案,而不是相信感觉。能把顺序报出来、报得对、还能重复做到,这就是整门课的目的。

讲解

下面这套流程对任何代码片段都适用。在纸上按顺序做这四步。

  1. 做同步遍历。 从上到下读。每个普通行现在就运行,async 函数运行到它的第一个 await 为止。把打印的内容按顺序写下来。每经过一个被推迟的东西就记一笔:setTimeout 的回调进任务队列;.then()/.catch()/.finally() 的回调和 await 之后的续接部分进微任务队列12
  2. 清空所有微任务。 同步遍历结束、栈空了,把整个微任务队列按安排顺序运行完;如果微任务又安排了微任务,继续运行3。写下打印的内容。
  3. 运行一个任务。 从任务队列取出等得最久的那一个回调,运行它1
  4. 重复第 2-3 步。 那个任务之后,再清一遍微任务,再运行下一个任务,直到两个队列都空。

把一切串起来的只有一条规则:微任务永远在下一个任务之前清空1。第 2、3 步就是这条规则的编码:微任务队列没清空之前,你绝不运行第二个任务。在纸上把两个队列各记成一张单子,这套流程就是纯机械操作。

现在先对下面的代码片段跑一遍流程,再看答案。

如果你的判断对了,那不是猜的,你是在执行一套流程。如果偏了,失误几乎总在第 2 步和第 3 步之间:在微任务队列还没空时就让任务运行了。整门课里所有「顺序不对」的意外,都来自这一条边界。

完整示例(跟着做)

对这段代码把四步出声走一遍:

第 1 步:同步遍历。 console.log("A") 打印 AsetTimeout 那一行把回调 B 排进任务队列。go() 同步运行到它的 await:打印 C,然后挂起,把续接部分 D 排进微任务队列。.then() 那一行把 E 排进微任务队列(在 D 之后)。console.log("F") 打印 F。到目前为止同步输出:ACF。任务队列:[B]。微任务队列:[D, E]

第 2 步:清空所有微任务。 栈空了。运行 D(打印 D),再运行 E(打印 E)。微任务队列空了。

第 3 步:运行一个任务。 从任务队列取出 B 运行(打印 B)。

第 4 步:重复。 两个队列都空了,结束。最终顺序:ACFDEB。每一步都来自流程,不靠直觉。

你来试试(填空示例)

自己跑一遍四步。边读边记一张「任务队列」单和一张「微任务队列」单,然后把顺序填出来:

答案:顺序是 xprzqy。第 1 步(同步遍历):x 打印;setTimeout 把任务 y 入队;.then() 把微任务 z 入队;m() 运行到它的 await,打印 p 并把微任务 q 入队;r 打印。所以同步输出是 xpr;任务队列 [y];微任务队列 [z, q].then()m() 运行之前就安排了,所以 z 排在 q 前面)。第 2 步按顺序清微任务:z,然后 q。第 3 步运行那个任务:y。第 3 行的 .then() 先于 m()await 续接部分打印,因为它安排得更早:同一个队列内部,顺序就是安排顺序。

小结 + 下一步

你有的是一套流程,不是一种感觉:做同步遍历(async 函数运行到第一个 await),清空整个微任务队列,运行一个任务,重复,而微任务永远在下一个任务之前清空。这一台机器(栈,然后所有微任务,然后一个任务)解释了你开课时困惑过的每一个「顺序不对」。console.log 从来没有乱序,是你拿错了模型去读它们。现在你拿对了,而且能在任何控制台里证明。范围说明一条:这里教的是浏览器/通用事件循环模型,它正是让同步、微任务、任务三者顺序可预测的那一个;Node 在其上又叠了一层机制,process.nextTick(优先级高于 promise 微任务队列)和 libuv 的阶段(timers / I/O / setImmediate)可能造成本模型不覆盖的重排,这超出了本课范围。保持两张单子的习惯:哪次顺序再让你意外,就把任务队列和微任务队列并排写下来,走一遍四步。

Footnotes

  1. MDN: JavaScript execution model — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Execution_model 2 3

  2. MDN: await — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await

  3. MDN: Using microtasks in JavaScript with queueMicrotask() — https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide

练习

01

把收官代码片段(预测块里 1 到 6 那段)粘进 capstone.mjs。先用四步流程写下预测,再用 node capstone.mjs 运行,逐行核对是否和你写下的预测一致。

第 1 级(热身)
完成标准 · 本地勾选
02

在你自己的编辑器里新写一段代码,命名 myorder.mjs,按你选择的任意排布包含所有这些成分:至少两条同步 console.log、一个 setTimeout(fn, 0)、一个挂在已解决 promise 上的 .then(),以及一个带 await 且 await 之后有打印的 async 函数。然后在纸上或注释里:

第 2 级(收官:预测、运行、解释)
  1. 用四步流程预测完整输出顺序,并在每一行旁边写一个简短理由(同步/微任务/任务)。
  2. node myorder.mjs(或浏览器控制台)运行,和预测对照。
  3. 如果有任何一行不同,找出第一个分歧点,用一句话写出你漏掉的规则。
完成标准 · 本地勾选