前置知识: HTML5、CSS

事件循环

14 min高级

JavaScript 事件循环模型——HTML 规范、Node.js 实现、微任务/宏任务、渲染调度

阅读建议:核心必读。形式化模型可先跳过,重点看执行顺序示例与对比表;反复读直到能口算执行顺序。

事件循环(Event Loop)

前置知识

学习目标

  • 掌握「0. 导言」的核心机制、典型用法与常见陷阱
  • 掌握「1. 历史动机与技术演进」的核心机制、典型用法与常见陷阱
  • 掌握「2. 形式化定义」的核心机制、典型用法与常见陷阱
  • 掌握「3. 浏览器事件循环(HTML 规范)」的核心机制、典型用法与常见陷阱
  • 掌握「4. Node.js 事件循环」的核心机制、典型用法与常见陷阱

0. 导言

事件循环(Event Loop)是 JavaScript 异步编程的核心机制。它协调任务调度、I/O 处理、UI 渲染与用户交互,使单线程的 JavaScript 能够处理高并发场景而不阻塞主线程。

理解事件循环是掌握现代 JavaScript 的关键:

  • Promise、async/await、Generator 的语义都建立在微任务之上
  • 浏览器与 Node.js 的事件循环模型存在显著差异
  • 性能优化(避免长任务、合理调度渲染)依赖对事件循环的精确理解
  • 调试竞态条件、内存泄漏、死锁等问题需要还原事件循环执行轨迹

核心命题:JavaScript 是单线程语言,但通过事件循环可以实现非阻塞 I/O 与高并发。这一设计源自 Brendan Eich 在 1995 年的选择——为了保证 DOM 操作的安全性,避免多线程带来的竞态问题。


1. 历史动机与技术演进

1.1 单线程设计的起源(1995)

Brendan Eich 在设计 JavaScript 时选择了单线程模型,主要动机包括:

动机解释
DOM 安全多线程同时修改 DOM 会导致竞争条件,单线程避免锁
简化学习曲线浏览器脚本作者多为非专业开发者,单线程更易理解
历史背景当时 GUI 编程主流是事件驱动单线程(Mac Toolbox、Windows)
性能限制1995 年的 CPU 难以承担浏览器内的线程调度开销

1.2 事件循环的演化时间线

时间事件影响
1983Smalltalk-80 引入事件循环现代 GUI 事件循环鼻祖
1984Mac OS Toolbox Event ManagerGetNextEvent() 模型影响后续设计
1995-05Brendan Eich 实现 JavaScript借鉴 Netscape 已有的事件循环
1995-12Netscape Navigator 2.0setTimeout 作为首个异步 API
2006jQuery 推广 Promise 模式异步编程成为主流
2009Ryan Dahl 发布 Node.jslibuv 事件循环模型
2011ES6 Promise 提案进入 Stage 1微任务概念成形
2015ES2015 标准化 Promise正式引入 Job Queue
2017Node.js 11 改变微任务调度nextTick 与 Promise 顺序调整
2018requestIdleCallback 跨浏览器可用空闲调度标准化
2024Scheduler API 进入 Stage 3优先级调度即将标准化

1.3 关键人物与论文

  • Brendan Eich:JavaScript 创始人,1995 年在 Netscape 用 10 天实现原型。
  • Ryan Dahl:Node.js 创始人,2009 年基于 libuv 设计 Node.js 事件循环。
  • Jake Archibald:Google Chrome 团队,2014 年 JSConf.Asia 演讲《In The Loop》成为事件循环经典讲解。
  • Anne van Kesteren:WHATWG 编辑,主导 HTML 事件循环规范化。

学术溯源:事件循环的并发模型在学术上属于”协作式多任务”(Cooperative Multitasking)与”反应器模式”(Reactor Pattern)的结合。相关经典论文:

  • Schmidt, D. C. (1995). Reactor: An Object Behavioral Pattern for Concurrent Event Demultiplexing and Event Handler Dispatching. Proceedings of the 2nd Conference on Pattern Languages of Programs (PLoP ‘95).
  • Pai, V. P., et al. (2007). A Case for Event-Driven Servers in High-Performance Networking. USENIX Annual Technical Conference.

2. 形式化定义

2.1 事件循环的数学模型

设 T\mathcal{T} 为任务集合,M\mathcal{M} 为微任务集合,R\mathcal{R} 为渲染步骤。事件循环可形式化为:

EventLoop=while true{1.task←selectTask(T)2.execute(task)3.while M≠∅:μ←dequeue(M)execute(μ)4.if renderingOpportunity():render(R)\text{EventLoop} = \text{while true} \begin{cases} 1. \quad \text{task} \leftarrow \text{selectTask}(\mathcal{T}) \\ 2. \quad \text{execute}(\text{task}) \\ 3. \quad \textbf{while } \mathcal{M} \neq \emptyset: \\ \quad\quad \mu \leftarrow \text{dequeue}(\mathcal{M}) \\ \quad\quad \text{execute}(\mu) \\ 4. \quad \textbf{if } \text{renderingOpportunity}(): \\ \quad\quad \text{render}(\mathcal{R}) \\ \end{cases}

其中:

  • selectTask(T)\text{selectTask}(\mathcal{T}) 从任务队列按优先级选择任务(HTML 规范定义了选择算法)。
  • 微任务队列在每轮宏任务后完全清空(drain),包括执行过程中新加入的微任务。
  • renderingOpportunity()\text{renderingOpportunity}() 取决于屏幕刷新率、页面可见性、display:none 等因素。

2.2 ECMAScript Job 与 HTML Task 的关系

ECMAScript 规范定义了 Job 的抽象概念(不规定具体实现),HTML 规范将 Job 实现为 Microtask:

JobECMA≡MicrotaskHTML\text{Job}_{\text{ECMA}} \equiv \text{Microtask}_{\text{HTML}}

ECMAScript 中的 Job 主要包括:

  • NewPromiseReactionJob:Promise 状态变化时的反应任务
  • NewPromiseResolveThenableJob:解析 thenable 时的任务

2.3 任务源(Task Source)

HTML 规范定义任务按”任务源”分类,不同任务源进入不同的任务队列:

T={QDOM manipulation,Quser interaction,Qnetworking,Qnavigation,Qfile timer,…}\mathcal{T} = \{ Q_{\text{DOM manipulation}}, Q_{\text{user interaction}}, Q_{\text{networking}}, Q_{\text{navigation}}, Q_{\text{file timer}}, \ldots \}

事件循环每轮从多个任务队列中选择一个任务(按规范定义的选择算法)。

2.4 Node.js 事件循环的阶段模型

Node.js 基于 libuv,事件循环分为 6 个阶段:

NodeLoop=while true{timers:执行 setTimeout/setInterval 到期回调pending callbacks:执行延迟到下一轮的 I/O 回调idle, prepare:内部使用poll:检索新 I/O 事件,执行 I/O 回调check:执行 setImmediate 回调close callbacks:执行 close 事件回调\text{NodeLoop} = \text{while true} \begin{cases} \text{timers}: \text{执行 setTimeout/setInterval 到期回调} \\ \text{pending callbacks}: \text{执行延迟到下一轮的 I/O 回调} \\ \text{idle, prepare}: \text{内部使用} \\ \text{poll}: \text{检索新 I/O 事件,执行 I/O 回调} \\ \text{check}: \text{执行 setImmediate 回调} \\ \text{close callbacks}: \text{执行 close 事件回调} \\ \end{cases}

每个阶段之间会清空 process.nextTick 队列与微任务队列。

2.5 单线程的形式化证明

JavaScript 单线程意味着:

∀t∈Time:∣ActiveTask(t)∣≤1\forall t \in \text{Time}: |\text{ActiveTask}(t)| \leq 1

即任意时刻至多一个任务在执行。这一约束简化了并发模型,但要求所有 I/O 必须异步:

SyncIO⇒Block(EventLoop)⇒Starve(UI)\text{SyncIO} \Rightarrow \text{Block}(\text{EventLoop}) \Rightarrow \text{Starve}(\text{UI})

3. 浏览器事件循环(HTML 规范)

3.1 处理模型(Processing Model)

HTML Living Standard 第 8.1.7 节定义了事件循环的完整处理模型。简化版步骤如下:

  1. 选择任务:从任务队列中选择最旧的可运行任务(按任务源优先级)。若无任务,跳到渲染步骤。
  2. 执行任务:将任务设为”当前运行中”,执行其回调。
  3. 清空微任务:循环取出微任务队列中的任务并执行,直到队列为空。
  4. 更新渲染:检查是否有渲染机会(rendering opportunity),若有则执行渲染步骤:
    • 遍历 requestAnimationFrame 回调队列
    • 执行布局(Layout/Reflow)
    • 执行绘制(Paint)
    • 合成(Composite)
  5. 清空微任务:rAF 回调中产生的微任务也需要清空。
  6. 执行 requestIdleCallback:如果还有空闲时间,执行 rIC 队列中的回调。
  7. 回到步骤 1。
// 伪代码表示事件循环
while (true) {
  // 1. 选择并执行一个任务
  const task = taskQueue.shift();
  if (task) task.run();

  // 2. 清空微任务
  while (microtaskQueue.length > 0) {
    microtaskQueue.shift().run();
  }

  // 3. 检查渲染机会
  if (hasRenderingOpportunity()) {
    // 4. 执行 rAF 回调
    while (rafCallbacks.length > 0) {
      rafCallbacks.shift()(performance.now());
    }
    // 5. 渲染:布局 + 绘制 + 合成
    render();

    // 6. rAF 回调产生的微任务
    while (microtaskQueue.length > 0) {
      microtaskQueue.shift().run();
    }

    // 7. 空闲回调(如果还有时间)
    if (hasIdleTime()) {
      while (ricCallbacks.length > 0 && hasIdleTime()) {
        ricCallbacks.shift()({ didTimeout: false, timeRemaining: () => 5 });
      }
    }
  }
}

3.2 任务队列(Task Queue)

HTML 规范定义多个任务源(task source),每个任务源对应一个或多个任务队列:

任务源示例优先级
DOM manipulationMutationObserver 回调、Promise.then(已废弃,移至微任务)中
User interactionclick、keydown、scroll高
Networkingfetch 完成、XMLHttpRequest 完成中
Navigationlocation 变化、history 导航高
TimersetTimeout、setInterval低
File operationsFileReader 完成中

关键:HTML 规范允许浏览器在多个任务队列间自由选择,但同一队列内必须 FIFO。这意味着不同任务源的相对顺序在不同浏览器中可能不同。

3.3 微任务队列(Microtask Queue)

微任务(Microtask)在当前任务结束后、下一个任务前执行。微任务源包括:

  • Promise.then/catch/finally 回调
  • queueMicrotask(fn) 注册的回调
  • MutationObserver 回调
  • IntersectionObserver 回调(部分实现)
  • await 后续代码(本质是 Promise.then)
// 微任务演示
console.log('1: 同步');

setTimeout(() => console.log('5: 宏任务'), 0);

Promise.resolve().then(() => {
  console.log('3: 微任务');
  // 微任务中再产生微任务,仍在当前清空阶段执行
  Promise.resolve().then(() => console.log('4: 嵌套微任务'));
});

queueMicrotask(() => console.log('2: queueMicrotask'));

// 输出顺序:1 → 3 → 2 → 4 → 5
// 关键:Promise.then 与 queueMicrotask 进入同一条 FIFO 微任务队列——
// then(3) 先注册所以先执行;3 执行中新产生的 4 只能排在已入队的 2 之后,
// 微任务不能插队到已在队列中的任务前面

3.4 微任务的优先级

HTML 规范定义微任务队列是单一 FIFO 队列,没有优先级区分。但 ECMAScript 规范允许实现有多个 Job 队列。在浏览器中:

  • Promise.then 与 queueMicrotask 同优先级
  • MutationObserver 与 Promise 同优先级
  • 所有微任务都在当前任务后清空

在 Node.js 中情况不同(见 5.3 节)。

3.5 零延迟 setTimeout 的真相

console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');

// 输出:start, end, promise, timeout

setTimeout(fn, 0) 实际并非 0ms 执行:

  1. HTML 规范规定嵌套超过 5 层的 setTimeout 最小延迟为 4ms
  2. 浏览器实现可能将最小延迟设为 1ms、4ms 或更高
  3. 后台标签页会被节流到 1000ms
  4. 即使到了时间,也需要等待当前任务与微任务完成
// 测试 setTimeout 实际延迟
function measureTimeout() {
  const start = performance.now();
  setTimeout(() => {
    console.log(`实际延迟:${performance.now() - start}ms`);
  }, 0);
}
measureTimeout();  // 通常输出 1-4ms

// 嵌套超过 5 层后
function nested(depth) {
  if (depth >= 10) return;
  const start = performance.now();
  setTimeout(() => {
    const actual = performance.now() - start;
    console.log(`depth=${depth}, actual=${actual.toFixed(2)}ms`);
    nested(depth + 1);
  }, 0);
}
nested(0);
// 前 5 层约 0.1-1ms,第 6 层起约 4ms+

3.6 async/await 的微任务语义

async/await 是 Promise 的语法糖,await 后的代码等价于 Promise.then:

async function async1() {
  console.log('async1 start');
  await async2();  // 等价于 Promise.resolve(async2()).then(() => { ... })
  console.log('async1 end');  // 微任务
}

async function async2() {
  console.log('async2');
}

console.log('script start');
setTimeout(() => console.log('setTimeout'), 0);
async1();
new Promise((resolve) => {
  console.log('promise');
  resolve();
}).then(() => console.log('promise.then'));
console.log('script end');

// 输出顺序:
// script start
// async1 start
// async2
// promise
// script end
// async1 end
// promise.then
// setTimeout

详细执行轨迹

步骤输出任务/微任务说明
1script start同步主任务开始
2async1 start同步async1 函数体执行
3async2同步await 调用 async2
4promise同步Promise 构造器执行
5script end同步主任务结束
6async1 end微任务await 后续代码
7promise.then微任务then 回调
8setTimeout宏任务下一轮任务

关键:await 后的代码进入微任务队列,与 Promise.then 同优先级,按入队顺序执行。

3.7 await 的微任务入队时机

不同浏览器对 await 的实现存在细微差异:

async function a() {
  console.log('a1');
  await Promise.resolve();
  console.log('a2');
}

async function b() {
  console.log('b1');
  await Promise.resolve();
  console.log('b2');
}

a();
b();

// V8(旧版本,<7.2)输出:a1, b1, a2, b2
// V8(新版本,>=7.2)输出:a1, b1, a2, b2
// 但中间过程不同:旧版需要 4 个微任务,新版优化为 2 个

V8 7.2+ 的优化(Fast Async Functions)减少了 await 产生的微任务数量,从 4 个降到 2 个,显著提升性能。


4. Node.js 事件循环

4.1 libuv 模型

Node.js 使用 libuv 作为事件循环实现,与浏览器事件循环差异显著。libuv 事件循环分为 6 个阶段:

flowchart TD
    Timers[timers(setTimeout)<br/>执行到期的定时器回调] --> Pending[ pending callbacks<br/>执行上一轮延迟的 I/O 回调]
    Pending --> Idle[idle, prepare<br/>内部使用]
    Idle --> Poll[poll<br/>检索新 I/O 事件,执行回调]
    Poll --> Check[check(setImmediate)<br/>执行 setImmediate 回调]
    Check --> Close[close callbacks<br/>执行 close 事件(如 socket.on('close'))]
    Poll --> Timers

4.2 各阶段详解

4.2.1 timers 阶段

执行 setTimeout 与 setInterval 到期的回调。libuv 内部使用最小堆(min-heap)维护定时器,按到期时间排序。

// Node.js 定时器
setTimeout(() => {
  console.log('timer');
}, 100);

注意:定时器的实际执行时间可能晚于设定值,因为 poll 阶段可能阻塞。

4.2.2 pending callbacks 阶段

执行上一轮 poll 中因操作繁忙而被延迟的 I/O 回调,例如:

  • TCP 错误回调(如 ECONNREFUSED)
  • DNS 查询错误
  • 文件系统操作的某些错误回调

4.2.3 idle, prepare 阶段

libuv 内部使用,开发者通常不直接接触。

4.2.4 poll 阶段

事件循环的核心阶段,执行 I/O 回调:

  1. 计算阻塞超时:
    • 如果 timers 队列有任务,超时为最近的定时器到期时间
    • 否则,超时为无限(除非有 setImmediate 待执行,则超时为 0)
  2. 阻塞等待 I/O 事件,直到超时或有事件到达
  3. 执行所有就绪的 I/O 回调

4.2.5 check 阶段

执行 setImmediate 回调。setImmediate 是 Node.js 特有 API,在当前事件循环结束后、下一个事件循环开始前执行。

setImmediate(() => {
  console.log('immediate');
});

4.2.6 close callbacks 阶段

执行关闭事件的回调,例如:

const socket = new net.Socket();
socket.on('close', () => {
  console.log('socket closed');
});
socket.destroy();

4.3 process.nextTick 与微任务

process.nextTick 是 Node.js 特有的微任务,优先级高于 Promise:

setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));

// 输出顺序:nextTick → promise → immediate

nextTickQueue 与 microtaskQueue 的关系

Node.js 维护两个独立队列:

  • nextTickQueue:存储 process.nextTick 回调
  • microtaskQueue:存储 Promise 回调(包括 queueMicrotask)

在每个阶段切换时,Node.js 会清空两个队列,顺序为:

  1. 清空 nextTickQueue
  2. 清空 microtaskQueue
process.nextTick(() => {
  console.log('nextTick 1');
  Promise.resolve().then(() => console.log('promise 1(在 nextTick 中)'));
});

Promise.resolve().then(() => {
  console.log('promise 2');
  process.nextTick(() => console.log('nextTick 2(在 promise 中)'));
});

// Node.js 输出:
// nextTick 1
// promise 1(在 nextTick 中)  ← 注意:这里 promise 进入微任务队列,与 promise 2 一起执行
// promise 2
// nextTick 2(在 promise 中)  ← nextTick 进入队列,下一轮清空

Node.js 11+ 的变化:Node.js 11 起,每个宏任务执行后立即清空微任务队列(与浏览器一致)。之前是每个阶段切换时才清空。这导致一些代码行为变化:

// Node.js 11+ 与浏览器行为一致
setTimeout(() => {
  console.log('timer1');
  Promise.resolve().then(() => console.log('promise1'));
}, 0);

setTimeout(() => {
  console.log('timer2');
  Promise.resolve().then(() => console.log('promise2'));
}, 0);

// Node.js 11+ 输出:timer1, promise1, timer2, promise2
// Node.js <11 输出:timer1, timer2, promise1, promise2

4.4 setImmediate vs setTimeout(0)

setImmediate 与 setTimeout(fn, 0) 的执行顺序取决于调用上下文:

// 在主模块中调用,顺序不确定
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// 输出顺序可能为 timeout, immediate 或 immediate, timeout

// 在 I/O 回调中调用,setImmediate 总是先于 setTimeout
const fs = require('fs');
fs.readFile(__filename, () => {
  setTimeout(() => console.log('timeout'), 0);
  setImmediate(() => console.log('immediate'));
});
// 输出顺序总是:immediate, timeout

原因分析

在 I/O 回调中,事件循环处于 poll 阶段。poll 阶段结束后,下一个阶段是 check(setImmediate),然后才是下一轮的 timers(setTimeout)。因此 setImmediate 总是先于 setTimeout 执行。

在主模块中,事件循环尚未完全启动,定时器的入队时机与事件循环的初始阶段关系不确定。

4.5 浏览器与 Node.js 差异对比

维度浏览器Node.js
实现基础HTML 规范libuv
任务队列数多个(按任务源)6 个阶段
微任务队列单一nextTick + microtask 双队列
微任务清空时机每个任务后每个阶段切换时(Node.js 11+ 同浏览器)
setImmediate部分支持原生支持
process.nextTick不支持原生支持
requestAnimationFrame支持不支持
requestIdleCallback支持不支持
I/O 模型OS 异步 I/Olibuv 线程池 + epoll/kqueue
任务调度任务源优先级阶段顺序固定
渲染有无

4.6 微任务清空时机的演进

Node.js 微任务清空策略经历了多次调整:

Node.js 版本行为
<8每个阶段切换时清空 nextTick 与 microtask
8-10引入 --experimental-modules,行为基本不变
11+每个宏任务后清空 microtask(与浏览器一致),但 nextTick 仍在阶段切换时清空
12+完全对齐浏览器行为,nextTick 也改为每个任务后清空
// 在 Node.js 11+ 中,行为与浏览器一致
async function test() {
  console.log('A');
  await Promise.resolve();
  console.log('B');
}

test();
console.log('C');

// 输出:A, C, B(与浏览器一致)

5. requestAnimationFrame 与 requestIdleCallback

5.1 渲染时机与帧率

浏览器渲染遵循屏幕刷新率,通常 60Hz(每帧约 16.67ms)。事件循环在每轮任务后检查是否有渲染机会:

[Task] → [Microtasks] → [rAF] → [Layout] → [Paint] → [Composite] → [rIC] → [Next Task]

如果任务执行时间过长,会跳过渲染机会,导致掉帧:

[Long Task 50ms] → [Microtasks] → [Skip Render] → [Next Task] → [Render]
                   ↑                                          ↑
                   跳过的帧                                   下一帧

5.2 requestAnimationFrame

requestAnimationFrame(rAF)在每次渲染前调用,常用于动画:

let startTime = null;
function animate(timestamp) {
  if (!startTime) startTime = timestamp;
  const progress = (timestamp - startTime) / 1000;
  element.style.transform = `translateX(${progress * 300}px)`;
  if (progress < 2) {
    requestAnimationFrame(animate);
  }
}
requestAnimationFrame(animate);

rAF 的特点

  • 与屏幕刷新率同步(60Hz 显示器约 16.67ms 一次)
  • 后台标签页暂停(节省电量)
  • 接收 DOMHighResTimeStamp 参数(页面加载后的毫秒数)
  • 返回 ID,可通过 cancelAnimationFrame 取消

rAF 与 setTimeout 对比

// 错误:用 setTimeout 做动画
function animateBad() {
  element.style.transform = `translateX(${x}px)`;
  x += 5;
  setTimeout(animateBad, 16);  // 不与渲染同步,可能掉帧
}

// 正确:用 rAF
function animateGood() {
  element.style.transform = `translateX(${x}px)`;
  x += 5;
  requestAnimationFrame(animateGood);  // 与渲染同步
}
维度setTimeout(fn, 16)requestAnimationFrame
调用时机定时器到期渲染前
与刷新率同步否是
后台标签页节流到 1Hz暂停
帧率自适应否(固定 16ms)是(60/120/144Hz 自动)
性能较差最优
推荐场景非动画任务动画、可视化

5.3 requestIdleCallback

requestIdleCallback(rIC)在浏览器空闲时调用,适合非紧急任务:

// 在空闲时处理日志上报
requestIdleCallback((deadline) => {
  while (deadline.timeRemaining() > 0 && logs.length > 0) {
    const log = logs.shift();
    sendLog(log);
  }
  if (logs.length > 0) {
    requestIdleCallback(sendLogs);  // 继续下一轮
  }
});

// 设置超时(最迟 2000ms 后执行)
requestIdleCallback(processQueue, { timeout: 2000 });

deadline 对象

  • timeRemaining():返回当前帧剩余空闲时间(毫秒)
  • didTimeout:是否已超时
requestIdleCallback((deadline) => {
  // 优先处理超时任务
  if (deadline.didTimeout) {
    processUrgentTasks();
  } else {
    // 在剩余时间内处理普通任务
    while (deadline.timeRemaining() > 1 && tasks.length > 0) {
      processTask(tasks.shift());
    }
  }
});

rIC 的特点

  • 不保证执行时间(依赖浏览器空闲)
  • 后台标签页可能完全不执行
  • 可能因超时而强制执行
  • 不应在 rIC 中修改 DOM(可能触发额外布局)

5.4 rAF 与 rIC 的区别

维度requestAnimationFramerequestIdleCallback
执行时机渲染前渲染后空闲时
是否保证执行是(每帧一次)否(可能跳过)
后台标签页暂停暂停
适用场景动画、可视化日志、数据预处理、低优先级任务
接收参数timestampdeadline(timeRemaining, didTimeout)
可设置超时否是
浏览器支持全部Chrome、Firefox(Safari 不支持)

5.5 帧时间预算

60Hz 显示器每帧 16.67ms,最佳实践:

  • JavaScript 执行:6-8ms
  • 样式计算与布局:4-5ms
  • 绘制与合成:3-4ms
// 长任务拆分
function processTasks(tasks) {
  const startTime = performance.now();
  while (tasks.length > 0) {
    const task = tasks.shift();
    task();
    // 检查是否接近帧时间预算
    if (performance.now() - startTime > 5) {
      // 让出主线程
      requestAnimationFrame(() => processTasks(tasks));
      break;
    }
  }
}

6. Promise 与微任务

6.1 Promise 的微任务语义

Promise 的 then/catch/finally 回调在微任务中执行:

const promise = new Promise((resolve) => {
  console.log('executor');
  resolve();
});

promise.then(() => console.log('then'));

console.log('sync');

// 输出:executor, sync, then

6.2 Promise 链与微任务

每个 then 都产生一个微任务:

Promise.resolve()
  .then(() => console.log('A'))
  .then(() => console.log('B'))
  .then(() => console.log('C'));

console.log('sync');

// 输出:sync, A, B, C

执行过程:

  1. 第一个 then 注册回调 → 微任务队列:[A]
  2. 同步代码执行 → 输出 sync
  3. 清空微任务:
    • 执行 A → 输出 A → A 返回 Promise,触发下一个 then → 微任务队列:[B]
    • 执行 B → 输出 B → 微任务队列:[C]
    • 执行 C → 输出 C

6.3 Promise.resolve 的优化

// 直接 resolve thenable
Promise.resolve().then(() => console.log('A'));

// resolve 一个 thenable
Promise.resolve(Promise.resolve()).then(() => console.log('B'));

// A 总是先于 B 执行
// 因为 resolve thenable 需要额外的微任务

V8 优化:当 Promise 已 resolved 时,Promise.resolve(p) 直接返回 p,避免额外微任务。

6.4 await 的内部机制

async function foo() {
  console.log('foo start');
  const result = await bar();
  console.log('foo end', result);
  return result;
}

function bar() {
  console.log('bar');
  return Promise.resolve(42);
}

foo();
console.log('main end');

// 输出:foo start, bar, main end, foo end 42

V8 7.2+ 的优化

旧版 V8 中,await 会创建 4 个微任务:

  1. 创建 Promise 包装 bar() 的返回值
  2. 挂起 foo,恢复调用者
  3. 解析 Promise
  4. 恢复 foo

新版 V8 优化为 2 个微任务:

  1. 挂起 foo,恢复调用者
  2. 解析并恢复 foo

性能提升约 65%。


7. 常见陷阱与修复

7.1 微任务递归导致 starvation

// 错误:微任务无限递归,宏任务永远无法执行
function recursiveMicrotask() {
  Promise.resolve().then(() => {
    console.log('microtask');
    recursiveMicrotask();  // 永不退出,UI 卡死
  });
}
recursiveMicrotask();
setTimeout(() => console.log('never'), 0);  // 永远不执行

修复:使用 setTimeout 让出控制权:

function safeRecursive() {
  Promise.resolve().then(() => {
    console.log('microtask');
    setTimeout(safeRecursive, 0);  // 让出主线程
  });
}

7.2 setInterval 累积漂移

// 错误:setInterval 在回调耗时长时漂移
setInterval(() => {
  doHeavyWork();  // 耗时 50-200ms
}, 100);
// 实际间隔可能变成 150-300ms

// 修复:使用递归 setTimeout + 绝对时间
let expected = Date.now() + 100;
function tick() {
  const drift = Date.now() - expected;
  doHeavyWork();
  expected += 100;
  setTimeout(tick, Math.max(0, 100 - drift));
}
setTimeout(tick, 100);

7.3 后台标签页节流

// 错误:依赖 setInterval 在后台标签继续运行
setInterval(updateClock, 1000);
// 后台标签页节流到 1Hz,但实际可能更慢

// 修复:基于时间戳计算
let lastTime = Date.now();
setInterval(() => {
  const now = Date.now();
  const delta = now - lastTime;
  lastTime = now;
  updateClock(delta);  // 基于 delta 更新
}, 1000);

// 监听可见性变化,重新同步
document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'visible') {
    resync();
  }
});

7.4 await 与 forEach 的陷阱

// 错误:forEach 不等待 await
const urls = ['/a', '/b', '/c'];
urls.forEach(async (url) => {
  const data = await fetch(url);
  console.log(data);
});
console.log('done');
// 'done' 立即输出,fetch 并行执行但顺序不确定

// 修复:使用 for...of 顺序执行
for (const url of urls) {
  const data = await fetch(url);
  console.log(data);
}
console.log('done');

// 或使用 Promise.all 并行执行
await Promise.all(urls.map(async (url) => {
  const data = await fetch(url);
  console.log(data);
}));
console.log('done');

7.5 Promise 构造器中的异步操作

// 错误:在 Promise 构造器中使用 await
const promise = new Promise(async (resolve) => {
  const data = await fetch('/api');  // 异步操作
  resolve(data);
});
// 如果 fetch 抛错,promise 不会 reject(错误被吞)

// 修复:使用 async 函数
async function fetchData() {
  const data = await fetch('/api');
  return data;
}

7.6 微任务中的异常处理

// 错误:微任务中的异常不被 try/catch 捕获
try {
  Promise.resolve().then(() => {
    throw new Error('oops');
  });
} catch (e) {
  // 永远不会执行
  console.error(e);
}

// 修复:在 Promise 链中处理
Promise.resolve()
  .then(() => {
    throw new Error('oops');
  })
  .catch(e => console.error(e));

// 或使用全局 unhandledrejection 监听
window.addEventListener('unhandledrejection', (event) => {
  console.error('Unhandled rejection:', event.reason);
});

7.7 requestAnimationFrame 在 SSR 中不可用

// 错误:SSR 中调用 rAF
function animate() {
  // ...
  requestAnimationFrame(animate);
}
animate();  // SSR 中 ReferenceError

// 修复:检查环境
const raf = typeof requestAnimationFrame !== 'undefined'
  ? requestAnimationFrame
  : (cb) => setTimeout(cb, 16);

7.8 nextTick 与 Promise 的混淆

// Node.js 中 nextTick 优先于 Promise
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
// 输出:nextTick, promise

// 浏览器中没有 nextTick
// 等价的优先级通过 Scheduler API 实现(提案中)

7.9 长任务阻塞主线程

// 错误:同步处理 10000 条数据
function processAll(data) {
  data.forEach(item => processItem(item));  // 阻塞主线程
}

// 修复:分批处理
function processBatched(data, batchSize = 100) {
  let i = 0;
  function processChunk() {
    const end = Math.min(i + batchSize, data.length);
    for (; i < end; i++) {
      processItem(data[i]);
    }
    if (i < data.length) {
      // 让出主线程,使用 setTimeout 或 requestIdleCallback
      setTimeout(processChunk, 0);  // 或 requestIdleCallback
    }
  }
  processChunk();
}

7.10 setImmediate 在浏览器中行为不一致

// 浏览器中 setImmediate 仅 IE/Edge 支持
// 不要在浏览器代码中使用 setImmediate
setImmediate(() => console.log('hi'));  // ReferenceError in Chrome/Firefox

// 修复:使用 MessageChannel 或 setTimeout
const channel = new MessageChannel();
channel.port1.onmessage = () => console.log('hi');
channel.port2.postMessage(null);  // 立即触发

8. 工程实践

8.1 长任务检测与拆分

// 检测长任务(>50ms)
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.warn(`长任务:${entry.duration}ms`, entry.attribution);
  }
});
observer.observe({ entryTypes: ['longtask'] });

// 拆分长任务
async function processLargeArray(items) {
  const CHUNK_SIZE = 100;
  for (let i = 0; i < items.length; i += CHUNK_SIZE) {
    const chunk = items.slice(i, i + CHUNK_SIZE);
    processChunk(chunk);
    // 每处理完一块,让出主线程
    if (i + CHUNK_SIZE < items.length) {
      await yieldToMain();
    }
  }
}

function yieldToMain() {
  return new Promise((resolve) => {
    // 优先使用 Scheduler API(如果支持)
    if ('scheduler' in window && 'postTask' in scheduler) {
      scheduler.postTask(resolve, { priority: 'user-visible' });
    } else {
      // 降级到 MessageChannel
      const channel = new MessageChannel();
      channel.port1.onmessage = resolve;
      channel.port2.postMessage(null);
    }
  });
}

8.2 优先级调度器

class PriorityScheduler {
  constructor() {
    this.queues = {
      'user-blocking': [],   // 最高优先级
      'user-visible': [],    // 中优先级
      'background': [],      // 低优先级
    };
    this.isRunning = false;
  }

  postTask(callback, { priority = 'user-visible' } = {}) {
    return new Promise((resolve, reject) => {
      this.queues[priority].push({ callback, resolve, reject });
      this.scheduleRun();
    });
  }

  scheduleRun() {
    if (this.isRunning) return;
    this.isRunning = true;
    // 优先使用原生 Scheduler API
    if ('scheduler' in window) {
      scheduler.postTask(() => this.runNext(), { priority: 'user-blocking' });
    } else {
      // 降级:使用 MessageChannel
      const channel = new MessageChannel();
      channel.port1.onmessage = () => this.runNext();
      channel.port2.postMessage(null);
    }
  }

  runNext() {
    this.isRunning = false;
    // 按优先级顺序选择任务
    for (const priority of ['user-blocking', 'user-visible', 'background']) {
      const queue = this.queues[priority];
      if (queue.length > 0) {
        const { callback, resolve, reject } = queue.shift();
        try {
          const result = callback();
          resolve(result);
        } catch (err) {
          reject(err);
        }
        // 继续调度下一个任务
        if (this.hasTasks()) {
          this.scheduleRun();
        }
        return;
      }
    }
  }

  hasTasks() {
    return Object.values(this.queues).some(q => q.length > 0);
  }
}

// 使用
const scheduler = new PriorityScheduler();
scheduler.postTask(() => doCriticalWork(), { priority: 'user-blocking' });
scheduler.postTask(() => doNormalWork(), { priority: 'user-visible' });
scheduler.postTask(() => doBackgroundWork(), { priority: 'background' });

8.3 背压控制

// 流式处理:基于消费者能力的背压
class BackpressureProcessor {
  constructor(concurrency = 4) {
    this.concurrency = concurrency;
    this.active = 0;
    this.queue = [];
  }

  async process(items, processor) {
    return new Promise((resolve) => {
      let index = 0;
      let completed = 0;
      const results = new Array(items.length);

      const next = () => {
        while (this.active < this.concurrency && index < items.length) {
          const currentIndex = index++;
          this.active++;

          Promise.resolve(processor(items[currentIndex]))
            .then((result) => {
              results[currentIndex] = result;
            })
            .catch((err) => {
              results[currentIndex] = { error: err };
            })
            .finally(() => {
              this.active--;
              completed++;
              if (completed === items.length) {
                resolve(results);
              } else {
                next();
              }
            });
        }
      };
      next();
    });
  }
}

// 使用
const processor = new BackpressureProcessor(4);
const results = await processor.process(urls, async (url) => {
  const response = await fetch(url);
  return response.json();
});

8.4 动画调度优化

// 多个动画统一调度,避免多次 rAF
class AnimationScheduler {
  constructor() {
    this.callbacks = new Set();
    this.scheduled = false;
  }

  add(callback) {
    this.callbacks.add(callback);
    this.schedule();
    return () => this.callbacks.delete(callback);
  }

  schedule() {
    if (this.scheduled) return;
    this.scheduled = true;
    requestAnimationFrame((timestamp) => {
      this.scheduled = false;
      const callbacks = Array.from(this.callbacks);
      for (const cb of callbacks) {
        cb(timestamp);
      }
    });
  }
}

// 使用
const animScheduler = new AnimationScheduler();
element.addEventListener('input', () => {
  animScheduler.add((timestamp) => {
    updateProgress(timestamp);
  });
});

8.5 离线任务队列

// Service Worker 中的后台同步
self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-messages') {
    event.waitUntil(syncMessages());
  }
});

async function syncMessages() {
  const messages = await getAllPendingMessages();
  for (const message of messages) {
    try {
      await sendMessage(message);
      await markAsSent(message.id);
    } catch (err) {
      // 失败时重新注册同步
      await self.registration.sync.register('sync-messages');
      return;
    }
  }
}

// 主线程注册同步
async function registerSync() {
  if ('serviceWorker' in navigator && 'SyncManager' in window) {
    const reg = await navigator.serviceWorker.ready;
    await reg.sync.register('sync-messages');
  } else {
    // 降级:在线时立即同步
    window.addEventListener('online', syncMessages);
  }
}

9. 案例研究

9.1 React Fiber 与时间切片

React 16+ 的 Fiber 架构基于事件循环实现时间切片:

// React Fiber 简化模型
function workLoop(deadline) {
  let shouldYield = false;
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    // 检查是否需要让出主线程
    shouldYield = deadline.timeRemaining() < 1;
  }
  if (nextUnitOfWork) {
    requestIdleCallback(workLoop);
  }
}

requestIdleCallback(workLoop);

React 18 改用 MessageChannel 调度,提供更精确的时间控制:

// React 18 Scheduler 简化
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;

function schedulePerformWork() {
  port.postMessage(null);
}

function performWorkUntilDeadline() {
  const startTime = performance.now();
  let hasMoreWork = true;
  try {
    while (hasMoreWork && !shouldYield(startTime)) {
      hasMoreWork = performWork();
    }
  } finally {
    if (hasMoreWork) {
      schedulePerformWork();
    }
  }
}

function shouldYield(startTime) {
  // 5ms 时间片
  return performance.now() - startTime > 5;
}

9.2 Vue 的 nextTick

Vue 的 nextTick 利用微任务实现 DOM 更新后的回调:

// Vue 3 nextTick 实现
const resolvedPromise = Promise.resolve();
let currentFlushPromise = null;

export function nextTick(fn) {
  const p = currentFlushPromise || resolvedPromise;
  return fn ? p.then(fn) : p;
}

// 在组件更新流程中
function flushJobs() {
  // 执行所有 watcher
  // ...
  currentFlushPromise = null;
}

function queueFlush() {
  if (!currentFlushPromise) {
    currentFlushPromise = resolvedPromise.then(flushJobs);
  }
}

9.3 Node.js 的 cluster 模块

Node.js 通过 cluster 模块利用多核 CPU,每个 worker 是独立的事件循环:

const cluster = require('cluster');
const os = require('os');

if (cluster.isPrimary) {
  const numCPUs = os.cpus().length;
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
  cluster.on('exit', (worker) => {
    console.log(`Worker ${worker.process.pid} died`);
    cluster.fork();  // 重启
  });
} else {
  // 每个 worker 是独立的 Node.js 进程
  require('./server');
}

9.4 浏览器 tab 的事件循环独立性

每个浏览器标签页有独立的事件循环,但同一 origin 的多个标签可能共享 Service Worker。多标签页通信需使用 BroadcastChannel 或 postMessage。


10. 对比分析

10.1 不同宿主环境的事件循环对比

宿主实现微任务任务队列渲染
ChromeBlink单队列多队列有
FirefoxSpiderMonkey + Gecko单队列多队列有
SafariJavaScriptCore + WebKit单队列多队列有
Node.jsV8 + libuvnextTick + microtask6 阶段无
DenoV8 + Tokio单队列单队列无
BunJavaScriptCore单队列单队列无
Web Worker同浏览器(无渲染)单队列多队列无
Service Worker同浏览器(无 DOM)单队列多队列无

10.2 微任务 API 对比

API浏览器Node.js优先级
Promise.then支持支持微任务
queueMicrotask支持支持微任务
MutationObserver支持不支持微任务
process.nextTick不支持支持高于微任务
Immediate部分支持支持(setImmediate)宏任务

10.3 任务调度策略对比

策略优点缺点适用场景
setTimeout(0)简单4ms 延迟通用
setImmediate(Node.js)立即仅 NodeNode.js I/O 后
MessageChannel0 延迟复杂高性能调度
queueMicrotask高优先级可能 starvationPromise 链
requestAnimationFrame同步渲染仅动画视觉更新
requestIdleCallback不阻塞不保证时间低优先级
postTask(Scheduler API)优先级控制兼容性现代应用

10.4 MessageChannel vs setTimeout(0)

// MessageChannel 延迟接近 0
function measureMessageChannel() {
  const channel = new MessageChannel();
  const start = performance.now();
  channel.port1.onmessage = () => {
    console.log(`MessageChannel: ${performance.now() - start}ms`);
  };
  channel.port2.postMessage(null);
}
measureMessageChannel();  // 通常 0.1-1ms

// setTimeout(0) 有最小延迟
function measureTimeout() {
  const start = performance.now();
  setTimeout(() => {
    console.log(`setTimeout: ${performance.now() - start}ms`);
  }, 0);
}
measureTimeout();  // 通常 1-4ms
维度MessageChannelsetTimeout(0)
最小延迟~0ms1-4ms
嵌套不受限是5 层后 4ms
后台标签页不节流节流到 1Hz
优先级任务源:MessageChannel任务源:Timer
复杂度中低

填空题知识点讲解

  1. (remember)在浏览器事件循环中,每执行完一个宏任务后会清空 ______ 队列中的所有任务。
  2. (understand)HTML 规范规定嵌套超过 ______ 层的 setTimeout 最小延迟为 4ms。
  3. (remember)Node.js 事件循环的六个阶段依次是 timers、、idle/prepare、、check、close callbacks。
  4. (understand)async/await 中 await 后的代码等价于 ______ 的回调,进入微任务队列。
  5. (remember)requestAnimationFrame 在 ______ 前执行,requestIdleCallback 在 ______ 后执行。

11.3 代码修复题

  1. (analyze)以下代码预期每 100ms 输出一次心跳,但实际在长时间运行后出现严重漂移。请修复:
setInterval(() => {
  console.log('heartbeat', Date.now());
  doHeavyWork();  // 耗时 50-200ms
}, 100);

解析讲解:

// 使用递归 setTimeout 替代 setInterval,基于绝对时间补偿漂移
let expected = performance.now() + 100;
const tick = () => {
  const drift = performance.now() - expected;
  console.log('heartbeat', performance.now(), 'drift:', drift);
  doHeavyWork();
  expected += 100;
  // 延迟为下一周期目标时间与当前时间的差
  setTimeout(tick, Math.max(0, 100 - (performance.now() - expected + 100)));
};
setTimeout(tick, 100);
  1. (evaluate)以下代码期望按顺序输出 1, 2, 3,但实际输出顺序不确定。请修复:
const items = [1, 2, 3];
items.forEach(async (item) => {
  const result = await fetch(`/api/${item}`);
  console.log(result);
});

解析讲解:

// 使用 for...of 顺序执行
const items = [1, 2, 3];
for (const item of items) {
  const result = await fetch(`/api/${item}`);
  console.log(result);
}

// 或使用 Promise.all 并行执行(如果顺序不重要)
await Promise.all(items.map(async (item) => {
  const result = await fetch(`/api/${item}`);
  console.log(result);
}));
  1. (create)实现一个支持时间切片的长任务处理器,要求:
  • 每帧最多执行 5ms 任务
  • 不阻塞主线程
  • 支持取消

解析讲解:

class TimeSlicedProcessor {
  constructor() {
    this.cancelled = false;
  }

  async process(items, processor) {
    const CHUNK_TIME = 5;  // 每帧最多 5ms
    let i = 0;

    while (i < items.length && !this.cancelled) {
      const startTime = performance.now();
      while (i < items.length &&
             performance.now() - startTime < CHUNK_TIME &&
             !this.cancelled) {
        await processor(items[i]);
        i++;
      }
      if (i < items.length && !this.cancelled) {
        await this.yieldToMain();
      }
    }
  }

  yieldToMain() {
    return new Promise((resolve) => {
      if ('scheduler' in window) {
        scheduler.postTask(resolve, { priority: 'user-visible' });
      } else {
        const channel = new MessageChannel();
        channel.port1.onmessage = resolve;
        channel.port2.postMessage(null);
      }
    });
  }

  cancel() {
    this.cancelled = true;
  }
}

// 使用
const processor = new TimeSlicedProcessor();
processor.process(bigArray, async (item) => {
  await processItem(item);
});

// 取消
// processor.cancel();

11.4 开放式问题

  1. (create)请设计一个支持任务优先级的调度器,要求:(1) 高优先级任务先于低优先级执行;(2) 同优先级按 FIFO;(3) 每帧预留 5ms 给渲染;(4) 不阻塞主线程。请描述数据结构与调度算法。

    参考要点:

    • 数据结构:多优先级队列(小顶堆或链表数组)
    • 时间预算:performance.now() 检查
    • 让出机制:requestAnimationFrame 钩子或 MessageChannel
    • 可中断恢复:保存任务上下文
    • 降级:setTimeout(0) 兜底
    • 背压:高优先级队列过长时丢弃低优先级任务
  2. (evaluate)比较浏览器与 Node.js 事件循环的异同,分析为何 Node.js 需要独立的模型。

    参考要点:

    • 浏览器:面向 UI 交互,需要渲染时机
    • Node.js:面向服务器,重视 I/O 吞吐
    • 阶段模型:libuv 的 poll 阶段是核心
    • 微任务:nextTick 是 Node.js 特有需求
    • 渲染:Node.js 无需渲染
    • 多核:Node.js 通过 cluster/worker_threads 利用多核
  3. (analyze)分析 setTimeout(fn, 0) 与 queueMicrotask(fn) 的本质区别,并说明何时选择哪个。

    参考要点:

    • 队列:宏任务 vs 微任务
    • 时机:下一轮 vs 当前任务后
    • 延迟:4ms(嵌套后) vs 0ms
    • starvation:微任务可能饿死宏任务
    • 选择:紧急且短小用微任务,需要让出主线程用宏任务
  4. (create)设计一个支持取消与超时的 Promise 包装器。

    参考要点:

    • 使用 AbortController 实现取消
    • 使用 Promise.race 实现超时
    • 错误类型:AbortError、TimeoutError
    • 资源清理:取消后释放底层资源
    • 信号传播:嵌套调用时传递 signal
  5. (evaluate)讨论事件循环模型对 JavaScript 性能调优的影响,列举至少 3 个优化策略。

    参考要点:

    • 长任务拆分:避免阻塞主线程
    • 微任务控制:避免无限递归
    • rAF 同步:动画与渲染对齐
    • rIC 利用:低优先级任务
    • 背压:避免内存溢出
    • 后台标签页:节流感知

12.1 规范文档

12.2 书籍

  • Flanagan, D. (2020). JavaScript: The Definitive Guide, 7th Edition. O’Reilly Media. ISBN 978-1491952023.
  • Richardson, L., & Ruby, S. (2007). RESTful Web Services. O’Reilly Media. ISBN 978-0596529260.
  • Kleppmann, M. (2017). Designing Data-Intensive Applications. O’Reilly Media. ISBN 978-1449373320.
  • Titzer, B. L. (2019). Event-Loop Programming. ACM Queue.

12.3 论文

12.4 经典演讲

12.5 开源项目

13. 附录

13.1 事件循环执行顺序速查

同步代码 → 微任务队列清空 → requestAnimationFrame → 渲染 → 微任务队列清空 → requestIdleCallback → 下一个宏任务

13.2 Node.js 阶段速查

timers → pending callbacks → idle/prepare → poll → check → close callbacks
                                                                  ↓
                              每个阶段切换时清空 nextTickQueue + microtaskQueue

13.3 常见 API 任务类型速查

API类型优先级
Promise.then微任务中
queueMicrotask微任务中
MutationObserver微任务中
process.nextTick微任务(仅 Node.js)高
setTimeout宏任务低
setInterval宏任务低
setImmediate(Node.js)宏任务中
MessageChannel宏任务中
requestAnimationFrame渲染前高
requestIdleCallback空闲时低
postMessage宏任务中
I/O 回调宏任务中

13.4 性能清单

  • 长任务(>50ms)已被拆分
  • 动画使用 requestAnimationFrame
  • 低优先级任务使用 requestIdleCallback
  • 后台标签页暂停非关键定时器
  • 微任务不递归调用
  • setInterval 改为递归 setTimeout
  • 大数据处理使用时间切片
  • 监控 unhandledrejection
  • 避免在 Promise 构造器中异步操作
  • 使用 MessageChannel 替代 setTimeout(0) 实现零延迟

13.5 调试技巧

// 1. 追踪任务执行顺序
const originalThen = Promise.prototype.then;
Promise.prototype.then = function (...args) {
  console.trace('Promise.then called');
  return originalThen.apply(this, args);
};

// 2. 测量微任务执行时间
const start = performance.now();
Promise.resolve().then(() => {
  console.log(`微任务延迟:${performance.now() - start}ms`);
});

// 3. 检测长任务
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.warn('长任务:', entry.duration, 'ms');
  }
});
observer.observe({ entryTypes: ['longtask'] });

// 4. 追踪 Node.js 事件循环延迟
const { performance } = require('perf_hooks');
setInterval(() => {
  const start = performance.now();
  setImmediate(() => {
    const delay = performance.now() - start;
    if (delay > 10) {
      console.warn(`事件循环延迟:${delay.toFixed(2)}ms`);
    }
  });
}, 1000);

13.6 术语表

术语定义
事件循环(Event Loop)协调任务、微任务、渲染的执行机制
任务(Task)宏任务,由任务源调度
微任务(Microtask)在当前任务后立即执行的任务
任务源(Task Source)任务的分类,决定队列归属
渲染机会(Rendering Opportunity)浏览器决定是否渲染的时机
长任务(Long Task)执行时间超过 50ms 的任务
时间切片(Time Slicing)将长任务拆分为多个短任务
背压(Backpressure)消费者反馈给生产者的流速控制
协作式多任务(Cooperative Multitasking)任务主动让出 CPU 的并发模型
反应器模式(Reactor Pattern)事件驱动 I/O 的设计模式
Job(ECMAScript)规范定义的抽象任务,由宿主实现为微任务
Agent(ECMAScript)执行代理,可独立运行的逻辑单元
libuvNode.js 使用的跨平台异步 I/O 库
nextTickQueueNode.js 中存储 process.nextTick 回调的队列
帧时间预算(Frame Budget)单帧允许的最大执行时间(约 16.67ms)

14. 修订日志

版本日期修订内容修订人
1.02026-06-14初始版本fanquanpp
2.02026-07-20金标准升级:新增 HTML 规范处理模型、Node.js libuv 阶段、rAF/rIC 调度、微任务优先级、案例研究(React Fiber/Vue nextTick)、习题、参考文献FANDEX Content Engineering Team

手推执行时序(先写答案,再跑代码验证)

请按“同步栈 → 微任务队列 → 宏任务队列”手动推演下面的输出顺序:

console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');

预期输出:A D C B。理由:同步代码先执行(A、D);微任务(Promise.then)在同步栈清空后、下一个宏任务前执行(C);setTimeout 是宏任务,最后执行(B)。

执行时序表(浏览器):

队列示例执行时机
同步栈普通函数调用当前任务立即执行
微任务Promise.then / queueMicrotask同步栈清空后立即执行
宏任务setTimeout / 事件回调每个宏任务之间执行所有微任务
渲染requestAnimationFrame渲染前执行,与屏幕刷新同步

记住一句话:微任务清空后才轮到下一个宏任务。遇到 async/await 时,await 后面的代码相当于 .then 回调,也进微任务。