事件循环机制
2026/3/25大约 7 分钟
事件循环机制
什么是事件循环
JavaScript 是一门单线程、非阻塞的语言。事件循环(Event Loop)是 JavaScript 实现异步编程的核心机制,它负责协调主线程执行同步代码和管理异步任务的执行顺序。
为什么需要事件循环
JavaScript 只有一个主线程,同步代码会阻塞主线程。事件循环的作用是在主线程空闲时,从任务队列中取出任务执行,从而实现「非阻塞」的特性。
执行栈和任务队列
执行栈(Call Stack)
执行栈是一个后进先出(LIFO)的数据结构,用于存储代码执行时的上下文。
function fn1() {
console.log("fn1 开始");
fn2();
console.log("fn1 结束");
}
function fn2() {
console.log("fn2 执行");
}
fn1();
console.log("全局执行完成");执行过程:
- 全局代码进入执行栈
- fn1 进入执行栈 → 输出 "fn1 开始"
- fn2 进入执行栈 → 输出 "fn2 执行"
- fn2 执行完毕出栈
- fn1 继续执行 → 输出 "fn1 结束"
- fn1 出栈 → 输出 "全局执行完成"
任务队列(Task Queue)
任务队列是一个先进先出(FIFO)的数据结构,用于存储待执行的异步回调。JavaScript 中有多种任务队列:
- 宏任务队列(Macro Task Queue):setTimeout、setInterval、I/O、UI Rendering
- 微任务队列(Micro Task Queue):Promise、MutationObserver、queueMicrotask
事件循环的执行过程
完整流程图
┌─────────────────────────────────────────────────────────────────┐
│ 执行栈 (Call Stack) │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ │ │ │ │ │ │ │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ │
└───────────────────────────────┬─────────────────────────────────┘
│
▼
┌───────────────────────┐
│ 检查微任务队列 │
│ (Micro Task Queue) │
│ Promise, async/await │
└───────────┬───────────┘
│ 有微任务?
┌───────────┴───────────┐
│ │ │
是 否 是
│ │ │
▼ ▼ ▼
执行所有微 检查宏任务 执行一个宏任务
任务完毕 队列为空? 后回到微任务
│ │
└──────────┬───────────┘
│
▼
┌───────────────────────┐
│ 检查宏任务队列 │
│ (Macro Task Queue) │
│ setTimeout, setInterval│
└───────────┬───────────┘
│ 有宏任务?
┌───────────┴───────────┐
│ │ │
是 否 是
│ │ │
▼ ▼ ▼
执行一个 等待新任务 执行一个宏任务
宏任务 产生时加入 后回到微任务
│ │ │
└──────────┴───────────┘
│
▼
回到「检查微任务队列」执行步骤详解
- 同步代码优先执行:执行栈中的同步代码会优先执行
- 清空微任务队列:每次事件循环开始时,会先执行完所有微任务
- 执行宏任务:微任务队列清空后,执行一个宏任务
- 再次检查微任务:宏任务执行完后,再次检查微任务队列
- 重复循环:不断重复步骤 2-4
宏任务与微任务
宏任务(Macro Task)
| API | 说明 | 优先级 |
|---|---|---|
| script(整体代码) | 整体 script 标签代码 | 最高 |
| setTimeout | 定时器 | 低 |
| setInterval | 间隔定时器 | 低 |
| I/O | 输入输出操作 | 中 |
| UI Rendering | UI 渲染 | 中 |
| setImmediate | Node.js 专用 | 中 |
微任务(Micro Task)
| API | 说明 |
|---|---|
| Promise.then/catch/finally | Promise 回调 |
| async/await | async 函数的 await 后面的代码 |
| queueMicrotask | 手动添加微任务 |
| MutationObserver | DOM 变化监听 |
| process.nextTick | Node.js 专用 |
经典实例讲解
实例 1:基础事件循环
console.log("1"); // 同步代码
setTimeout(() => {
console.log("2");
}, 0); // 宏任务
Promise.resolve().then(() => {
console.log("3");
}); // 微任务
console.log("4"); // 同步代码执行顺序:
输出: 1 → 4 → 3 → 2分析:
- 首先执行同步代码:输出 1,输出 4
- 微任务 Promise.then 加入微任务队列
- setTimeout 加入宏任务队列
- 同步代码执行完毕,检查微任务队列
- 执行微任务:输出 3
- 微任务队列清空,执行宏任务:输出 2
实例 2:嵌套的 Promise
console.log("1");
Promise.resolve()
.then(() => {
console.log("2");
Promise.resolve().then(() => {
console.log("3");
});
})
.then(() => {
console.log("4");
});
console.log("5");执行顺序:
输出: 1 → 5 → 2 → 3 → 4分析:
- 输出 1
- Promise.resolve() 立即 resolved,第一个 .then 加入微任务队列
- 输出 5(同步代码)
- 执行第一个微任务:输出 2
- 嵌套的 Promise.resolve() 的 .then 加入微任务队列
- 执行第二个微任务:输出 3
- 第一个 .then 的后续 .then 加入微任务队列
- 执行第三个微任务:输出 4
实例 3:async/await
console.log("1");
async function asyncFn() {
console.log("2");
await console.log("3");
console.log("4");
}
asyncFn();
console.log("5");执行顺序:
输出: 1 → 2 → 3 → 5 → 4分析:
- 输出 1
- 调用 asyncFn,进入执行栈,输出 2
- 执行
await console.log('3'),这是一个微任务 - asyncFn 暂停,asyncFn 出栈
- 输出 5(同步代码)
- 检查并执行微任务:输出 4
实例 4:async/await 详细解析
async function async1() {
console.log("1");
await async2();
console.log("2");
}
async function async2() {
console.log("3");
}
console.log("4");
setTimeout(() => {
console.log("5");
}, 0);
async1();
new Promise((resolve) => {
console.log("6");
resolve();
}).then(() => {
console.log("7");
});
console.log("8");执行顺序:
输出: 4 → 1 → 3 → 6 → 8 → 7 → 2 → 5分析:
- 输出 4(同步代码)
- 调用 async1,进入执行栈
- 输出 1
- 执行 await async2:调用 async2
- async2 中输出 3,async2 执行完毕
- await 后的代码(console.log('2'))作为微任务加入队列
- async1 出栈
- Promise 立即执行,输出 6
- .then 加入微任务队列
- 输出 8(同步代码)
- 执行微任务队列:
- 输出 7(Promise.then)
- 输出 2(async 中的 await 后代码)
- 执行宏任务队列:
- 输出 5(setTimeout)
实例 5:setTimeout 嵌套
setTimeout(() => {
console.log("1");
}, 0);
console.log("2");
new Promise((resolve) => {
resolve();
}).then(() => {
console.log("3");
});
console.log("4");执行顺序:
输出: 2 → 4 → 3 → 1实例 6:综合练习
console.log("A");
setTimeout(() => {
console.log("B");
}, 0);
new Promise((resolve, reject) => {
console.log("C");
resolve();
}).then(() => {
console.log("D");
});
console.log("E");执行顺序:
输出: A → C → E → D → B分析:
- 输出 A
- setTimeout 加入宏任务队列
- 执行 Promise,输出 C
- .then 加入微任务队列
- 输出 E
- 执行微任务:输出 D
- 执行宏任务:输出 B
实例 7:多次事件循环
setTimeout(() => {
console.log("1");
}, 0);
setTimeout(() => {
console.log("2");
}, 0);
console.log("3");执行顺序:
输出: 3 → 1 → 2分析:
- 两个 setTimeout 加入宏任务队列
- 输出 3
- 执行第一个宏任务:输出 1
- 执行第二个宏任务:输出 2
实例 8:queueMicrotask
console.log("1");
queueMicrotask(() => {
console.log("2");
});
console.log("3");执行顺序:
输出: 1 → 3 → 2分析:
- 输出 1
- queueMicrotask 加入微任务队列
- 输出 3
- 执行微任务:输出 2
实例 9:Promise 链式调用
console.log("1");
new Promise((resolve) => {
resolve();
})
.then(() => {
console.log("2");
return "a";
})
.then((val) => {
console.log("3", val);
return "b";
})
.then((val) => {
console.log("4", val);
});
console.log("5");执行顺序:
输出: 1 → 5 → 2 → 3 a → 4 b实例 10:综合面试题
async function async1() {
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
new Promise((resolve) => {
console.log("3");
resolve();
}).then(() => {
console.log("4");
});
console.log("5");
}
console.log("6");
async1();
console.log("7");执行顺序:
输出: 6 → 1 → 3 → 5 → 7 → 4 → 2分析:
- 输出 6
- 调用 async1,输出 1
- setTimeout 加入宏任务队列
- Promise 立即执行,输出 3
- .then 加入微任务队列
- 输出 5
- async1 执行完毕,出栈
- 输出 7(同步代码)
- 执行微任务:输出 4
- 执行宏任务:输出 2
Node.js 中的事件循环
Node.js 中的事件循环与浏览器有所不同,它有多个阶段(phase)。
Node.js 事件循环阶段
┌───────────────────────────┐
│ timers │ ← setTimeout, setInterval
│ (执行定时器回调) │
└────────────┬────────────┘
│
┌────────────▼────────────┐
│ pending callbacks │ ← 延迟的 I/O 回调
└────────────┬────────────┘
│
┌────────────▼────────────┐
│ idle, prepare │ ← 内部使用
└────────────┬────────────┘
│
┌────────────▼────────────┐
│ poll │ ← I/O 回调,获取新的 I/O 事件
│ (轮询阶段) │ 执行几乎所有回调,除了 close callbacks 和 timers
└────────────┬────────────┘
│
┌────────────▼────────────┐
│ check │ ← setImmediate 回调
└────────────┬────────────┘
│
┌────────────▼────────────┐
│ close callbacks │ ← socket.close 回调
└────────────┬────────────┘
│
▼
┌───────────────────────────┐
│ 执行微任务队列 │ ← process.nextTick, Promise
└────────────┬────────────┘
│
▼
回到 timers 阶段Node.js 特有 API
setImmediate
setImmediate(() => {
console.log("setImmediate");
});
setTimeout(() => {
console.log("setTimeout");
}, 0);在 Node.js 中,setTimeout 和 setImmediate 的执行顺序不确定,取决于系统性能。
process.nextTick
process.nextTick(() => {
console.log("nextTick");
});
Promise.resolve().then(() => {
console.log("Promise");
});执行顺序:
输出: nextTick → Promiseprocess.nextTick 的优先级高于 Promise。
总结
事件循环核心要点
- 同步代码优先执行:主线程会先执行完所有同步代码
- 清空微任务队列:每次事件循环开始时,会执行完所有微任务
- 一个宏任务执行后检查微任务:宏任务执行完后,立即检查并执行所有微任务
- 不断循环:重复「清空微任务 → 执行一个宏任务」的过程
- UI 渲染时机:在微任务队列清空后、宏任务执行前可能触发渲染
记忆口诀
先同步,后微任务,最后宏任务,一个一个来
常见考点
- Promise vs setTimeout 的执行顺序
- async/await 的本质是 Promise + 事件循环
- async 函数中的 await 会阻塞后续代码执行
- Node.js 的 process.nextTick 优先级高于 Promise
- 事件循环是 JavaScript 实现异步的基础