三点都对,尤其第一点——微任务队列是“抽干”不是“轮询”,很多教程只讲“再清空微任务”却不说清空过程中新产生的也算,推 async 题必然翻车。
再补几个精度细节:
await 的 tick 数取决于跟的是什么。 await 1、await undefined 是 1 个 tick(PromiseResolve 直接 resolve);await Promise.resolve() 在现代 V8 也是 1 个 tick;但 await 一个 thenable(手写的 {then(r){r()}})要走 PromiseResolveThenableJob,多一个 tick。另外 Chrome 73 / Node 12 之前 await 原生 Promise 是 3 个 tick,老博客里的推导和现在的 DevTools 对不上就是这个原因,别拿旧结论纠错。你说的“注册时机早于 then 链第二个回调”没错,但更准确的机制是:promise2 的回调压根不是一开始就排进队列的,它挂在第一个 then 返回的 promise 上,要等第一个 then 执行完才入队——这才是它排在 async1 end 后面的真正原因。
Node 那边再补两点。 setImmediate 在 I/O 回调内部一定先于 setTimeout(fn,0)(I/O 回调属于 poll 阶段,紧接着就进 check 阶段),只有主模块里才不确定;process.nextTick 递归同样会饿死 microtask,不只是 then 递归会饿死定时器。
帖子截断那步我帮你接上:栈空时微任务队列初始是 [promise1 回调, async1 的 await 恢复]。清 promise1 → 输出并把它返回的 promise resolve,promise2 回调此刻才入队,队列变成 [await 恢复, promise2 回调] → 输出 async1 end → 输出 promise2 → 取宏任务 setTimeout。这步正是整题的核心,值得单独加粗。
坑:微任务递归饿死渲染时 DevTools 会卡顿,可以先暂停脚本,看微任务队列尾部是谁在重注册。

正式会员
认证极客










