第 3 课:微任务队列与执行优先级
本课目标:
- 区分微任务队列和任务队列。
- 解释 promise 的
.then()回调是微任务,不是任务。- 预测整个微任务队列会在下一个任务之前清空,所以
.then()先于setTimeout(fn, 0)运行。
.then() 写在后面,为什么先于定时器运行?
击穿多数人模型的正是这个顺序。你在某一行安排了 setTimeout(fn, 0),几行之后(在它后面)又给一个已解决的 promise 挂了 .then()。两者都「被推迟」,看起来都该等同步代码之后运行。按第 2 课的任务队列规则,你可能预期先登记的定时器赢。它没有。.then() 先运行,每次都是。存在第二个优先级更高的等候室,promise 就住在里面。看见它之后,promise 对定时器的顺序就不再是抛硬币。
讲解
队列有两个,不是一个。第 2 课认识的任务队列装的是 setTimeout 的回调。它旁边还有一个独立的微任务队列,而且正式地说,微任务队列不是任务队列,它是独立的一种东西1。promise 用微任务队列来运行自己的回调:.then()、.catch() 或 .finally() 的处理函数都被安排成微任务2。
差别在于两个队列各自何时被清空,这就是本课的全部内容。当前同步代码运行结束、栈空之后,事件循环并不会立刻抓下一个任务。它先运行所有微任务,把微任务队列彻底清空,然后才从任务队列取一个任务3。微任务优先级更高,微任务队列会在碰任务队列之前先被清空3。
对比这两个回调。setTimeout(fn, 0) 的回调进任务队列。.then() 的回调进微任务队列。同步脚本结束时,微任务队列先清空,所以 .then() 先于 setTimeout 运行,无论哪一行写在前面2。另外,.then() 的处理函数永远异步运行,哪怕 promise 已经落定;它是被安排的,从不原地执行4。
带着两个队列走一遍。console.log("1") 同步运行,打印 1。setTimeout 那一行把回调排进任务队列。Promise.resolve().then(...) 那一行把回调排进微任务队列。console.log("4") 同步运行,打印 4。现在脚本结束、栈空了。事件循环先清空微任务队列,打印 3。然后才从任务队列取一个任务,最后打印 2。顺序是 1、4、3、2。
从「先把微任务队列整个清空」还能推出两件事。如果一个微任务又安排了新的微任务,新的那个也会在下一个任务之前运行:事件循环会一直清下去,直到微任务队列真正为空,哪怕微任务不断添加更多2。而链式 promise(.then().then())的每个处理函数都被安排成一个独立的微任务,所以一条很长的 promise 链可以在一个 setTimeout(fn, 0) 运行之前完整走完。
完整示例(跟着做)
一个已解决的 promise 带两步链,与一个定时器赛跑:
追踪两个队列。start 和 end 是同步的,先打印:start,然后 end。setTimeout 的回调进任务队列。第一个 .then() 进微任务队列。栈空之后,微任务队列开始清空:then 1 运行并打印,它的完成又把 then 2 安排成新的微任务,而事件循环在碰任何任务之前会先运行它,于是 then 2 也打印了。到这时微任务队列才空,事件循环取出那个任务,打印 timer。顺序是 start、end、then 1、then 2、timer。整条两步 promise 链在零延迟定时器得到机会之前就走完了。
你来试试(填空示例)
预测输出,给每个被推迟的行标上「任务」或「微任务」。把顺序填出来:
答案:顺序是 a、e、c、d、b。同步打印 a 和 e 先出,按源码顺序。setTimeout 的回调(b)是任务;两个 .then() 回调(c、d)是微任务。栈空之后,微任务队列按登记顺序先清空:先 c,后 d。微任务队列空了,事件循环才运行那个任务:b。所以两个 promise 回调都先于定时器,哪怕定时器登记得更早:微任务永远在下一个任务之前清空。
小结 + 下节课预告
等候室有两个。setTimeout 的回调进任务队列;promise 的 .then()/.catch()/.finally() 回调进微任务队列。栈空时,事件循环先把整个微任务队列清空,才运行一个任务:所以 promise 回调先于零延迟定时器,与源码顺序无关。你现在拥有完整的顺序机器:栈,然后所有微任务,然后一个任务,循环往复。最后一块拼图,是即便懂了这些也仍会绊倒人的那个问题:await 到底对它下面那行做了什么?答案是「它把函数剩下的部分变成一个微任务」,这就是第 4 课。
Footnotes
-
HTML Living Standard: Event loops — https://html.spec.whatwg.org/multipage/webappapis.html#event-loops ↩
-
MDN: Using microtasks in JavaScript with queueMicrotask() — https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide ↩ ↩2 ↩3
-
MDN: JavaScript execution model — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Execution_model ↩ ↩2
-
MDN: Promise.prototype.then() — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then ↩