await、queueMicrotask、Promise.then 和 setTimeout 的执行顺序怎么推?
先理解原理和示例,再练习完整口述与追问。
回答要点
同步代码先走完,调用栈清空后才清空微任务;微任务里新排入的微任务仍在本轮清空。await 即使等的是已完成的值,恢复也要排一次微任务。setTimeout 是宏任务,要等当前轮微任务全部结束。顺序由“谁先登记”决定,不是由写在源码更下面决定。
原理拆解
一轮事件循环先执行当前调用栈里的同步代码。栈空之后,按登记顺序执行微任务,包括 Promise.then、queueMicrotask、MutationObserver,以及 await 的恢复。执行微任务时又登记的微任务,排到当前微任务队列尾部,仍在进入下一次宏任务之前执行。setTimeout(fn, 0) 只是把 fn 放进宏任务队列,延迟是最早可执行时间,不是“插到微任务前面”。
await 表达式 等价于把表达式交给 Promise.resolve,再把函数后半段作为 then 回调。因此 await 写在同步函数调用期间,它的恢复会比这行之后才执行的 queueMicrotask 和 .then 更早入队。恢复执行后再遇到第二个 await,后半段才重新入队,排到当时队列里已有的微任务之后。
示例与推演
console.log('1')
async function run() {
console.log('2')
await Promise.resolve()
console.log('4')
await null
console.log('6')
}
run()
queueMicrotask(() => console.log('5'))
Promise.resolve().then(() => console.log('3'))
setTimeout(() => console.log('7'))
console.log('8')
同步阶段打印 1、2、8。run 执行到第一个 await 时,恢复回调已经入队,然后才登记 5 和 3,7 进入宏任务。第一条微任务恢复后打印 4,第二个 await null 把打印 6 的回调排到 5 和 3 后面。所以顺序是 1 2 8 4 5 3 6 7。
易错点
浏览器还要渲染,微任务清空后才可能绘制;连续微任务会挡住渲染和定时器。Node 还有 process.nextTick 和 setImmediate,顺序与浏览器不完全相同,见专题深化第 1 题。不要背“谁写在前面谁先执行”,先看它在哪一次调用里被登记。
参考回答可直接练习口述
同步代码全部执行完,再按入队顺序清空微任务,微任务里新产生的微任务继续排在本轮。await 会把后续代码登记成微任务,而且在执行到这行时就入队,所以它可能早于后面才写到的 queueMicrotask 和 then。setTimeout 属于下一轮宏任务。上面这段代码的顺序是 1、2、8、4、5、3、6、7:4 先于 5,是因为它在 run 的同步阶段就已经入队;6 晚于 3,是因为第二个 await 发生在第一条微任务里面。