JavaScript的秘密生活:Promise(Microtasks)
Source: Dev.to
理解 VIP 队列:微任务 vs. 宏任务
Timothy 坐在小图书馆的桌子旁,翻动着两张纸。Margaret 走近时,他抬起头,眉头紧锁。
“我不明白,”他低声说。“我让代码等待零毫秒。零。那应该是瞬间的,对吧?”
他把一段代码片段滑到桌子上。
console.log("1. Start");
setTimeout(() => {
console.log("2. Timeout");
}, 0);
Promise.resolve().then(() => {
console.log("3. Promise");
});
console.log("4. End");
“我本来以为它会是 1、2、3、4,”Timothy 解释道。“或者是 1、4、2、3。但看看实际发生了什么。”
1. Start
4. End
3. Promise
2. Timeout
“你还记得我们在第 6 卷里讨论过事件循环吗?我们说过服务员会检查队列。”
“对,”Timothy 点头。
“好吧,”Margaret 低声说,“我没有把全部故事告诉你。并不是只有一个队列。其实有两个。”
她在黑板上画了一个大圆并标注为 The Event Loop,随后添加了两个独立的框:
- 宏任务队列 – 标准队列。存放
setTimeout、setInterval、用户交互等。 - 微任务队列 – VIP 队列。存放 Promise、
queueMicrotask和MutationObserver。
“引擎有一条严格规则,”她解释道。“当当前任务(比如你的主脚本)结束时,服务员不会直接去取下一个宏任务。首先,他会检查 VIP 队列。”
队列的处理方式
- 先运行微任务队列 中的所有微任务。
- 只有在此之后 才从宏任务队列中挑选下一个宏任务。
由于这条规则,微任务总是在任何待处理的宏任务之前执行,即使宏任务的计时器被设为 0 ms。
执行顺序解释
console.log("1. Start")和console.log("4. End")立即在调用栈上运行(当前任务)。setTimeout(..., 0)被放入 宏任务 队列。Promise.resolve().then(...)被放入 微任务 队列。
当主脚本结束后,事件循环首先处理微任务队列,产生 “3. Promise”,随后再处理宏任务队列,产生 “2. Timeout”。
嵌套微任务与饥饿问题
Margaret 用一个嵌套示例说明了潜在的陷阱:
Promise.resolve().then(() => {
console.log("VIP 1");
Promise.resolve().then(() => {
console.log("VIP 2 (Nested)");
// If we keep adding Promises here...
});
});
setTimeout(() => console.log("Standard Line"), 0);
在这种情况下:
- 打印 “VIP 1”。
- 嵌套的
Promise.resolve().then再向微任务队列前端添加 另一个 微任务(“VIP 2”)。 - 事件循环会持续处理微任务 直到队列为空。
如果代码不断地加入新的微任务,微任务队列将永远不会清空,事件循环也永远到达不了宏任务队列。这种情况被称为 饥饿——标准队列(宏任务)被剥夺了执行时间,可能导致浏览器卡死。
最佳实践
- 使用 Promise(微任务)进行 快速、紧急的更新,这些更新必须在下次渲染或 I/O 任务之前完成。
- 避免排入无限数量的微任务;否则会导致宏任务被饿死,性能下降。
“这不是时间的问题,”Timothy 总结道。“而是状态的问题。”
“正是如此,”Margaret 回答。“在 JavaScript 中,Promise 是一种庄严的誓言。它的优先级高于普通的超时。”