在 JavaScript 中,事件循环(Event Loop) 是 JS 引擎实现异步编程的核心机制 —— 由于 JS 是单线程语言(同一时间只能执行一个任务),事件循环通过 “任务队列” 调度同步 / 异步任务的执行顺序,让 JS 既能保持单线程的简单性,又能处理异步操作(如网络请求、定时器、DOM 事件)。
一、核心前提:JS 的单线程与任务分类
1. 单线程的本质
JS 设计为单线程(避免多线程操作 DOM 导致冲突),但单线程的问题是:若所有任务同步执行,耗时操作(如网络请求)会阻塞后续代码,导致页面卡死。因此 JS 将任务分为两类:
2. 任务分类(关键)
| 任务类型 | 执行时机 | 包含典型操作 |
|---|---|---|
| 同步任务 | 立即执行,进入 “执行栈” | 变量声明、函数调用、基本运算等同步代码 |
| 异步任务 | 不立即执行,进入 “任务队列” | 又分为「宏任务(Macrotask)」和「微任务(Microtask)」,异步操作完成后进入对应队列 |
(1)宏任务(Macrotask)
- 特点:执行优先级低,每次事件循环仅执行一个宏任务,执行完后清空所有微任务;
- 常见类型:
- 全局代码(
script标签) - 定时器(
setTimeout/setInterval/setImmediate) - DOM 事件(
click/resize等) - 网络请求回调(
fetch/XMLHttpRequest) requestAnimationFrame(浏览器专属)- I/O 操作(Node.js 专属)
- 全局代码(
(2)微任务(Microtask)
- 特点:执行优先级高,宏任务执行后立即清空所有微任务(微任务队列空了才会执行下一个宏任务);
- 常见类型:
Promise.then/catch/finallyasync/await(本质是 Promise 语法糖,await 后的代码属于微任务)queueMicrotask()(手动添加微任务)MutationObserver(浏览器专属)process.nextTick(Node.js 专属,优先级高于普通微任务)
二、事件循环的执行流程(浏览器环境)
事件循环的核心是 “执行栈 + 宏任务队列 + 微任务队列” 的协作,流程可拆解为 5 步:
1 | 1. 执行全局同步代码(属于第一个宏任务),同步任务进入执行栈,执行完毕出栈; |
直观示例(浏览器):
1 | console.log("同步1"); // 同步任务,立即执行 |
三、关键细节:微任务的 “插队” 特性
微任务的优先级远高于宏任务,且微任务队列会被一次性清空(包括执行微任务时新增的微任务),这是事件循环的核心考点:
示例:微任务嵌套微任务
1 | console.log("同步"); |
解析:
- 同步代码执行完,先清空微任务队列 → 执行 “微任务 1”;
- “微任务 1” 中新增的 “宏任务 2” 进入宏任务队列;
- 微任务队列空后,执行下一个宏任务 “宏任务 1”;
- “宏任务 1” 执行完,立即清空其产生的微任务 “微任务 2”;
- 最后执行 “宏任务 2”。
四、浏览器 vs Node.js 事件循环的差异
Node.js 也实现了事件循环,但针对服务端场景做了调整,核心差异如下:
| 特性 | 浏览器事件循环 | Node.js 事件循环 |
|---|---|---|
| 宏任务执行顺序 | 无细分,按队列顺序执行 | 分 6 个阶段(timers → I/O → idle → poll → check → close),按阶段执行 |
| 微任务优先级 | 普通微任务(Promise)优先 | process.nextTick > 普通微任务(Promise) |
| 触发时机 | 宏任务执行后 → 微任务 → 渲染 | 每个阶段执行完 → 清空微任务队列 → 进入下一个阶段 |
Node.js 事件循环的核心阶段(简化):
- timers:执行
setTimeout/setInterval回调; - I/O callbacks:执行 I/O 操作(如文件、网络)的回调;
- idle/prepare:内部阶段,忽略;
- poll:等待新的 I/O 事件,是核心阶段;
- check:执行
setImmediate回调; - close callbacks:执行
close事件回调(如socket.on('close'))。
五、经典面试题(事件循环执行顺序)
1 | console.log('start'); |
解析:
- 同步代码:
start→end; - 清空微任务队列:执行
promise2,并新增timeout2到宏任务队列; - 执行下一个宏任务
timeout1; - 清空
timeout1产生的微任务:promise1; - 执行下一个宏任务
timeout2。
六、async/await 与事件循环
async/await 是 Promise 的语法糖,await 后的代码会被包裹为 Promise.then 的微任务:
1 | async function fn() { |
解析:
- 同步代码:
sync1→ 执行fn()输出async1→sync2; await后的async2进入微任务队列,同步代码执行完后清空微任务 → 输出async2。
七、核心总结
事件循环的目的:解决 JS 单线程下异步任务的调度问题;
核心规则
:
- 同步任务优先执行,异步任务分宏任务 / 微任务;
- 宏任务执行完 → 清空所有微任务(包括新增的)→ 渲染(浏览器)→ 下一个宏任务;
- 微任务优先级 > 宏任务,
process.nextTick(Node)> 普通微任务;
常见误区
:
setTimeout(fn, 0)不是立即执行,而是 “当前宏任务执行完后尽快执行”;- 微任务队列会被一次性清空,而非只执行一个;
应用价值:理解事件循环能精准预判异步代码的执行顺序,解决回调地狱、异步竞态等问题。
简单记:同步先执行,微任务插队宏任务前,宏任务排队挨个来。



