JS事件循环中的宏/微任务为什么要分三六九等?

Unknown Author 独酌
2026-01-19 前端

Event Loop (事件循环)一直是前端新手的“拦路虎”,但它也是进阶的必经之路。

下面我带着三个问题,并且用一个生动的 银行柜台办理业务 的例子,讲讲这背后的逻辑。

  1. JS的事件循环(Event Loop)中,宏任务(Macrotask)和微任务(Microtask)的执行优先级差异的底层原因是什么?
  2. 浏览器环境和Node.js环境的事件循环有哪些核心差异?
  3. 请举例说明微任务队列嵌套执行的场景与潜在风险?

一、 宏任务与微任务:为什么要分三六九等?

首先,你要记住一个铁律:javaScript是单线程的。这意味着它同一时间只能做一件事情,就像银行只有一个办事窗口,且只有一个柜员(主线程)。

银行柜台模型比喻浏览器

  • 主线程(Main Thread):就是那个唯一的银行柜员。
  • 宏任务(Macrotask):普通的排队客户。比如:setTimeout(定时器)、setIntervalsetImmediateMessageChannel、I/O 操作、UI 渲染、 UI事件回调。他们是主要的大块工作。
  • 微任务(Microtask):客户办完业务后,突然想起的紧急小事。比如:Promise.then/.catch/.finallyMutationObserverqueueMicrotask()process.nextTick(这个比较特殊在node.js优先级比其他微任务还要搞)
  • 渲染(Rendering): 柜员去上厕所或喝水(更新页面界面)

任务执行流程

  1. 柜员上班:优先处理当前手头最紧急的代码(同步代码)。
  2. 处理微任务(插队机制):柜员刚给一位客户(当前的宏任务)办完业务,准备喊“下一位”之前,必须线问当前这位客户:“您还有什么补充的紧急小表格要填吗?”
    • 如果客户说:“有”(产生了微任务),柜员会立刻处理这些小表格,绝不会让后面的排队客户(下一个宏任务)先上来。
    • 不管有多少张小表格(微任务队列),柜员都会一口气全办完,直到当前客户彻底满意离开。
  3. 渲染(UI Render):如果此时银行经理觉得需要更新以下大屏幕汇率(浏览器需要渲染),柜员会趁着两个客户交接的间隙去更新。
  4. 下一个宏任务:喊“下一位”,处理队列里的下一个普通客户。

优先级“底层差异”

原因一:减少上下文切换的开销(为了快)

宏任务通常比较“重”,比如它是浏览器内部发起的(如定时器到期、鼠标点击)。而微任务通常是JS引擎内部产生的(如Promise)。
比喻:既然我已经把你的资料打开了(上下文还在),帮你顺手改个签字(微任务)是非常快的。如果让你重新去排队(变成宏任务),我还要重新调取你的档案,这太浪费时间了。

原因二:保证渲染的一致性(为了准)

浏览器通常会在一个宏任务执行结束后,检查是否需要重新渲染页面。假设你用Promise获取数据后要更新DOM

如果微任务不插队:宏任务改了数据 -> 浏览器渲染(旧数据) -> 下一个宏任务更新 DOM -> 浏览器再渲染。你会看到页面闪烁。

微任务插队(现状):宏任务改了数据 -> 微任务立刻更新 DOM -> 浏览器渲染(新数据)。

微任务的设计是为了在渲染之前,把所有的数据状态都更新到最新,避免页面出现不一致的中间状态。

浏览器环境和 Node.js 环境的事件循环有哪些核心差异?

1. 微任务的执行时机

  • 浏览器雷打不动,每执行i一个宏任务,就清空一次微任务队列。
  • Node.js(v11版本之前):Node的宏任务是分阶段(Timer阶段、Poll阶段),它要等一个阶段里所有的宏任务都做完,才去清空微任务队列。
  • Node.js(v11及以后):改了!现在跟浏览器看齐了。

2. Node.js特有的任务类型:

  • process.nextTick:这是超级微任务。它的优先级比Promise.then还要高!不管微任务队列里有什么,nextTick永远插队在最前面。
  • setImmediate:它属于宏任务,但它专门设计用来在 I/O 循环结束后立刻执行。这和setTimeout(..., 0)有微妙的区别(取决于调用环境),但在浏览器里没有这个东西。

请举例说明微任务队列嵌套执行的场景与潜在风险?

微任务饿死主线程,进入无限续杯,还是用上面的比喻。

  • 正常情况:客户 A 办完事,有一个微任务(填张表)。填完表,客户 A 走人,客户 B(下一个宏任务)上来。
  • 嵌套场景: 客户 A 填第一张表时,突然说:“哎呀,我这还有张表(产生新微任务)”。填第二张时,又说:“稍等,我还有一张(又产生新微任务)”……
  • 结果:柜员被客户 A 彻底缠住了!后面的客户 B 永远排不到,银行经理也没法去大屏幕更新汇率(无法渲染)。

代码案例

1
2
3
4
5
6
7
8
9
10
11
12
function killBrowser() {
console.log('开始作死...');

function loop() {
// 创建一个 Promise,resolve 后立刻再次调用 loop
// 这就是嵌套微任务
Promise.resolve().then(loop);
}

loop();
}
killBrowser()

潜在风险

  1. 页面假死:因为微任务队列永远清不空(一直在自我繁殖),事件循环永远无法进入“渲染”步骤。用户点击按钮没反应,GIF 图不痛,页面彻底冻结。
  2. 宏任务无法执行:如果你设定了一个 setTimeout 准备 1秒后救命,对不起,因为微任务队列没空,这个定时器永远拿不到执行权。
标签:
独酌

独酌

小镇码农

相关文章