> ## Documentation Index
> Fetch the complete documentation index at: https://opc-43d279b8.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Event Loop

> 讲清任务与微任务的调度顺序、浏览器渲染时机、async/await 输出题，以及 Node.js 事件循环差异。

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

***

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

```text theme={null}
┌──────────────┐   ┌──────────────┐   ┌──────────────┐   ┌──────────────┐
│  同步代码    │ → │  微任务队列  │ → │  浏览器渲染  │ → │  任务队列    │
│  (Call Stack)│   │ (Microtask)  │   │ (Layout/Paint│   │ (Task)       │
│              │   │              │   │  /Composite) │   │              │
└──────────────┘   └──────────────┘   └──────────────┘   └──────────────┘
       ↑                                                         │
       └─────────────────────────────────────────────────────────┘
                              无限循环
```

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

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

**2. 致命痛点: 同步阻塞**

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

**3. 1996年: Event Loop 机制引入**

Netscape 2.0 引入了 `setTimeout`，让 JS 可以"延迟执行"代码。但底层怎么实现？浏览器需要一个机制来管理这些"稍后执行"的代码——这就是 Event Loop。

**Event Loop 解决的核心问题：**

```text theme={null}
问题：单线程 + 同步执行 = 阻塞

用户点击按钮 → 发网络请求 → 等服务器响应（3秒）→ 页面阻塞 3 秒

解决：把"等"的事情交给浏览器（它不是单线程的），
     JS 继续执行后面的代码，等结果回来再处理

用户点击按钮 → 发网络请求 → JS 继续执行 → 服务器响应到了 → JS 再处理
```

> [!NOTE]
>
> Event Loop 让单线程的 JS 可以非阻塞地处理异步任务。

> [card] 🚫 没有 Event Loop
>
> 如果开一家餐厅，又当服务员又当厨师（单线程同步）。客人点完单，你跑去后厨炒菜，这时新来的客人只能干瞪眼等着（阻塞）。

> [card] ✅ 引入 Event Loop
>
> 有了 Event Loop 后，相当于雇了厨师（浏览器的 Web API 多线程）。你（主线程）只负责招待客人，把菜单递给厨房后，立刻去接待下一桌。等厨师把菜做好了，按一下响铃（把回调放入任务队列），你听到铃声再去把菜端给客人。

> [!TIP]
>
> Event Loop 就像一家只有一位咖啡师的咖啡店（引用自 Jake Archibald）。咖啡师是单线程的——一次只能做一杯咖啡（执行一个任务）。排队等候的顾客是宏任务（setTimeout、I/O 等），必须按顺序一个个来。但如果你是"加急订单"（微任务：Promise.then），咖啡师会在做完当前这杯之后、服务下一位普通顾客之前，优先把你的加急单做完。而且，如果加急单做完又来了新的加急单，咖啡师会继续做完所有加急单，直到加急队列彻底清空，才去叫下一个普通顾客的号。

> [!TIP]
>
> 回答时先给宏观框架（30 秒说出 4 个阶段），然后等对方追问某一步的细节。这展示了你的知识体系和沟通能力。不要一上来就背诵 V8 源码。

***

## Q2: 同步代码的执行机制

同步代码在调用栈（Call Stack）中执行。调用栈是一个后进先出（LIFO）的数据结构，每次调用函数就压入一个栈帧，函数返回就弹出。

```text theme={null}
┌───────────────────────────────────────┐
│           Call Stack（调用栈）         │  ← 同步代码在这里执行
│  ┌─────────────────────────────────┐  │
│  │ main()                          │  │
│  │   └─ foo()                      │  │
│  │       └─ bar()  ← 当前执行      │  │
│  └─────────────────────────────────┘  │
└───────────────────────────────────────┘
```

```javascript theme={null}
// 调用栈的执行过程
function bar() {
  return 1;
}
function foo() {
  return bar();
}
foo(); // main → foo → bar → bar 返回 → foo 返回 → main 返回
```

> [!WARNING]
>
> 调用栈是单线程的——同一时间只能执行一个函数。如果一个函数执行时间过长（如死循环、大量计算），调用栈就一直被占用，后续的同步代码、微任务、宏任务、渲染都无法执行。
>
> * 这就是为什么 `while(true)` 会导致页面阻塞。
> * 这也是为什么需要异步——把耗时操作交给浏览器后台线程，JS 主线程继续执行。

> [!TIP]
>
> 调用栈就像超市收银台——只有一个窗口（单线程），每次只能服务一位顾客（执行一个函数）。如果前面的顾客买了整整一车东西（耗时操作），后面的顾客只能干等。Event Loop 的作用就是：把"等快递"这种耗时的事情交给快递员（浏览器后台线程），收银员继续服务下一位顾客。

### 执行上下文的类型

执行上下文（Execution Context）有三种类型：

| 类型 | 说明 | 存储位置 |
| - | - | - |
| **全局执行上下文** | 代码首次执行时创建，`this` 指向 `window` | 调用栈底部 |
| **函数执行上下文** | 每次调用函数时创建，函数执行完销毁 | 调用栈中 |
| **Eval 执行上下文** | `eval()` 函数内部的代码 | 调用栈中（不推荐使用） |

```javascript theme={null}
// 执行上下文的创建过程
var x = 10; // 全局执行上下文
function foo() {
  // 函数执行上下文
  var y = 20;
  function bar() {
    // 函数执行上下文
    var z = 30;
    return x + y + z; // 沿作用域链查找 x, y, z
  }
  return bar();
}
foo(); // 调用栈：main → foo → bar
```

### 作用域链与闭包

作用域链（Scope Chain）是执行上下文创建时生成的变量对象链。当访问一个变量时，引擎会沿着作用域链从内到外查找。

闭包（Closure）的本质是：函数记住了它被创建时的词法环境。当一个内部函数引用了外部函数的变量，即使外部函数执行完毕，这些变量也不会被垃圾回收——因为内部函数还持有对它们的引用。

```javascript theme={null}
// 闭包的本质：函数记住词法环境
function createCounter() {
  let count = 0; // 这个变量被闭包"记住"了
  return function () {
    return ++count; // 每次调用都访问同一个 count
  };
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
// createCounter 执行完毕，但 count 没有被回收
```

***

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

调用栈清空后，引擎会彻底清空微任务队列。微任务的优先级高于宏任务——必须把所有微任务执行完，才能进入下一个阶段。

```text theme={null}
┌───────────────────────────────────────┐
│        Microtask Queue（微任务队列）   │  ← Promise.then, queueMicrotask
│  ┌─────────────────────────────────┐  │
│  │ 微任务 1 → 微任务 2 → 微任务 3  │  │
│  └─────────────────────────────────┘  │
└───────────────────────────────────────┘
         │
         ▼  全部清空（包括新产生的）
    进入渲染阶段或取宏任务
```

```javascript theme={null}
// 微任务的执行顺序
console.log("1"); // 同步
setTimeout(() => console.log("2"), 0); // 宏任务
Promise.resolve().then(() => console.log("3")); // 微任务
console.log("4"); // 同步

// 输出：1 → 4 → 3 → 2
// 同步代码先执行 → 清空微任务 → 取宏任务
```

> [!WARNING]
>
> Event Loop 的铁律：**必须把当前的微任务队列彻底清空，才能进入下一个阶段（渲染或下一个宏任务）。**
>
> * 如果微任务中又产生了新的微任务，新产生的也会在本轮清空——不会推迟到下一轮。
> * 这意味着如果微任务无限递归产生新微任务，主线程会永远阻塞在这一步，页面彻底无响应。
> * 宏任务不存在这个问题——每次只取一个宏任务执行，执行完就有机会去渲染和响应用户输入。

> [!TIP]
>
> 宏任务就像咖啡店里排队等候的普通顾客（引用自 Jake Archibald）。做完一杯，如果你还想再点一杯，必须乖乖去队尾重新排。微任务则拥有"加急特权"——做完当前这杯，你直接跟咖啡师说"我马上还要一杯"，他必须立刻给你做。只要加急队列里还有人（不管是不是中途刚加进来的），咖啡师就一直服务加急顾客，直到加急队伍彻底空了，才去叫下一个普通顾客的号。如果加急单源源不断（微任务无限递归），咖啡师就永远困在加急队列里，普通顾客永远轮不到。

### 为什么 W3C 要设计微任务？

在早期的浏览器中，确实只有宏任务。但随着前端复杂度的提升，W3C 需要一种机制来监控 DOM 的变化，于是引入了 MutationObserver。

如果每次 DOM 发生细微改变就同步触发回调，性能会直接崩溃；如果用宏任务（如 setTimeout）来异步触发，回调执行的时机就要被推迟到下一次 Event Loop，也就是浏览器渲染之后了。这会导致极其严重的视觉闪烁（先渲染了旧状态，宏任务再修改 DOM 触发二次渲染）。

**为了解决这个两难境地，微任务（Microtask）应运而生。** 它的设计初衷是：在当前同步代码执行完毕后、浏览器下一次渲染（Paint）之前，批量、原子化地处理完所有的状态更新。这样就能保证在屏幕刷新前，DOM 已经被彻底修改到了最终形态。这也是为什么 Vue 的 `$nextTick` 底层首选 `Promise.then` 来实现异步更新队列的原因。

```ts theme={null}
// Vue 的 $nextTick 本质就是微任务
// Vue 2 源码：src/core/util/next-tick.js
function nextTick(cb, ctx) {
  let _resolve;
  callbacks.push(() => {
    if (cb) {
      cb.call(ctx);
    } else {
      _resolve(ctx);
    }
  });
  if (!pending) {
    pending = true;
    timerFunc(); // timerFunc 优先使用 Promise.then
  }
}

// Vue 3 源码：packages/runtime-core/src/scheduler.ts
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>;
let currentFlushPromise: Promise<void> | null = null;
function nextTick(fn?: () => void): Promise<void> {
  const p = currentFlushPromise || resolvedPromise;
  return fn ? p.then(this ? fn.bind(this) : fn) : p;
}
```

在 Node.js 环境下，微任务内部也存在严格的阶级差异：`process.nextTick` 属于 Node.js 自己的底层机制，存放在 `nextTickQueue` 中；而 `Promise.then` 属于 V8 引擎的机制，存放在 `microtaskQueue` 中。在进入每一个阶段之前，Node.js 都会强制优先清空 `nextTickQueue`，然后再把控制权交给 V8 清空 Promise 队列。因此，不管代码怎么写，`process.nextTick` 永远快于 `Promise.then`。

### 微任务的分类

| 微任务类型 | 说明 | 适用环境 |
| - | - | - |
| **Promise.then/catch/finally** | Promise 的回调 | 浏览器 + Node.js |
| **queueMicrotask()** | 显式添加微任务 | 浏览器 + Node.js |
| **MutationObserver** | DOM 变化监听 | 浏览器 |
| **process.nextTick** | Node.js 特有 | Node.js |

```javascript theme={null}
// 微任务的各种来源
Promise.resolve().then(() => console.log("promise"));
queueMicrotask(() => console.log("microtask"));

// MutationObserver（浏览器）
const observer = new MutationObserver(() => console.log("mutation"));
observer.observe(document.body, { childList: true });
document.body.appendChild(document.createElement("div"));
// 输出：promise → microtask → mutation
// 三者都在同一轮微任务里，按入队顺序执行
```

### process.nextTick vs Promise.then（Node.js）

在 Node.js 中，`process.nextTick` 的优先级**高于** `Promise.then`。原因是 `process.nextTick` 属于 Node.js 自己的底层机制，存放在 `nextTickQueue` 中；而 `Promise.then` 属于 V8 引擎的机制，存放在 `microtaskQueue` 中。

```javascript theme={null}
// Node.js 中的优先级
process.nextTick(() => console.log("nextTick 1"));
Promise.resolve().then(() => console.log("promise 1"));
process.nextTick(() => console.log("nextTick 2"));

// 输出：nextTick 1 → nextTick 2 → promise 1
// process.nextTick 全部执行完，才执行 Promise.then
```

> [!WARNING]
>
> `process.nextTick` 会在当前阶段结束、切换到下一阶段前立即执行。如果在 `process.nextTick` 中不断递归产生新的 `process.nextTick`，会饥饿整个 I/O 轮询——Node.js 永远无法进入下一个阶段，I/O 回调永远无法执行。
>
> ```javascript theme={null}
> // ❌ 饥饿 I/O——Node.js 永远卡在 nextTick 阶段
> function recursiveNextTick() {
>   process.nextTick(recursiveNextTick);
> }
> recursiveNextTick(); // I/O 回调永远不会执行
>
> // ✅ 用 setImmediate 替代——不会饥饿 I/O
> function recursiveImmediate() {
>   setImmediate(recursiveImmediate);
> }
> recursiveImmediate(); // 每次执行完都有机会处理 I/O
> ```

***

## Q4: 浏览器渲染阶段

微任务清空后，浏览器按帧决定是否渲染。渲染由浏览器调度，通常对齐显示器刷新率（60Hz 下约 16.67ms 一帧），并不是每过 16.67ms 就必然渲染一次。

```text theme={null}
微任务清空 → 浏览器判断是否渲染？
              │
              ├─ 不需要 → 直接取宏任务
              │
              └─ 需要渲染 → rAF → Layout → Paint → Composite → 取宏任务
```

渲染阶段的执行顺序：

| 阶段 | 做什么 | 代价 |
| - | - | - |
| **requestAnimationFrame** | 在渲染前执行动画回调 | 低 |
| **Layout（重排）** | 计算每个节点的几何信息 | 高 |
| **Paint（重绘）** | 生成绘制指令列表 | 中 |
| **Composite（合成）** | GPU 合成最终画面 | 低 |

> [!WARNING]
>
> JS 执行和页面渲染共用同一个主线程。JS 执行时，渲染必须等待。如果一段 JS 代码执行了 3 秒，这 3 秒内浏览器没有机会执行渲染——页面白屏，无法交互。
>
> ```javascript theme={null}
> // ❌ 阻塞渲染 3 秒——页面白屏
> const start = Date.now();
> while (Date.now() - start < 3000) {}
> ```

> [!TIP]
>
> Layout 就像施工队量房间尺寸、算家具位置（几何计算）。Paint 就像按图纸刷墙、铺地板（绘制指令）。Composite 就像最后的软装——把家具摆好、挂上窗帘（GPU 合成）。如果只是换个窗帘颜色（opacity 变化），不需要重新量房间（重排），直接换就行。

### 重排（Reflow）的触发条件

重排需要重新计算节点的几何信息，代价最高。触发条件：

* DOM 增删（插入/删除节点）
* 元素尺寸变化（`width`、`height`、`padding`、`margin`、`border`）
* 内容变化（文本增减、图片尺寸变化）
* 浏览器窗口 resize
* 读取布局属性（`offsetWidth`、`offsetHeight`、`getComputedStyle`、`getBoundingClientRect`）

```javascript theme={null}
// ❌ 强制同步布局：读取 → 修改 → 读取 → 修改（多次重排）
for (const el of elements) {
  el.style.width = el.offsetWidth + 10 + "px"; // 每次循环都重排
}

// ✅ 批量读取 → 批量修改（一次重排）
const widths = elements.map((el) => el.offsetWidth);
elements.forEach((el, i) => (el.style.width = widths[i] + 10 + "px"));
```

### 重绘（Repaint）的触发条件

重绘只需要重新生成绘制指令，不影响布局，代价较低。触发条件：

* 颜色变化（`color`、`background-color`、`border-color`）
* 阴影变化（`box-shadow`、`text-shadow`）
* 可见性变化（`visibility`、`outline`）
* 文本装饰变化（`text-decoration`）

### Composite 阶段的优化

合成阶段由 GPU 执行，只需要矩阵变换，不需要重排重绘。触发合成的操作：

* `transform`（位移、缩放、旋转）
* `opacity`（透明度）
* `will-change`（提前告知浏览器哪些属性会变化）

```javascript theme={null}
// 只触发 Composite，不触发 Layout 和 Paint
element.style.transform = "translateX(100px)";
element.style.opacity = 0.5;

// 触发 Layout + Paint + Composite，性能差
element.style.left = "100px";
element.style.width = "200px";
```

***

## Q5: 宏任务（task）的执行机制

渲染完成后（或跳过渲染），从任务队列中取出一个 task 执行。每次只取一个——执行完后回到步骤 1，开始下一轮循环。

HTML 规范里只有 **task（任务）** 这个概念，`setTimeout`、I/O、用户交互的回调分别属于不同的 task source；"宏任务"（macrotask）是社区俗称，规范中并不存在这个术语，用它只是为了和 microtask 对照。

```text theme={null}
┌───────────────────────────────────────┐
│   Task Queue（任务队列，俗称宏任务）   │  ← setTimeout, setInterval, I/O
│  ┌─────────────────────────────────┐  │
│  │ 宏任务 1 → 宏任务 2 → 宏任务 3  │  │
│  └─────────────────────────────────┘  │
└───────────────────────────────────────┘
         │
         ▼  每次只取一个
    执行完 → 回到步骤 1（执行同步代码 → 清空微任务 → 渲染 → 取下一个宏任务）
```

**宏任务和微任务的分类：**

| 类型 | 包含 | 优先级 |
| - | - | - |
| **同步代码** | 所有 JS 语句 | 最高 |
| **微任务** | Promise.then/catch/finally、queueMicrotask、MutationObserver、process.nextTick（Node.js） | 高 |
| **宏任务** | setTimeout/setInterval、I/O、用户交互、MessageChannel | 低 |

> [!TIP]
>
> 宏任务就像快递站点每天派送一批包裹，每次只派一个，派完回站点再拿下一个。微任务就像快递员手里的"加急件"——每当他送完一个普通包裹（宏任务），必须先把手里所有加急件全部送完，才能去拿下一个普通包裹。如果加急件源源不断（微任务无限递归），快递员就永远在送加急件，普通包裹永远送不到。

> [!WARNING]
>
> 微任务在当前宏任务执行完后**立即清空**，宏任务每次只取一个。这意味着：
>
> * 微任务适合：需要在当前操作完成后、渲染前批量处理的逻辑（如 Vue 的 `$nextTick`）
> * 宏任务适合：可以推迟到下一轮循环的逻辑（如 `setTimeout` 做时间切片）
>
> ```javascript theme={null}
> // ❌ 微任务无限递归——页面阻塞
> function loop() {
>   Promise.resolve().then(loop);
> }
> loop(); // 主线程永远阻塞在清空微任务，无法渲染
>
> // ✅ 宏任务递归——页面不阻塞
> function loop() {
>   setTimeout(loop, 0);
> }
> loop(); // 每次执行完一个宏任务，都有机会渲染
> ```

### 宏任务的分类

| 宏任务类型 | 说明 | 适用环境 |
| - | - | - |
| **setTimeout/setInterval** | 定时器回调 | 浏览器 + Node.js |
| **I/O 回调** | 文件读写、网络请求 | 浏览器 + Node.js |
| **UI 渲染** | 不属于 task queue，见 Q4 | 浏览器 |
| **requestAnimationFrame** | 渲染前回调，非 task | 浏览器 |
| **setImmediate** | Node.js 特有 | Node.js |
| **MessageChannel** | 消息通道 | 浏览器 + Node.js |

### requestAnimationFrame 的执行时机

`requestAnimationFrame`（rAF）严格说不是宏任务也不是微任务——它在浏览器渲染前执行。

```javascript theme={null}
// 执行顺序：
同步代码 → 微任务 → rAF → 浏览器渲染 → 宏任务

// rAF 保证在每一帧渲染前执行
function animate() {
  element.style.transform = `translateX(${x}px)`;
  x += 1;
  if (x < 300) requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
// 比 setTimeout(fn, 16) 更准确，因为它与显示器刷新率同步
```

> [!WARNING]
>
> `setTimeout(fn, 16)` 的问题：
>
> 1. 最小延迟 4ms 的限制
> 2. 回调执行时间和渲染时机不同步，可能在帧中间执行导致掉帧
> 3. 后台标签页中 `setTimeout` 继续执行（浪费资源），rAF 自动暂停（省电）
>
> ```javascript theme={null}
> // ❌ setTimeout 做动画——不准确
> setTimeout(() => {
>   element.style.transform = `translateX(${x}px)`;
>   x += 1;
>   if (x < 300) setTimeout(animate, 16); // 实际延迟 > 16ms
> }, 16);
>
> // ✅ rAF 做动画——与渲染同步
> function animate() {
>   element.style.transform = `translateX(${x}px)`;
>   x += 1;
>   if (x < 300) requestAnimationFrame(animate);
> }
> requestAnimationFrame(animate);
> ```

***

## Q6: 经典面试题：输出顺序是什么？

```javascript theme={null}
console.log("1");

setTimeout(() => console.log("2"), 0);

Promise.resolve()
  .then(() => console.log("3"))
  .then(() => console.log("4"));

new Promise((resolve) => {
  console.log("5");
  resolve();
}).then(() => console.log("6"));

console.log("7");
```

**逐步分析（带代码注释）：**

```javascript theme={null}
// ====== 第一步：执行同步代码（调用栈） ======
console.log("1"); // → 输出 1（同步）

setTimeout(
  () =>
    // → setTimeout 整体推入宏任务队列（不执行回调）
    console.log("2"),
  0,
);

Promise.resolve() // Promise.resolve() 立刻返回 fulfilled Promise
  .then(() =>
    // → .then 回调推入微任务队列 1
    console.log("3"),
  )
  .then(() =>
    // → 注意：这个 .then 还没注册，要等上面的微任务执行完才注册
    console.log("4"),
  );

new Promise((resolve) => {
  // → executor 是同步的！立刻执行！
  console.log("5"); // → 输出 5（同步，不是微任务！）
  resolve(); // → resolve 后，.then 回调推入微任务队列 2
}).then(() => console.log("6"));

console.log("7"); // → 输出 7（同步）
```

```text theme={null}
第一步执行完的状态：

调用栈：  已清空 ✅
输出：    1 → 5 → 7

微任务队列：[ log('3'), log('6') ]
宏任务队列：[ setTimeout(log('2')) ]

注意：log('4') 此时还没注册！它要等 log('3') 的微任务执行完才注册
      （因为 .then 返回新 Promise，第二个 .then 挂在新 Promise 上）
```

```javascript theme={null}
// ====== 第二步：清空微任务队列（按入队顺序逐个执行） ======
// 微任务 1：console.log('3') → 输出 3
//   └─ 执行完后，它的 .then(() => console.log('4')) 被注册为新的微任务 3
// 微任务 2：console.log('6') → 输出 6
// 微任务 3：console.log('4') → 输出 4（是微任务 1 执行时新产生的！）
//   └─ 如果此时又有新的微任务产生，继续清空——直到队列彻底为空

// ====== 第三步：执行宏任务（每次只取一个） ======
// 执行 setTimeout 回调：console.log('2') → 输出 2
// 执行完后回到第一步，开始下一轮循环（本题没有更多宏任务了）
```

```text theme={null}
最终输出顺序：1 → 5 → 7 → 3 → 6 → 4 → 2
               ├──同步──┤  ├─微任务─┤  ├宏任务┤
```

**解题口诀（逐行注释版）：**

```text theme={null}
遇到输出顺序题，按以下 4 步走：

第 1 步：从上到下扫描代码，标记每一行属于什么类型
  ├── console.log(...)         → 同步，立刻输出
  ├── setTimeout(...)          → 宏任务队列（只注册，不执行回调）
  ├── Promise.resolve().then() → 微任务队列（只注册回调）
  ├── new Promise(executor)    → executor 是同步的！立刻执行！
  └── await 右边的表达式       → 同步执行；await 下面的代码 → 微任务

第 2 步：执行完所有同步代码后，记录输出结果

第 3 步：清空微任务队列（包括微任务执行过程中新产生的微任务！）

第 4 步：取一个宏任务执行，执行完回到第 1 步（开始下一轮循环）
```

> [!TIP]
>
> 做输出顺序题就像分析一场话剧的彩排流程。同步代码是演员上台表演（立刻执行）。setTimeout 是"中场休息后再演的节目"（放入宏任务队列排队）。Promise.then 是"导演临时加的戏"（微任务，必须在休息前全部演完）。而 `new Promise(executor)` 里的代码是"开场白"——它是同步的，跟演员表演一样立刻执行，不是加戏。

> [!WARNING]
>
> `new Promise(executor)` 里的代码是**同步执行**的，只有 `.then` 回调才是微任务。很多人把 `console.log('5')` 当成微任务，这是最常见的错误。
>
> ```javascript theme={null}
> // ❌ 错误理解：以为 console.log('5') 是微任务
> new Promise((resolve) => {
>   console.log("5"); // 这是同步代码！不是微任务
>   resolve();
> }).then(() => console.log("6")); // 这才是微任务
> ```

### 进阶题：async/await 与 Event Loop 混战

这是各大厂出场率最高的一道终极输出题：

```javascript theme={null}
console.log("script start");

async function async1() {
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2 end");
}
async1();

setTimeout(function () {
  console.log("setTimeout");
}, 0);

new Promise((resolve) => {
  console.log("Promise");
  resolve();
})
  .then(function () {
    console.log("promise1");
  })
  .then(function () {
    console.log("promise2");
  });

console.log("script end");
```

**输出：** `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` 执行，不要按旧引擎的结论背成相反顺序。

> [card] 💡 破题心法: async/await 的本质
>
> 面试遇到 async/await 千万别慌，只要牢记这个核心转换公式：

### async/await 的本质

`async/await` 是 `Promise.then` 的语法糖。核心转换公式：

```javascript theme={null}
// async/await 写法
async function foo() {
  console.log("1");
  await bar();
  console.log("2"); // 这行变成微任务
}

// 等价于 Promise.then 写法
function foo() {
  console.log("1");
  bar().then(() => {
    console.log("2"); // 微任务
  });
}
```

**记住两点：**

1. `await` 右边的函数**立刻同步执行**
2. `await` 下面的代码被打包进**隐形的 `Promise.then()`**，变成微任务

### 输出顺序的解题框架

遇到输出顺序题，按以下步骤分析：

```text theme={null}
第一步：执行同步代码（调用栈）
  - 所有 console.log 立刻输出
  - setTimeout → 推入宏任务队列
  - Promise.resolve().then() → 推入微任务队列
  - new Promise(executor) → executor 是同步的！
  - await 右侧同步执行，下方代码推入微任务队列

第二步：清空微任务队列
  - 按顺序执行所有微任务
  - 如果微任务中产生新微任务，继续清空

第三步：执行宏任务
  - 每次只取一个宏任务执行
  - 执行完回到第一步
```

***

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

Node.js 的 Event Loop 有 **6 个阶段**，按顺序循环执行：

```text theme={null}
   ┌───────────────────────────┐
┌─>│         timers            │  ← setTimeout / setInterval
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │  ← 系统级回调（TCP 错误等）
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │  ← 内部使用
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │          poll             │  ← I/O 操作（文件读写、网络）
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │          check            │  ← setImmediate
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │      close callbacks      │  ← socket.on('close')
│  └───────────────────────────┘
```

> [!TIP]
>
> 浏览器的 Event Loop 像一条简单的环线公交——同步代码、微任务、渲染、宏任务，四个站循环跑。Node.js 的 Event Loop 则像一座大城市的地铁网络——有 6 条线路（timers、poll、check 等），每条线路负责不同类型的乘客（回调）。而且 Node.js 还有个"超级 VIP 通道"（process.nextTick），比普通 VIP（Promise.then）优先级更高，可以无视线路规则直接插队。

**关键区别：**

| 对比维度 | 浏览器 | Node.js |
| - | - | - |
| 微任务执行时机 | 每个宏任务后清空 | 每个宏任务后清空（11+ 对齐浏览器） |
| 微任务优先级 | Promise.then | process.nextTick > Promise.then |
| 宏任务类型 | setTimeout/setInterval/I/O | 6 个阶段，每种对应不同的宏任务 |
| 渲染 | 有（通常 16.67ms 一帧） | 无 |

```javascript theme={null}
// Node.js 经典题：setImmediate vs setTimeout(0)
setImmediate(() => console.log("immediate"));
setTimeout(() => console.log("timeout"), 0);
// 输出顺序不确定！取决于进入 event loop 的时机

// 但在 I/O 回调中，setImmediate 一定先执行
const fs = require("fs");
fs.readFile(__filename, () => {
  setImmediate(() => console.log("immediate")); // 先
  setTimeout(() => console.log("timeout"), 0); // 后
});
```

> [!WARNING]
>
> `process.nextTick` 属于 Node.js 自己的底层机制，存放在 `nextTickQueue` 中；而 `Promise.then` 属于 V8 引擎的机制，存放在 `microtaskQueue` 中。在进入每一个阶段之前，Node.js 都会强制优先清空 `nextTickQueue`，然后再把控制权交给 V8 清空 Promise 队列。
>
> ```javascript theme={null}
> // process.nextTick 永远快于 Promise.then
> process.nextTick(() => console.log("nextTick"));
> Promise.resolve().then(() => console.log("promise"));
>
> // 输出：nextTick → promise
> ```

### 6 个阶段的详细说明

| 阶段 | 说明 | 对应 API |
| - | - | - |
| **timers** | 执行到期的定时器回调 | `setTimeout`、`setInterval` |
| **pending callbacks** | 执行系统级回调 | TCP 错误等 |
| **idle, prepare** | 内部使用 | - |
| **poll** | 执行 I/O 回调，阻塞等待新 I/O 事件 | 文件读写、网络 |
| **check** | 执行 `setImmediate` 回调 | `setImmediate` |
| **close callbacks** | 执行 close 事件回调 | `socket.on('close')` |

```text theme={null}
   ┌───────────────────────────┐
┌─>│         timers            │  ← setTimeout / setInterval
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │  ← 系统级回调（TCP 错误等）
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │  ← 内部使用
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │          poll             │  ← I/O 操作（文件读写、网络）
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │          check            │  ← setImmediate
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │      close callbacks      │  ← socket.on('close')
│  └───────────────────────────┘
```

### setImmediate vs setTimeout(0)

```javascript theme={null}
// 在全局作用域下：顺序不确定
setImmediate(() => console.log("immediate"));
setTimeout(() => console.log("timeout"), 0);
// 取决于进入 event loop 的时机（>1ms 还是 <1ms）

// 但在 I/O 回调中：setImmediate 一定先执行
const fs = require("fs");
fs.readFile(__filename, () => {
  setImmediate(() => console.log("immediate")); // 先
  setTimeout(() => console.log("timeout"), 0); // 后
});
// 因为 I/O 回调在 poll 阶段执行，poll 结束后直接进入 check 阶段
```

> [!WARNING]
>
> 在 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) 不是立即执行？

```javascript theme={null}
// 浏览器规范规定：setTimeout 的最小延迟是 4ms
// 当嵌套层级超过 5 层时，delay 强制设为 4ms

setTimeout(() => {
  setTimeout(() => {
    setTimeout(() => {
      setTimeout(() => {
        setTimeout(() => {
          // 第 5 层嵌套，delay 被强制设为 4ms
        }, 0); // 实际延迟 4ms
      }, 0);
    }, 0);
  }, 0);
}, 0);
```

**为什么不是 0ms？有两个维度的原因：**

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

2. **Event Loop 机制**：即使没有嵌套，`setTimeout` 也只是把回调放入宏任务队列。如果当前调用栈很深，或者微任务队列里有海量任务，`setTimeout` 必须排队等待。所以 0ms 只是"最快在 0ms 后进入队列"，而不是"0ms 后执行"。

> [!TIP]
>
> `setTimeout(fn, 0)` 就像你去电影院买票，售票员说"你去 3 号厅等"（放入宏任务队列）。但你现在还在 1 号厅看电影（当前宏任务还没执行完），看完电影后还要去洗手间（清空微任务），然后才能去 3 号厅。所以"0ms 后去 3 号厅"不等于"0ms 后就到了 3 号厅"。

### 4ms 最小延迟的规范

HTML5 标准明确规定：当 `setTimeout` 的嵌套层级超过 5 层时，浏览器必须强制加上最小 4ms 的延迟。

在 Chromium 源码中，这个常量被定义为：

* `kMaxTimerNestingLevel = 5`
* `kMinimumInterval = 4ms`

```javascript theme={null}
// 嵌套层级与实际延迟
setTimeout(() => {
  setTimeout(() => {
    setTimeout(() => {
      setTimeout(() => {
        setTimeout(() => {
          // 第 5 层嵌套，delay 被强制设为 4ms
        }, 0); // 实际延迟 4ms
      }, 0);
    }, 0);
  }, 0);
}, 0);
```

### 嵌套层级超过 5 层的限制

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

```javascript theme={null}
// ❌ 无限嵌套 setTimeout——CPU 占用高
function loop() {
  setTimeout(loop, 0); // 嵌套层级不断增加，延迟从 0ms → 4ms
}

// ✅ 用 requestAnimationFrame 做动画——与渲染同步
function animate() {
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
```

***

## 底层源码：V8 与 Chromium 是如何实现的？

V8 的微任务队列是怎么实现的？

V8 的 `v8::internal::MicrotaskQueue` 用环形缓冲区（ring buffer）存放待执行的微任务，统一入口是 `MicrotaskQueue::RunMicrotasks()`，`Isolate::RunMicrotasks` 只是转发（详见 [V8 源码：src/execution/microtask-queue.cc](https://github.com/v8/v8/blob/main/src/execution/microtask-queue.cc)）：

```cpp theme={null}
class MicrotaskQueue {
  static const int kRingBufferOffset;
  static const int kCapacityOffset;
  static const int kSizeOffset;
  static const int kStartOffset;

  // 环形缓冲区：enqueue 时 size++，dequeue 时 start++
  void EnqueueMicrotask(Microtask* microtask);
  void RunMicrotasks(Isolate* isolate);
};

// RunMicrotasks 的核心逻辑：
// while (current_index < size_) {
//   microtask = buffer_[current_index++];
//   microtask->Run(isolate);
// }
// 循环执行直到所有微任务清空（包括新产生的微任务）
```

浏览器的 Event Loop 在 Blink 中怎么实现？

详见 [Blink 源码：third\_party/blink/renderer/platform/scheduler/](https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/scheduler/)：

```cpp theme={null}
// MessagePumpDefault::Run() 是真正的事件循环

// 5 种任务队列：
// 1. Input（用户输入，最高优先级）
// 2. Compositor（合成器，高优先级）
// 3. Default（普通任务）
// 4. Timer（定时器）
// 5. Idle（空闲任务，最低优先级）

// 关于每帧时间分配的一种常见经验估计（不是规范硬规则）：
// 约 5ms 给 JS 执行
// 约 11ms 给浏览器渲染
// JS 长任务会推迟渲染，但不存在"超过 5ms 就机械跳过整帧"的硬阈值
```

Node.js 的 Event Loop 在 libuv 中怎么实现？

Node.js 的 Event Loop 实现在 [`deps/uv/src/unix/core.c`](https://github.com/libuv/libuv/blob/v1.x/src/unix/core.c) 的 `uv_run` 函数中：

```c theme={null}
// uv_run 的简化逻辑
while (r != 0 && loop->stop_flag == 0) {
  uv__update_time(loop);           // 更新时间
  uv__run_timers(loop);            // 1. timers：执行到期的 setTimeout/setInterval
  ran_pending = uv__run_pending(loop); // 2. pending callbacks：执行已完成的 I/O 回调
  uv__run_idle(loop);              // 3. idle：内部使用
  uv__run_prepare(loop);           // 4. prepare：内部使用
  uv__io_poll(loop, timeout);      // 5. poll：阻塞等待 I/O 事件（核心）
  uv__run_check(loop);             // 6. check：执行 setImmediate
  uv__run_closing_handles(loop);   // 7. close callbacks：执行 close 事件
}
```

`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 得以非阻塞运行的底层基础。<sup>\[1]</sup>

> 浏览器端由 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 给渲染"只是经验值、并非规范硬规则，长任务会推迟渲染但不会超阈值机械跳过整帧，是国内理解浏览器帧调度机制最权威的参考资料。<sup>\[2]</sup>

> 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 队列。<sup>\[3]</sup>

> 实际项目中的建议：把长任务拆成小块（用 `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 实战参考。<sup>\[4]</sup>

### 常见追问

> [accordion] 微任务和宏任务的精确执行时机?
>
> **答题要点：**
> 不能只答"宏任务先执行，微任务后执行"，必须加上\*\*渲染（Render）\*\*的节点。
>
> **高分回答：**
> "在每一轮 Event Loop 中，主线程首先取出一个宏任务执行。该宏任务执行完毕后，会**立即锁定并清空当前所有的微任务队列**（包括在清空过程中新产生的微任务）。
>
> 微任务队列彻底清空后，浏览器会按帧判断是否需要进行 UI 渲染（60Hz 下通常约 16.67ms 一帧）。渲染完成后，再去任务队列取下一个 task。
> 所以时机是：**宏任务 → 全部微任务 → UI 渲染 → 下一个宏任务**。"

> [accordion] 为什么 setTimeout(fn, 0) 实际并不是 0ms?
>
> **答题要点：**
> 结合 HTML5 规范与系统底层解释。
>
> **高分回答：**
> "主要有两个维度的原因：
>
> 1. **规范限制**：HTML5 标准明确规定，当 `setTimeout` 的嵌套层级超过 5 层时，浏览器必须强制加上最小 4ms 的延迟。在 Chromium 源码中，这个常量被定义为 `kMaxTimerNestingLevel = 5` 和 `kMinimumInterval = 4ms`。
> 2. **Event Loop 机制**：即使没有嵌套，`setTimeout` 也只是把回调放入宏任务队列。如果当前执行上下文栈很深，或者微任务队列里有海量任务，`setTimeout` 必须排队等待。所以 0ms 只是'最快在 0ms 后进入队列'，而不是'0ms 后执行'。"

> [accordion] Node.js 中 setImmediate 和 setTimeout(0) 的顺序?
>
> **答题要点：**
> 必须分场景讨论！直接给确定答案的都是错的。
>
> **高分回答：**
> "分两种情况：
> **1. 在全局作用域（Main Module）下执行：顺序不确定。**
> 因为 Node.js 的启动耗时可能大于或小于 1ms（`setTimeout(0)` 在底层会被转化为 `setTimeout(1)`）。如果进入 Event Loop 时已经过了 1ms，Timer 阶段就会执行它；如果没到 1ms，就会跳过 Timer 阶段，在 Check 阶段先执行 `setImmediate`。
>
> **2. 在 I/O 回调（如 fs.readFile）中执行：setImmediate 绝对先执行。**
>
> ```javascript theme={null}
> const fs = require('fs');
> fs.readFile(__filename, () => {
>     setTimeout(() => console.log('timeout'), 0);
>     setImmediate(() => console.log('immediate')); // 永远先输出！
> });
> ```
>
> 因为 I/O 回调在 `poll` 阶段执行，`poll` 阶段结束后，紧接着就是 `check` 阶段（执行 `setImmediate`）。而 `setTimeout` 必须等到下一轮循环的 `timer` 阶段。"

> [accordion] 🔥 如果在微任务中不断无限递归产生新的微任务, 会怎样?
>
> **答题要点：**
> 对比宏任务递归，点出"阻塞渲染"的致命性。
>
> **高分回答：**
> "会导致**主线程彻底阻塞，页面无法点击，甚至连 UI 都无法重绘。**
> 因为 Event Loop 规定：必须把当前的微任务队列**彻底清空**，才能进入下一个阶段（渲染或下一个宏任务）。如果是无限递归产生微任务，这个队列就永远清不空，主线程就永远困在这一步。
>
> **对比宏任务：**
>
> ```javascript theme={null}
> // ❌ 微任务递归——页面阻塞
> function loop() {
>   Promise.resolve().then(loop);
> }
>
> // ✅ 宏任务递归——页面不阻塞
> function loop() {
>   setTimeout(loop, 0);
> }
> ```
>
> 如果用 `setTimeout` 无限递归产生宏任务，页面**不会阻塞**！因为每次执行完一个宏任务，主线程就有机会去执行渲染和响应用户输入。这也是为什么很多计算密集型任务（如大数组遍历）可以利用 `setTimeout` 或 `requestAnimationFrame` 进行'时间切片（Time Slicing）'来避免阻塞。"

> [accordion] 闭包和 Event Loop 有什么关系？
>
> **答题要点：**
> 闭包让回调函数能访问外部变量，这是 Event Loop 能正常工作的基础。
>
> **高分回答：**
> "闭包（Closure）和 Event Loop 的关系是：**闭包让异步回调函数能访问到正确的变量**。
>
> ```javascript theme={null}
> function fetchData(url) {
>   let data = null; // 这个变量被闭包"记住"了
>   fetch(url).then(response => {
>     data = response.json(); // 回调通过闭包访问 data
>   });
>   return function getData() {
>     return data; // 即使 fetchData 已执行完毕，仍能访问 data
>   };
> }
> ```
>
> 如果没有闭包，异步回调执行时，外部函数的变量已经被垃圾回收，回调就拿不到数据了。所以闭包是 Event Loop 异步编程的基础——它保证了回调函数在'稍后执行'时，仍能访问到正确的上下文。"

> [accordion] async/await 和 Promise.then 的执行顺序？
>
> **答题要点：**
> async/await 是 Promise.then 的语法糖，await 下方代码变成微任务。
>
> **高分回答：**
> "`async/await` 是 `Promise.then` 的语法糖，执行顺序完全一致：
>
> ```javascript theme={null}
> console.log('1');
> async function foo() {
>   console.log('2');
>   await Promise.resolve();
>   console.log('3'); // 微任务
> }
> foo();
> console.log('4');
>
> // 输出：1 → 2 → 4 → 3
> ```
>
> 关键点：
>
> 1. `await` 右边的函数**立刻同步执行**（输出 2）
> 2. `await` 下面的代码被打包进**隐形的 `Promise.then()`**（输出 3 是微任务）
> 3. 所以 `await` 下面的代码一定在当前同步代码之后执行（输出 4 在输出 3 之前）
>
> 如果有多个 `await`，每个 `await` 下面的代码都是独立的微任务，按顺序执行。"

> [accordion] requestAnimationFrame 是宏任务还是微任务？
>
> **答题要点：**
> rAF 既不是宏任务也不是微任务，它在微任务之后、渲染之前执行。
>
> **高分回答：**
> "`requestAnimationFrame` 严格说**既不是宏任务也不是微任务**——它是一个独立的阶段，在微任务之后、浏览器渲染之前执行。
>
> 执行顺序：同步代码 → 微任务 → **rAF** → 浏览器渲染 → 宏任务
>
> 从 Blink 源码看，`requestAnimationFrame` 注册的回调在 `BeginMainFrame` 的 `Animate` 步骤中执行。这意味着：
>
> 1. rAF 回调一定在当前帧的渲染前执行
> 2. rAF 回调中的 DOM 修改会被当前帧渲染
> 3. 如果在 rAF 中修改 `transform`/`opacity`，只需要 Composite，不需要 Layout 和 Paint
>
> ```javascript theme={null}
> // rAF 保证在渲染前执行
> requestAnimationFrame(() => {
>   element.style.transform = 'translateX(100px)'; // 当前帧渲染
> });
> ```

> [accordion] 🔥 如何实现一个时间切片（Time Slicing）？
>
> **答题要点：**
> 用 setTimeout 或 requestAnimationFrame 把长任务拆成小块，每块执行完让出主线程。
>
> **高分回答：**
> "时间切片的核心思想是：**把一个长任务拆成多个小任务，每个小任务执行完后让出主线程，让浏览器有机会渲染和响应用户输入。**
>
> ```javascript theme={null}
> // ❌ 长任务——阻塞渲染 3 秒
> function processLargeArray(array) {
>   for (const item of array) {
>     heavyComputation(item); // 假设每次需要 1ms
>   }
> }
>
> // ✅ 时间切片——每处理 100 个让出主线程
> function processLargeArray(array, chunkSize = 100) {
>   let index = 0;
>   function processChunk() {
>     const end = Math.min(index + chunkSize, array.length);
>     for (; index < end; index++) {
>       heavyComputation(array[index]);
>     }
>     if (index < array.length) {
>       setTimeout(processChunk, 0); // 让出主线程
>     }
>   }
>   processChunk();
> }
> ```
>
> **更优雅的方案：用 `requestIdleCallback`**
>
> ```javascript theme={null}
> function processLargeArray(array) {
>   let index = 0;
>   function processChunk(deadline) {
>     while (deadline.timeRemaining() > 1 && index < array.length) {
>       heavyComputation(array[index++]);
>     }
>     if (index < array.length) {
>       requestIdleCallback(processChunk);
>     }
>   }
>   requestIdleCallback(processChunk);
> }
> ```
>
> `requestIdleCallback` 会在浏览器空闲时执行，不会阻塞渲染。但兼容性不如 `setTimeout`。"

***

## 延伸阅读与引用参考

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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.