JavaScript是单线程运行的脚本语言,为了不阻塞主线程并处理异步操作,它依赖事件循环(Event Loop)来调度任务。事件循环让单线程也能高效响应定时器、网络请求和用户交互。理解它的实现方式和阶段划分,对写出可预测的异步代码非常关键。

事件循环的基本结构
在浏览器或Node.js中,JS引擎维护一个执行栈和一个任务队列。当同步代码执行完后,运行时会查看是否有待处理的任务。事件循环的核心逻辑可以用下面的伪代码表示:
// 事件循环简化逻辑
while (queue.waitForTask()) {
// 1. 从宏任务队列取一个任务执行
const task = macroQueue.shift();
runTask(task);
// 2. 执行所有已存在的微任务
while (microQueue.length > 0) {
const micro = microQueue.shift();
runMicrotask(micro);
}
// 3. 必要时渲染页面(浏览器环境)
if (needRender) {
render();
}
}
事件循环的主要阶段
虽然浏览器和Node.js的实现细节不同,但总体都遵循先微任务后宏任务的原则。下面以浏览器为例说明常见阶段。
宏任务阶段
宏任务(Macro Task)代表较为独立的、粒度较大的工作单元。每轮事件循环只会从宏任务队列中取出一个来执行。
- setTimeout 和 setInterval 的回调
- UI 渲染与交互事件(如 click、load)
- I/O 操作完成后的回调
微任务阶段
微任务(Micro Task)通常在当前宏任务结束后、下一个宏任务开始前全部执行完。它的优先级高于宏任务。
- Promise 的 then、catch、finally 回调
- MutationObserver 的回调
- queueMicrotask 传入的函数
执行顺序示例
看一段代码,感受阶段切换带来的输出顺序:
console.log('start'); // 同步,立即执行
setTimeout(() => {
console.log('timeout'); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log('promise'); // 微任务
});
console.log('end'); // 同步,立即执行
// 输出顺序:start -> end -> promise -> timeout
上述代码先执行同步部分,打印 start 和 end;同步栈清空后,事件循环先处理微任务队列,打印 promise;再取宏任务,打印 timeout。
Node.js中的阶段差异
Node.js的 libuv 将事件循环分为多个阶段,如 timers、pending callbacks、poll、check 等。其中 setImmediate 在 check 阶段执行,而 setTimeout 在 timers 阶段,两者顺序在不同场景下可能互换。但微任务(如 process.nextTick 和 Promise)依然会在阶段切换前清空。
| 环境 | 宏任务示例 | 微任务示例 |
|---|---|---|
| 浏览器 | setTimeout, I/O, UI事件 | Promise.then, MutationObserver |
| Node.js | setTimeout, setImmediate, I/O | Promise.then, process.nextTick |
小结
JS通过事件循环在单线程上模拟并发。每轮循环先执行一个宏任务,再清空微任务队列,必要时渲染。掌握宏任务与微任务的划分,以及不同运行环境的阶段特点,能帮你准确判断异步代码的执行时机。
JavaScript事件循环Event_Loop宏任务微任务修改时间:2026-07-25 11:39:21