理解引擎实际上是如何调度同步代码、微任务和宏任务的。
在 Kodokon 中打开本课JavaScript 在单线程上运行你的代码,但事件循环并不是由 ECMAScript 定义的:描述它工作方式的是 HTML 规范(在 Node.js 一侧则是 libuv)。引擎为当前正在运行的代码维护一个调用栈、一个宏任务队列(定时器、I/O、事件)和一个微任务队列(Promise.prototype.then、queueMicrotask、MutationObserver)。黄金法则:每执行完一个宏任务后,微任务队列会被完全清空,然后才把控制权交还出去。
console.log("script start");
setTimeout(() => console.log("timeout"), 0);
Promise.resolve()
.then(() => console.log("promise 1"))
.then(() => console.log("promise 2"));
console.log("script end");
// script start, script end,
// promise 1, promise 2, timeout初始脚本本身就是一个宏任务:在它执行完之前,其他任何东西都不会运行。一旦调用栈清空,引擎就会清空微任务队列,包括清空过程中新加入的微任务。这个细节解释了为什么两个链式的 then 调用会先于 setTimeout(fn, 0) 运行:每个 then 都会调度另一个微任务,而在队列清空之前,控制权永远不会被交还出去。
setTimeout(() => console.log("macro 1"), 0);
setTimeout(() => console.log("macro 2"), 0);
queueMicrotask(() => {
console.log("micro 1");
queueMicrotask(() => console.log("micro 2"));
});
// micro 1, micro 2, macro 1, macro 2await 关键字建立在这套机制之上:它会挂起 async 函数,把控制权交还给调用方,然后把恢复执行调度为一个微任务。即使是 await null 也会触发这种挂起,因为该值会被包裹进一个已经 resolve 的 promise 中。这就是为什么放在 await 之后的代码永远不会同步运行。
async function main() {
console.log("before await");
await null;
console.log("after await");
}
main();
console.log("sync code");
// before await, sync code, after awaitC,通过 setTimeout(fn, 0) 调度 A,并通过 Promise.resolve().then 调度 B。这些字母以什么顺序出现?await 在底层实际上做了什么?