Skip to main content
Event Loop 我补过三次:先记住”微任务先于宏任务”,再把渲染时机串进去,最后看规范才明白为什么每轮宏任务结束都要把微任务清空。异步代码什么时候执行,基本都能从这个模型推出来。

Q1: Event Loop 的历史?为什么要有它?

1

1995年: JS 诞生与单线程宿命

Brendan Eich 用 10 天设计了 JavaScript。JS 被设计为单线程语言,原因是它的核心场景是操作 DOM——如果两个线程同时修改同一个 DOM 节点(一个删、一个改),会产生竞态问题。
2

致命痛点: 同步阻塞

单线程带来一个致命问题:如果一段代码在等网络请求或用户输入,整个页面就卡住了。
3

1996年: Event Loop 机制引入

Netscape 2.0 引入了 setTimeout,让 JS 可以”延迟执行”代码。但底层怎么实现?浏览器需要一个机制来管理这些”稍后执行”的代码——这就是 Event Loop。
Event Loop 解决的核心问题:
Event Loop 让单线程的 JS 可以非阻塞地处理异步任务。

🚫 没有 Event Loop

如果开一家餐厅,又当服务员又当厨师(单线程同步)。客人点完单,你跑去后厨炒菜,这时新来的客人只能干瞪眼等着(阻塞)。

✅ 引入 Event Loop

有了 Event Loop 后,相当于雇了厨师(浏览器的 Web API 多线程)。你(主线程)只负责招待客人,把菜单递给厨房后,立刻去接待下一桌。等厨师把菜做好了,按一下响铃(把回调放入任务队列),你听到铃声再去把菜端给客人。
Event Loop 就像一家只有一位咖啡师的咖啡店(引用自 Jake Archibald)。咖啡师是单线程的——一次只能做一杯咖啡(执行一个任务)。排队等候的顾客是宏任务(setTimeout、I/O 等),必须按顺序一个个来。但如果你是”加急订单”(微任务:Promise.then),咖啡师会在做完当前这杯之后、服务下一位普通顾客之前,优先把你的加急单做完。而且,如果加急单做完又来了新的加急单,咖啡师会继续做完所有加急单,直到加急队列彻底清空,才去叫下一个普通顾客的号。
回答时先给宏观框架(30 秒说出 4 个阶段),然后等对方追问某一步的细节。这展示了你的知识体系和沟通能力。不要一上来就背诵 V8 源码。

Q2: 同步代码的执行机制

同步代码在调用栈(Call Stack)中执行。调用栈是一个后进先出(LIFO)的数据结构,每次调用函数就压入一个栈帧,函数返回就弹出。
调用栈是单线程的——同一时间只能执行一个函数。如果一个函数执行时间过长(如死循环、大量计算),调用栈就一直被占用,后续的同步代码、微任务、宏任务、渲染都无法执行。
  • 这就是为什么 while(true) 会导致页面阻塞。
  • 这也是为什么需要异步——把耗时操作交给浏览器后台线程,JS 主线程继续执行。
调用栈就像超市收银台——只有一个窗口(单线程),每次只能服务一位顾客(执行一个函数)。如果前面的顾客买了整整一车东西(耗时操作),后面的顾客只能干等。Event Loop 的作用就是:把”等快递”这种耗时的事情交给快递员(浏览器后台线程),收银员继续服务下一位顾客。

执行上下文的类型

执行上下文(Execution Context)有三种类型:

作用域链与闭包

作用域链(Scope Chain)是执行上下文创建时生成的变量对象链。当访问一个变量时,引擎会沿着作用域链从内到外查找。 闭包(Closure)的本质是:函数记住了它被创建时的词法环境。当一个内部函数引用了外部函数的变量,即使外部函数执行完毕,这些变量也不会被垃圾回收——因为内部函数还持有对它们的引用。

Q3: 微任务队列的执行机制

调用栈清空后,引擎会彻底清空微任务队列。微任务的优先级高于宏任务——必须把所有微任务执行完,才能进入下一个阶段。
Event Loop 的铁律:必须把当前的微任务队列彻底清空,才能进入下一个阶段(渲染或下一个宏任务)。
  • 如果微任务中又产生了新的微任务,新产生的也会在本轮清空——不会推迟到下一轮。
  • 这意味着如果微任务无限递归产生新微任务,主线程会永远阻塞在这一步,页面彻底无响应。
  • 宏任务不存在这个问题——每次只取一个宏任务执行,执行完就有机会去渲染和响应用户输入。
宏任务就像咖啡店里排队等候的普通顾客(引用自 Jake Archibald)。做完一杯,如果你还想再点一杯,必须乖乖去队尾重新排。微任务则拥有”加急特权”——做完当前这杯,你直接跟咖啡师说”我马上还要一杯”,他必须立刻给你做。只要加急队列里还有人(不管是不是中途刚加进来的),咖啡师就一直服务加急顾客,直到加急队伍彻底空了,才去叫下一个普通顾客的号。如果加急单源源不断(微任务无限递归),咖啡师就永远困在加急队列里,普通顾客永远轮不到。

为什么 W3C 要设计微任务?

在早期的浏览器中,确实只有宏任务。但随着前端复杂度的提升,W3C 需要一种机制来监控 DOM 的变化,于是引入了 MutationObserver。 如果每次 DOM 发生细微改变就同步触发回调,性能会直接崩溃;如果用宏任务(如 setTimeout)来异步触发,回调执行的时机就要被推迟到下一次 Event Loop,也就是浏览器渲染之后了。这会导致极其严重的视觉闪烁(先渲染了旧状态,宏任务再修改 DOM 触发二次渲染)。 为了解决这个两难境地,微任务(Microtask)应运而生。 它的设计初衷是:在当前同步代码执行完毕后、浏览器下一次渲染(Paint)之前,批量、原子化地处理完所有的状态更新。这样就能保证在屏幕刷新前,DOM 已经被彻底修改到了最终形态。这也是为什么 Vue 的 $nextTick 底层首选 Promise.then 来实现异步更新队列的原因。
在 Node.js 环境下,微任务内部也存在严格的阶级差异:process.nextTick 属于 Node.js 自己的底层机制,存放在 nextTickQueue 中;而 Promise.then 属于 V8 引擎的机制,存放在 microtaskQueue 中。在进入每一个阶段之前,Node.js 都会强制优先清空 nextTickQueue,然后再把控制权交给 V8 清空 Promise 队列。因此,不管代码怎么写,process.nextTick 永远快于 Promise.then。

微任务的分类

process.nextTick vs Promise.then(Node.js)

在 Node.js 中,process.nextTick 的优先级高于 Promise.then。原因是 process.nextTick 属于 Node.js 自己的底层机制,存放在 nextTickQueue 中;而 Promise.then 属于 V8 引擎的机制,存放在 microtaskQueue 中。
process.nextTick 会在当前阶段结束、切换到下一阶段前立即执行。如果在 process.nextTick 中不断递归产生新的 process.nextTick,会饥饿整个 I/O 轮询——Node.js 永远无法进入下一个阶段,I/O 回调永远无法执行。

Q4: 浏览器渲染阶段

微任务清空后,浏览器按帧决定是否渲染。渲染由浏览器调度,通常对齐显示器刷新率(60Hz 下约 16.67ms 一帧),并不是每过 16.67ms 就必然渲染一次。
渲染阶段的执行顺序:
JS 执行和页面渲染共用同一个主线程。JS 执行时,渲染必须等待。如果一段 JS 代码执行了 3 秒,这 3 秒内浏览器没有机会执行渲染——页面白屏,无法交互。
Layout 就像施工队量房间尺寸、算家具位置(几何计算)。Paint 就像按图纸刷墙、铺地板(绘制指令)。Composite 就像最后的软装——把家具摆好、挂上窗帘(GPU 合成)。如果只是换个窗帘颜色(opacity 变化),不需要重新量房间(重排),直接换就行。

重排(Reflow)的触发条件

重排需要重新计算节点的几何信息,代价最高。触发条件:
  • DOM 增删(插入/删除节点)
  • 元素尺寸变化(width、height、padding、margin、border)
  • 内容变化(文本增减、图片尺寸变化)
  • 浏览器窗口 resize
  • 读取布局属性(offsetWidth、offsetHeight、getComputedStyle、getBoundingClientRect)

重绘(Repaint)的触发条件

重绘只需要重新生成绘制指令,不影响布局,代价较低。触发条件:
  • 颜色变化(color、background-color、border-color)
  • 阴影变化(box-shadow、text-shadow)
  • 可见性变化(visibility、outline)
  • 文本装饰变化(text-decoration)

Composite 阶段的优化

合成阶段由 GPU 执行,只需要矩阵变换,不需要重排重绘。触发合成的操作:
  • transform(位移、缩放、旋转)
  • opacity(透明度)
  • will-change(提前告知浏览器哪些属性会变化)

Q5: 宏任务(task)的执行机制

渲染完成后(或跳过渲染),从任务队列中取出一个 task 执行。每次只取一个——执行完后回到步骤 1,开始下一轮循环。 HTML 规范里只有 task(任务) 这个概念,setTimeout、I/O、用户交互的回调分别属于不同的 task source;“宏任务”(macrotask)是社区俗称,规范中并不存在这个术语,用它只是为了和 microtask 对照。
宏任务和微任务的分类:
宏任务就像快递站点每天派送一批包裹,每次只派一个,派完回站点再拿下一个。微任务就像快递员手里的”加急件”——每当他送完一个普通包裹(宏任务),必须先把手里所有加急件全部送完,才能去拿下一个普通包裹。如果加急件源源不断(微任务无限递归),快递员就永远在送加急件,普通包裹永远送不到。
微任务在当前宏任务执行完后立即清空,宏任务每次只取一个。这意味着:
  • 微任务适合:需要在当前操作完成后、渲染前批量处理的逻辑(如 Vue 的 $nextTick)
  • 宏任务适合:可以推迟到下一轮循环的逻辑(如 setTimeout 做时间切片)

宏任务的分类

requestAnimationFrame 的执行时机

requestAnimationFrame(rAF)严格说不是宏任务也不是微任务——它在浏览器渲染前执行。
setTimeout(fn, 16) 的问题:
  1. 最小延迟 4ms 的限制
  2. 回调执行时间和渲染时机不同步,可能在帧中间执行导致掉帧
  3. 后台标签页中 setTimeout 继续执行(浪费资源),rAF 自动暂停(省电)

Q6: 经典面试题:输出顺序是什么?

逐步分析(带代码注释):
解题口诀(逐行注释版):
做输出顺序题就像分析一场话剧的彩排流程。同步代码是演员上台表演(立刻执行)。setTimeout 是”中场休息后再演的节目”(放入宏任务队列排队)。Promise.then 是”导演临时加的戏”(微任务,必须在休息前全部演完)。而 new Promise(executor) 里的代码是”开场白”——它是同步的,跟演员表演一样立刻执行,不是加戏。
new Promise(executor) 里的代码是同步执行的,只有 .then 回调才是微任务。很多人把 console.log('5') 当成微任务,这是最常见的错误。

进阶题:async/await 与 Event Loop 混战

这是各大厂出场率最高的一道终极输出题:
输出: script start → async2 end → Promise → script end → async1 end → promise1 → promise2 → setTimeout 同步阶段先输出 script start、async2 end、Promise、script end;随后微任务队列里 async1 end 先入队,再是 promise1,最后才是宏任务 setTimeout。现代 V8 对 await 做了优化,async1 end 会先于 promise1 执行,不要按旧引擎的结论背成相反顺序。

💡 破题心法: async/await 的本质

面试遇到 async/await 千万别慌,只要牢记这个核心转换公式:

async/await 的本质

async/await 是 Promise.then 的语法糖。核心转换公式:
记住两点:
  1. await 右边的函数立刻同步执行
  2. await 下面的代码被打包进隐形的 Promise.then(),变成微任务

输出顺序的解题框架

遇到输出顺序题,按以下步骤分析:

Q7: Node.js 的 Event Loop 和浏览器有什么区别?

Node.js 的 Event Loop 有 6 个阶段,按顺序循环执行:
浏览器的 Event Loop 像一条简单的环线公交——同步代码、微任务、渲染、宏任务,四个站循环跑。Node.js 的 Event Loop 则像一座大城市的地铁网络——有 6 条线路(timers、poll、check 等),每条线路负责不同类型的乘客(回调)。而且 Node.js 还有个”超级 VIP 通道”(process.nextTick),比普通 VIP(Promise.then)优先级更高,可以无视线路规则直接插队。
关键区别:
process.nextTick 属于 Node.js 自己的底层机制,存放在 nextTickQueue 中;而 Promise.then 属于 V8 引擎的机制,存放在 microtaskQueue 中。在进入每一个阶段之前,Node.js 都会强制优先清空 nextTickQueue,然后再把控制权交给 V8 清空 Promise 队列。

6 个阶段的详细说明

setImmediate vs setTimeout(0)

在 Node.js 中,setTimeout(0) 会被转化为 setTimeout(1)。这意味着:
  • 如果 Node.js 启动耗时 > 1ms,进入 event loop 时 timer 已到期,在 timers 阶段执行
  • 如果 Node.js 启动耗时 < 1ms,进入 event loop 时 timer 未到期,跳过 timers 阶段
这就是为什么 setTimeout(0) 和 setImmediate 的顺序不确定。

Q8: 为什么 setTimeout(fn, 0) 不是立即执行?

为什么不是 0ms?有两个维度的原因:
  1. 规范限制:HTML5 标准明确规定,当 setTimeout 的嵌套层级超过 5 层时,浏览器必须强制加上最小 4ms 的延迟。在 Chromium 源码中,这个常量被定义为 kMaxTimerNestingLevel = 5 和 kMinimumInterval = 4ms。
  2. Event Loop 机制:即使没有嵌套,setTimeout 也只是把回调放入宏任务队列。如果当前调用栈很深,或者微任务队列里有海量任务,setTimeout 必须排队等待。所以 0ms 只是”最快在 0ms 后进入队列”,而不是”0ms 后执行”。
setTimeout(fn, 0) 就像你去电影院买票,售票员说”你去 3 号厅等”(放入宏任务队列)。但你现在还在 1 号厅看电影(当前宏任务还没执行完),看完电影后还要去洗手间(清空微任务),然后才能去 3 号厅。所以”0ms 后去 3 号厅”不等于”0ms 后就到了 3 号厅”。

4ms 最小延迟的规范

HTML5 标准明确规定:当 setTimeout 的嵌套层级超过 5 层时,浏览器必须强制加上最小 4ms 的延迟。 在 Chromium 源码中,这个常量被定义为:
  • kMaxTimerNestingLevel = 5
  • kMinimumInterval = 4ms

嵌套层级超过 5 层的限制

为什么规范要设置这个限制?原因是防止恶意代码通过无限嵌套 setTimeout(fn, 0) 来占用 CPU 资源。4ms 的延迟虽然很小,但足以让浏览器有机会处理其他任务(如渲染、用户输入)。

底层源码:V8 与 Chromium 是如何实现的?

V8 的 v8::internal::MicrotaskQueue 用环形缓冲区(ring buffer)存放待执行的微任务,统一入口是 MicrotaskQueue::RunMicrotasks(),Isolate::RunMicrotasks 只是转发(详见 V8 源码:src/execution/microtask-queue.cc):
详见 Blink 源码:third_party/blink/renderer/platform/scheduler/:
Node.js 的 Event Loop 实现在 deps/uv/src/unix/core.c 的 uv_run 函数中:
uv__io_poll 是核心——它调用 epoll_wait (Linux) / kqueue (macOS) 阻塞等待 I/O 事件。如果有到期的 timer,timeout 设为最近的 timer 时间;否则无限等待。这就是为什么 setTimeout(fn, 0) 不是立即执行——它的回调会进入下一轮的 timers 阶段执行。

及格回答 vs 满分回答

问题:“说说 Event Loop”

及格回答(60-70分):
“JS 是单线程的,Event Loop 让异步代码不阻塞。执行完同步代码后清空微任务队列,然后取一个宏任务执行。微任务优先级高于宏任务。”
问题出在:知道规则但不理解设计。 满分回答(90-100分):
Event Loop 解决的核心问题是单线程非阻塞。JS 引擎本身不处理 I/O 等待,而是交由浏览器多线程(网络、定时器等)在后台处理,结果就绪后再通过任务队列通知 JS 线程执行。这是一个无限循环:执行同步代码 → 清空微任务队列 → 判断是否渲染 → 取一个宏任务执行。Lin Clark 在 Chrome Developers 的《Inside look at modern web browser》四部曲中,用漫画风格的架构图详细阐述了浏览器的多进程架构:Chrome 为每个标签页分配独立的渲染进程,渲染进程中的主线程负责 JS 执行、样式计算、布局和绘制,而网络请求、定时器等则由浏览器进程的后台线程处理——这正是 Event Loop 得以非阻塞运行的底层基础。[1]
浏览器端由 Blink 的 MessagePumpDefault 驱动,管理 5 种优先级队列(Input > Compositor > Default > Timer > Idle)。同步代码执行后,引擎优先清空微任务队列(V8 由 MicrotaskQueue::RunMicrotasks() 用环形缓冲区实现),然后按帧判断是否渲染,最后再取一个 task。Jake Archibald 在其 2015 年的神作《Tasks, microtasks, queues and schedules》中,用交互式 Demo 深度证明了一个关键洞察:微任务的优先级甚至高于渲染——浏览器必须在执行任何渲染步骤之前,彻底清空微任务队列(包括在清空过程中新产生的微任务)。他在文中用咖啡店点单的类比解释:宏任务就像按号排队的顾客,微任务则拥有”插队特权”,而渲染只有在所有插队者服务完毕后才有机会执行。李兵在极客时间《浏览器工作原理与实践》中,从 Chrome 多进程架构的角度系统性地讲解了每一帧的时间分配——常说的”5ms 给 JS、11ms 给渲染”只是经验值、并非规范硬规则,长任务会推迟渲染但不会超阈值机械跳过整帧,是国内理解浏览器帧调度机制最权威的参考资料。[2]
Node.js 端由 libuv 驱动,具有 6 个阶段(timers → pending → idle → poll → check → close)。其最大特色是 process.nextTick 拥有比 Promise.then 更高的优先级——它会在当前阶段结束、切换到下一阶段前立即被执行,滥用极易饥饿整个 I/O 轮询。Node.js 官方文档《Event Loop, Timers, and nextTick》明确指出:process.nextTick 属于 Node.js 自己的底层机制,存放在 nextTickQueue 中;而 Promise.then 属于 V8 引擎的机制,存放在 microtaskQueue 中。在进入每一个阶段之前,Node.js 都会强制优先清空 nextTickQueue,然后再把控制权交给 V8 清空 Promise 队列。[3]
实际项目中的建议:把长任务拆成小块(用 setTimeout 或 requestAnimationFrame 做时间切片),避免阻塞渲染。Addy Osmani 在 Chrome Developers 博客的《The Cost of JavaScript》系列中指出,长任务(Long Task)是导致页面交互延迟(INP)的首要元凶,推荐使用 requestIdleCallback 在浏览器空闲时段执行非关键工作,或使用 scheduler.yield()(新 API)主动让出主线程。在 Node.js 端,避免在 process.nextTick 中做耗时操作,用 setImmediate 替代。冴羽在掘金的 JavaScript 深入系列中,通过大量输出顺序题的逐步推演,将 Event Loop 同步代码 → 微任务 → 宏任务的执行机制讲得极其透彻,是国内社区最实用的 Event Loop 实战参考。[4]

常见追问

答题要点: 不能只答”宏任务先执行,微任务后执行”,必须加上**渲染(Render)**的节点。高分回答: “在每一轮 Event Loop 中,主线程首先取出一个宏任务执行。该宏任务执行完毕后,会立即锁定并清空当前所有的微任务队列(包括在清空过程中新产生的微任务)。微任务队列彻底清空后,浏览器会按帧判断是否需要进行 UI 渲染(60Hz 下通常约 16.67ms 一帧)。渲染完成后,再去任务队列取下一个 task。 所以时机是:宏任务 → 全部微任务 → UI 渲染 → 下一个宏任务。”
答题要点: 结合 HTML5 规范与系统底层解释。高分回答: “主要有两个维度的原因:
  1. 规范限制:HTML5 标准明确规定,当 setTimeout 的嵌套层级超过 5 层时,浏览器必须强制加上最小 4ms 的延迟。在 Chromium 源码中,这个常量被定义为 kMaxTimerNestingLevel = 5 和 kMinimumInterval = 4ms。
  2. Event Loop 机制:即使没有嵌套,setTimeout 也只是把回调放入宏任务队列。如果当前执行上下文栈很深,或者微任务队列里有海量任务,setTimeout 必须排队等待。所以 0ms 只是’最快在 0ms 后进入队列’,而不是’0ms 后执行’。”
答题要点: 必须分场景讨论!直接给确定答案的都是错的。高分回答: “分两种情况: 1. 在全局作用域(Main Module)下执行:顺序不确定。 因为 Node.js 的启动耗时可能大于或小于 1ms(setTimeout(0) 在底层会被转化为 setTimeout(1))。如果进入 Event Loop 时已经过了 1ms,Timer 阶段就会执行它;如果没到 1ms,就会跳过 Timer 阶段,在 Check 阶段先执行 setImmediate。2. 在 I/O 回调(如 fs.readFile)中执行:setImmediate 绝对先执行。
因为 I/O 回调在 poll 阶段执行,poll 阶段结束后,紧接着就是 check 阶段(执行 setImmediate)。而 setTimeout 必须等到下一轮循环的 timer 阶段。”
答题要点: 对比宏任务递归,点出”阻塞渲染”的致命性。高分回答: “会导致主线程彻底阻塞,页面无法点击,甚至连 UI 都无法重绘。 因为 Event Loop 规定:必须把当前的微任务队列彻底清空,才能进入下一个阶段(渲染或下一个宏任务)。如果是无限递归产生微任务,这个队列就永远清不空,主线程就永远困在这一步。对比宏任务:
如果用 setTimeout 无限递归产生宏任务,页面不会阻塞!因为每次执行完一个宏任务,主线程就有机会去执行渲染和响应用户输入。这也是为什么很多计算密集型任务(如大数组遍历)可以利用 setTimeout 或 requestAnimationFrame 进行’时间切片(Time Slicing)‘来避免阻塞。”
答题要点: 闭包让回调函数能访问外部变量,这是 Event Loop 能正常工作的基础。高分回答: “闭包(Closure)和 Event Loop 的关系是:闭包让异步回调函数能访问到正确的变量。
如果没有闭包,异步回调执行时,外部函数的变量已经被垃圾回收,回调就拿不到数据了。所以闭包是 Event Loop 异步编程的基础——它保证了回调函数在’稍后执行’时,仍能访问到正确的上下文。”
答题要点: async/await 是 Promise.then 的语法糖,await 下方代码变成微任务。高分回答: “async/await 是 Promise.then 的语法糖,执行顺序完全一致:
关键点:
  1. await 右边的函数立刻同步执行(输出 2)
  2. await 下面的代码被打包进隐形的 Promise.then()(输出 3 是微任务)
  3. 所以 await 下面的代码一定在当前同步代码之后执行(输出 4 在输出 3 之前)
如果有多个 await,每个 await 下面的代码都是独立的微任务,按顺序执行。”
答题要点: rAF 既不是宏任务也不是微任务,它在微任务之后、渲染之前执行。高分回答: “requestAnimationFrame 严格说既不是宏任务也不是微任务——它是一个独立的阶段,在微任务之后、浏览器渲染之前执行。执行顺序:同步代码 → 微任务 → rAF → 浏览器渲染 → 宏任务从 Blink 源码看,requestAnimationFrame 注册的回调在 BeginMainFrame 的 Animate 步骤中执行。这意味着:
  1. rAF 回调一定在当前帧的渲染前执行
  2. rAF 回调中的 DOM 修改会被当前帧渲染
  3. 如果在 rAF 中修改 transform/opacity,只需要 Composite,不需要 Layout 和 Paint
答题要点: 用 setTimeout 或 requestAnimationFrame 把长任务拆成小块,每块执行完让出主线程。高分回答: “时间切片的核心思想是:把一个长任务拆成多个小任务,每个小任务执行完后让出主线程,让浏览器有机会渲染和响应用户输入。
更优雅的方案:用 requestIdleCallback
requestIdleCallback 会在浏览器空闲时执行,不会阻塞渲染。但兼容性不如 setTimeout。“

延伸阅读与引用参考

  • 🔗 Inside look at modern web browser (Part 1-4):Lin Clark 在 Chrome Developers 博客上连载的浏览器内部机制四部曲,用漫画风格的架构图详细阐述了多进程架构、渲染进程的主线程分工以及帧调度机制。[1]
  • 📖 Tasks, microtasks, queues and schedules:Jake Archibald(Chrome 开发者布道师)2015 年的神作,用交互式 Demo 证明了微任务优先级高于渲染的底层特性——浏览器必须在执行任何渲染步骤之前彻底清空微任务队列,被社区奉为 Event Loop 圣经。[2]
  • 🟢 Node.js Event Loop, Timers, and nextTick:Node.js 官方文档对 libuv 底层机制的最权威解释,详细说明了 6 个阶段和 process.nextTick 与 setImmediate 的区别。[3]
  • 🧑‍💻 冴羽 - JavaScript 深入系列:国内 JS 深入系列经典文章作者,通过大量输出顺序题的逐步推演,将同步代码 → 微任务 → 宏任务的执行机制讲得极其透彻,是国内社区最实用的 Event Loop 实战参考。[4]
  • 📘 李兵 - 浏览器工作原理与实践:极客时间专栏,从 Chrome 多进程架构的角度系统性地讲解了每一帧的时间分配(常说的 JS 5ms + 渲染 11ms 只是经验值,并非规范硬规则)、渲染流水线与 Event Loop 的协作原理,是国内理解浏览器底层最权威的参考资料。
  • 🎬 What the heck is the event loop anyway?:Philip Roberts 在 JSConf EU 2014 的经典演讲,用自建的 Loupe 可视化工具(latentflip.com/loupe)讲解 Call Stack、Web API、Task Queue 三者的协作原理,YouTube 播放量超百万。
  • 📖 The Cost of JavaScript:Addy Osmani 在 V8 官方博客上发布的 JavaScript 性能成本分析,指出长任务(Long Task)是导致页面交互延迟的首要元凶,推荐使用 requestIdleCallback 和 scheduler.yield() 做时间切片。
  • 📘 libuv Design Overview:libuv 官方设计文档,从底层 I/O 模型(epoll/kqueue)的角度解释了 Event Loop 的 6 阶段实现原理。