glownight

返回

一、为什么需要事件循环?#

JavaScript 是单线程语言,意味着它一次只能执行一个任务。如果所有操作都是同步的,一个耗时的网络请求就会阻塞整个页面,导致界面”卡死” 。

事件循环(Event Loop)就是 JavaScript 处理异步任务的调度机制。它让 JS 能够在等待 I/O 操作(网络请求、定时器、用户输入)的同时,继续执行其他代码,实现非阻塞的并发效果 。


二、核心组件架构#

┌─────────────────────────────────────────────────────────────┐
│                      JavaScript 引擎                         │
│  ┌──────────────┐                                          │
│  │  调用栈       │  ← 同步代码在这里执行(LIFO:后进先出)      │
│  │ Call Stack   │                                          │
│  └──────┬───────┘                                          │
│         │ 同步代码执行完毕                                   │
│         ▼                                                  │
│  ┌──────────────┐  ┌─────────────────┐                    │
│  │  微任务队列    │  │   宏任务队列      │                    │
│  │ Microtask    │  │  Macrotask      │                    │
│  │  (VIP 通道)   │  │  (普通通道)       │                    │
│  │              │  │                 │                    │
│  │ Promise.then │  │  setTimeout     │                    │
│  │ queueMicrotask│  │  setInterval    │                    │
│  │ MutationObserver│ │  DOM 事件       │                    │
│  └──────┬───────┘  └────────┬────────┘                    │
│         │                     │                            │
│         └─────────┬───────────┘                            │
│                   │                                         │
│              事件循环(Event Loop)                          │
│              不断检查:调用栈空了吗?微任务队列有任务吗?        │
└─────────────────────────────────────────────────────────────┘


┌─────────────────┐
│   Web APIs      │  ← 浏览器/Node.js 提供的异步能力
│  (定时器、网络请求)│
└─────────────────┘
plaintext

三、执行顺序铁律#

事件循环遵循严格的优先级规则 :

1. 执行同步代码(全部压入调用栈执行)
2. 调用栈清空后,检查微任务队列 → 执行所有微任务
3. 微任务队列清空后,执行一个宏任务
4. 回到步骤 2,循环往复
plaintext

关键规则

  • 微任务 = VIP 通道:每次调用栈清空后,先清空全部微任务队列,才会执行下一个宏任务
  • 宏任务 = 普通通道:每次只执行一个,然后回到检查微任务
  • 微任务可以插队:即使宏任务已经排队,新加入的微任务也会优先执行

四、任务类型分类#

类型包含内容优先级
同步任务普通 JavaScript 代码最高,立即执行
微任务(Microtask)Promise.then/catch/finallyqueueMicrotask()MutationObserver高,当前宏任务结束后立即执行
宏任务(Macrotask)setTimeoutsetIntervalsetImmediate(Node)、I/O 操作、UI 渲染低,每次循环执行一个
渲染浏览器重绘/重排微任务之后、下一个宏任务之前

五、经典代码分析#

基础示例:Promise vs setTimeout#

console.log('1. Start');

setTimeout(() => {
  console.log('4. Timeout');  // 宏任务
}, 0);

Promise.resolve().then(() => {
  console.log('3. Promise');  // 微任务
});

console.log('2. End');
javascript

输出顺序1 → 2 → 3 → 4

执行过程解析

步骤 1: console.log('1. Start')  → 同步,立即输出 "1. Start"
步骤 2: setTimeout(..., 0)       → 交给 Web API,0ms 后回调进入宏任务队列
步骤 3: Promise.resolve().then() → Promise 回调进入微任务队列
步骤 4: console.log('2. End')    → 同步,立即输出 "2. End"
       ─── 同步代码执行完毕,调用栈清空 ───
步骤 5: 检查微任务队列 → 有 Promise 回调 → 输出 "3. Promise"
步骤 6: 微任务队列已空 → 检查宏任务队列 → 输出 "4. Timeout"
plaintext

为什么 setTimeout(0) 比 Promise 慢? 因为 Promise 回调进入微任务队列(VIP 通道),而 setTimeout 进入宏任务队列(普通通道)。微任务总是优先执行 。


进阶示例:嵌套微任务#

console.log('A');

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

Promise.resolve()
  .then(() => {
    console.log('D');
    setTimeout(() => console.log('E'), 0);
    return Promise.resolve();
  })
  .then(() => console.log('F'));

setTimeout(() => console.log('G'), 0);

Promise.resolve().then(() => {
  console.log('H');
  Promise.resolve().then(() => console.log('I'));
});

console.log('J');
javascript

输出顺序A → J → D → H → F → I → B → C → G → E

解析

第 1 轮:
  同步:A, J
  微任务队列:Promise1(D→F), Promise2(H→I) → 输出 D, H, F, I
  宏任务队列:setTimeout1(B→C), setTimeout2(G), setTimeout3(E)

第 2 轮(取第一个宏任务 setTimeout1):
  执行 B,产生新微任务 C
  微任务队列:C → 输出 C
  宏任务队列:setTimeout2(G), setTimeout3(E)

第 3 轮(取 setTimeout2):
  输出 G

第 4 轮(取 setTimeout3):
  输出 E
plaintext

六、async/await 与事件循环#

async/await 是 Promise 的语法糖,本质上还是通过微任务实现 。

console.log('1');

async function foo() {
  console.log('2');
  await Promise.resolve();  // await 后面的代码变为微任务
  console.log('3');         // 进入微任务队列
}

foo();
console.log('4');

// 输出:1 → 2 → 4 → 3
javascript

解析

  • await 之前的代码同步执行(输出 2)
  • await 表达式返回的 Promise 被挂起
  • await 之后的代码被包装成微任务,等待当前同步代码执行完毕后执行

七、Node.js 的特殊情况#

Node.js 的事件循环比浏览器更复杂,分为 6 个阶段 :

阶段作用
timers执行 setTimeout/setInterval 回调
pending callbacks执行系统操作的回调(如 TCP 错误)
idle, prepareNode 内部使用
poll获取新的 I/O 事件,执行 I/O 回调
check执行 setImmediate 回调
close callbacks执行 close 事件回调

Node.js 特有的”超级 VIP”

process.nextTick(() => {
  console.log('nextTick');  // 比 Promise 还快!
});

Promise.resolve().then(() => {
  console.log('Promise');
});

// 输出:nextTick → Promise
javascript

⚠️ process.nextTick 不属于事件循环的正式阶段,它在当前操作完成后立即执行,如果滥用可能导致 I/O 饥饿 。


八、常见面试题#

题目 1#

console.log('1');

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

Promise.resolve().then(() => {
  console.log('3');
  return Promise.resolve();
}).then(() => console.log('4'));

console.log('5');
javascript

答案1 → 5 → 3 → 4 → 2

题目 2#

async function async1() {
  console.log('async1 start');
  await async2();
  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('promise1');
  resolve();
}).then(() => console.log('promise2'));
console.log('script end');
javascript

答案script start → async1 start → async2 → promise1 → script end → async1 end → promise2 → setTimeout


九、性能优化建议#

1. 避免微任务过多导致阻塞#

// ❌ 危险:微任务无限递归,阻塞宏任务和渲染
function loop() {
  Promise.resolve().then(loop);
}
loop();  // 页面卡死,setTimeout 永远不会执行
javascript

2. 大量数据处理使用宏任务分片#

// ❌ 阻塞主线程
items.forEach(item => heavyComputation(item));

// ✅ 使用 setTimeout 分片,让 UI 有机会渲染
async function processData(items) {
  for (let item of items) {
    heavyComputation(item);
    if (needToYield()) {
      await new Promise(resolve => setTimeout(resolve, 0));
    }
  }
}
javascript

3. 优先使用微任务处理紧急更新#

// 推荐:使用 queueMicrotask 或 Promise 处理需要尽快执行的逻辑
queueMicrotask(() => {
  // 比 setTimeout(..., 0) 更快执行
});
javascript

十、核心口诀#

同步代码最先走,微任务排队 VIP,
宏任务一个一个来,渲染穿插在中间。
await 挂起变微任务,nextTick 是超级 VIP。
plaintext

理解事件循环是掌握 JavaScript 异步编程的关键。记住:微任务优先于宏任务,同步优先于异步,await 后面的代码是微任务

事件循环
作者 glownight
发布于 2026年4月27日