JavaScript 事件循环详解:异步、回调、Promise 与微任务
事件循环是 JavaScript 并发模型的核心。理解它就能明白为什么 setTimeout(fn, 0) 不会立即执行,为什么 Promise 的行为如此,以及如何编写真正非阻塞的代码。
JavaScript 是单线程的
JavaScript 只有一个主线程,一次只能做一件事。但它通过事件驱动、非阻塞的模型可以处理数千个并发操作。

关键组件
┌─────────────────────────────────────────────┐
│ 调用栈 │
│ (执行同步代码,LIFO 顺序) │
├─────────────────────────────────────────────┤
│ Web APIs / Node APIs │
│ (setTimeout, fetch, fs.readFile 等) │
├──────────────────────┬──────────────────────┤
│ 微任务队列 │ 任务队列 │
│ (Promise, queueMicrotask, MutationObserver)│ (setTimeout, setInterval, I/O) │
└──────────────────────┴──────────────────────┘
↑ 事件循环从这里取出
优先级顺序:
- 调用栈 运行直到为空
- 微任务队列 完全清空(所有微任务)
- 一个任务 从任务队列取出
- 重复
逐步示例
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
为什么?
1和4同步执行(调用栈)- 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

为什么这在实践中很重要
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' 之前运行
}

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 测量异步操作性能。