JavaScript的秘密生活:Promise(Microtasks)

发布: (2026年2月3日 GMT+8 12:17)
4 分钟阅读
原文: Dev.to

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,随后添加了两个独立的框:

  • 宏任务队列 – 标准队列。存放 setTimeoutsetInterval、用户交互等。
  • 微任务队列 – VIP 队列。存放 Promise、queueMicrotaskMutationObserver

“引擎有一条严格规则,”她解释道。“当当前任务(比如你的主脚本)结束时,服务员不会直接去取下一个宏任务。首先,他会检查 VIP 队列。”

队列的处理方式

  1. 先运行微任务队列 中的所有微任务。
  2. 只有在此之后 才从宏任务队列中挑选下一个宏任务。

由于这条规则,微任务总是在任何待处理的宏任务之前执行,即使宏任务的计时器被设为 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);

在这种情况下:

  1. 打印 “VIP 1”。
  2. 嵌套的 Promise.resolve().then 再向微任务队列前端添加 另一个 微任务(“VIP 2”)。
  3. 事件循环会持续处理微任务 直到队列为空

如果代码不断地加入新的微任务,微任务队列将永远不会清空,事件循环也永远到达不了宏任务队列。这种情况被称为 饥饿——标准队列(宏任务)被剥夺了执行时间,可能导致浏览器卡死。

最佳实践

  • 使用 Promise(微任务)进行 快速、紧急的更新,这些更新必须在下次渲染或 I/O 任务之前完成。
  • 避免排入无限数量的微任务;否则会导致宏任务被饿死,性能下降。

“这不是时间的问题,”Timothy 总结道。“而是状态的问题。”

“正是如此,”Margaret 回答。“在 JavaScript 中,Promise 是一种庄严的誓言。它的优先级高于普通的超时。”

Back to Blog

相关文章

阅读更多 »