正在加载,请稍候…

JavaScript 事件循环详解:异步、回调、Promise 与微任务

理解 JavaScript 事件循环、调用栈、任务队列和微任务队列,掌握异步代码的执行机制,编写非阻塞的 JavaScript 代码。

JavaScript 事件循环详解:异步、回调、Promise 与微任务

JavaScript 事件循环详解:异步、回调、Promise 与微任务

事件循环是 JavaScript 并发模型的核心。理解它就能明白为什么 setTimeout(fn, 0) 不会立即执行,为什么 Promise 的行为如此,以及如何编写真正非阻塞的代码。

JavaScript 是单线程的

JavaScript 只有一个主线程,一次只能做一件事。但它通过事件驱动、非阻塞的模型可以处理数千个并发操作。

JavaScript 事件循环详解:异步、回调、Promise 与微任务 插图

关键组件

┌─────────────────────────────────────────────┐
│                  调用栈                      │
│  (执行同步代码,LIFO 顺序)                    │
├─────────────────────────────────────────────┤
│             Web APIs / Node APIs             │
│  (setTimeout, fetch, fs.readFile 等)         │
├──────────────────────┬──────────────────────┤
│   微任务队列          │      任务队列         │
│  (Promise, queueMicrotask, MutationObserver)│  (setTimeout, setInterval, I/O) │
└──────────────────────┴──────────────────────┘
              ↑ 事件循环从这里取出

优先级顺序

  1. 调用栈 运行直到为空
  2. 微任务队列 完全清空(所有微任务)
  3. 一个任务 从任务队列取出
  4. 重复

逐步示例

console.log('1'); // 同步 — 立即进入调用栈

setTimeout(() => {
  console.log('2 - setTimeout'); // 异步 — 0ms 后进入任务队列
}, 0);

Promise.resolve().then(() => {
  console.log('3 - Promise'); // 异步 — 进入微任务队列
});

console.log('4'); // 同步

// 输出顺序:1, 4, 3 - Promise, 2 - setTimeout

为什么?

  • 14 同步执行(调用栈)
  • Promise 回调进入 微任务队列(更高优先级)
  • setTimeout 回调进入 任务队列(较低优先级)
  • 微任务在下一个任务队列项之前全部清空

详细执行模型

console.log('start'); // 1

setTimeout(() => console.log('timeout 1'), 0); // 任务队列

Promise.resolve()
  .then(() => {
    console.log('promise 1'); // 微任务
    return 'value';
  })
  .then(() => {
    console.log('promise 2'); // 微任务(链式)
  });

queueMicrotask(() => console.log('microtask')); // 微任务

setTimeout(() => console.log('timeout 2'), 0); // 任务队列

console.log('end'); // 2

// 输出:
// start
// end
// promise 1
// promise 2
// microtask
// timeout 1
// timeout 2

JavaScript 事件循环详解:异步、回调、Promise 与微任务 插图

为什么这在实践中很重要

UI 渲染

// ❌ 冻结 UI — 阻塞循环
for (let i = 0; i < 10_000_000; i++) {
  heavyWork(i);
}
// 浏览器在此期间无法渲染或响应输入!

// ✅ 在块之间让出给浏览器
async function processInChunks(data) {
  for (let i = 0; i < data.length; i++) {
    processItem(data[i]);
    
    // 每 1000 项,让出以允许渲染
    if (i % 1000 === 0) {
      await new Promise(resolve => setTimeout(resolve, 0));
    }
  }
}

setTimeout(fn, 0) 不是立即执行

// setTimeout(fn, 0) 不会立即执行
// 它在以下之后运行:
// 1. 当前同步代码完成
// 2. 所有微任务清空
// 3. 然后 setTimeout 回调运行(作为任务)

function example() {
  setTimeout(() => console.log('later'), 0);
  
  // 即使 100 万次迭代也在 setTimeout 之前运行
  for (let i = 0; i < 1_000_000; i++) {
    // 全部同步
  }
  
  console.log('sync done'); // 在 'later' 之前运行
}

JavaScript 事件循环详解:异步、回调、Promise 与微任务 插图

Promise 微任务饥饿

// ⚠️ 无限微任务使任务队列饥饿
function infiniteMicrotasks() {
  Promise.resolve().then(infiniteMicrotasks); // 无限链!
  // setTimeout 回调永远不会运行 — 微任务总是先清空
}

// ✅ 使用 setImmediate / setTimeout 让出给任务
async function safeLoop() {
  while (true) {
    await new Promise(resolve => setTimeout(resolve, 0)); // 让出给任务队列
    processNextItem();
  }
}

Async/Await 底层原理

// async/await 是 Promise 的语法糖
async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`); // 在此暂停
  const data = await response.json();                  // 在此暂停
  return data;
}

// 等效的 Promise 链:
function fetchUser(id) {
  return fetch(`/api/users/${id}`)          // 微任务
    .then(response => response.json())         // 微任务
    .then(data => data);
}

// 两者底层工作方式相同:
// 1. fetch() 发起 I/O(进入 Web APIs)
// 2. 当 fetch 解析时,.then() 回调作为微任务排队
// 3. 执行在 .then() 处恢复

Node.js 事件循环(略有不同)

Node.js 有额外的阶段:

┌───────────────┐
│   timers      │  ← setTimeout, setInterval 回调
├───────────────┤
│ pending cbs   │  ← I/O 错误回调
├───────────────┤
│   idle/prep   │  ← 内部使用
├───────────────┤
│     poll      │  ← 检索新的 I/O 事件(大多数工作在此发生)
├───────────────┤
│    check      │  ← setImmediate 回调
├───────────────┤
│ close events  │  ← socket.on('close') 等
└───────────────┘
       ↑
  每个阶段清空其队列,然后微任务在阶段之间运行
// Node.js: setImmediate vs setTimeout(fn, 0)
setImmediate(() => console.log('setImmediate'));    // 在 'check' 阶段运行
setTimeout(() => console.log('setTimeout'), 0);    // 在 'timers' 阶段运行
process.nextTick(() => console.log('nextTick'));    // 在所有之前运行(next tick 队列)

// 顺序通常是:nextTick → setTimeout 或 setImmediate(顺序不定)

常见异步陷阱

// ❌ 顺序 await 但可并行
async function loadPage() {
  const user = await fetchUser();         // 等待 200ms
  const posts = await fetchPosts();       // 在 user 之后等待 200ms
  const ads = await fetchAds();           // 在 posts 之后等待 200ms
  // 总计:~600ms
}

// ✅ 使用 Promise.all 并行
async function loadPageFast() {
  const [user, posts, ads] = await Promise.all([
    fetchUser(),   // 所有同时启动
    fetchPosts(),
    fetchAds(),
  ]);
  // 总计:~200ms(仅最慢的那个)
}

// ✅ Promise.allSettled — 一个拒绝不会导致失败
const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()]);
results.forEach(result => {
  if (result.status === 'fulfilled') console.log(result.value);
  if (result.status === 'rejected') console.error(result.reason);
});

心智模型

把它想象成一家餐厅:

  • 厨师 = JavaScript 引擎(单线程,做实际工作)
  • 订单 = 同步代码(厨师一次处理一个)
  • VIP 订单 = 微任务(在常规订单之间处理,总是优先)
  • 常规订单 = 任务(setTimeout, I/O 回调 — 在 VIP 之后处理)
  • 准备工作 = Web APIs(在后台进行,不是由厨师)

→ 使用 Benchmark Builder 测量异步操作性能。