JavaScript事件循环机制
2026/3/20大约 13 分钟
JavaScript 事件循环机制
JavaScript 的单线程本质
为什么 JavaScript 是单线程
JavaScript 最初被设计为浏览器脚本语言,用于操作 DOM。如果允许多线程同时操作 DOM,会导致复杂的同步问题:
单线程的挑战
单线程意味着同一时间只能执行一个任务,那如何处理耗时操作?
// 如果没有异步机制,这将阻塞整个页面
const data = fetchData(); // 假设需要5秒
console.log(data); // 5秒后才执行
// 在这5秒内,页面完全无法响应用户操作
解决方案:事件循环 + 异步 API
浏览器中的事件循环
浏览器的多线程架构
虽然 JavaScript 是单线程,但浏览器本身是多线程的:
事件循环的完整模型
宏任务与微任务
| 类型 | 宏任务(Macrotask) | 微任务(Microtask) |
|---|---|---|
| 定义 | 每次事件循环执行一个 | 当前宏任务结束后全部执行 |
| 来源 | setTimeout, setInterval, I/O, UI 渲染 | Promise.then, queueMicrotask, MutationObserver |
| 优先级 | 低 | 高 |
| 执行时机 | 每轮事件循环 | 每个宏任务之后 |
经典面试题分析
console.log("1");
setTimeout(function () {
console.log("2");
Promise.resolve().then(function () {
console.log("3");
});
}, 0);
Promise.resolve().then(function () {
console.log("4");
setTimeout(function () {
console.log("5");
}, 0);
});
console.log("6");
// 输出顺序:1, 6, 4, 2, 3, 5
执行过程分析:
async/await 的执行顺序
async/await 是 Promise 的语法糖,理解其本质有助于分析执行顺序:
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 0);
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log("script end");
// 输出顺序:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
关键理解:
// await async2() 等价于:
async2().then(() => {
console.log("async1 end");
});
Node.js 中的事件循环
Node.js 架构
Node.js 使用 libuv 作为事件循环的底层实现:
Node.js 事件循环的六个阶段
各阶段详解
| 阶段 | 作用 | 回调来源 |
|---|---|---|
| timers | 执行 setTimeout 和 setInterval 的回调 | 定时器到期 |
| pending callbacks | 执行延迟到下一轮的 I/O 回调 | 某些系统操作 |
| idle, prepare | 仅内部使用 | - |
| poll | 获取新的 I/O 事件,执行 I/O 相关回调 | 网络、文件 I/O |
| check | 执行 setImmediate 的回调 | setImmediate |
| close callbacks | 执行 close 事件回调 | socket.on('close') |
process.nextTick vs setImmediate
setImmediate(() => {
console.log("setImmediate");
});
process.nextTick(() => {
console.log("nextTick");
});
console.log("main");
// 输出顺序:
// main
// nextTick
// setImmediate
执行时机对比:
setTimeout vs setImmediate
在不同上下文中,它们的执行顺序可能不同:
// 情况1:主模块中,顺序不确定
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
// 可能输出:timeout, immediate 或 immediate, timeout
// 情况2:I/O 回调中,setImmediate 总是先执行
const fs = require("fs");
fs.readFile(__filename, () => {
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
});
// 总是输出:immediate, timeout
原因分析:
浏览器 vs Node.js 事件循环对比
主要差异
微任务执行时机差异(Node.js 11+)
从 Node.js 11 开始,微任务的执行时机与浏览器保持一致:
// Node.js 11+ 和浏览器行为一致
setTimeout(() => {
console.log("timer1");
Promise.resolve().then(() => console.log("promise1"));
}, 0);
setTimeout(() => {
console.log("timer2");
Promise.resolve().then(() => console.log("promise2"));
}, 0);
// Node.js 11+: timer1, promise1, timer2, promise2
// Node.js 10-: timer1, timer2, promise1, promise2(已过时)
常见陷阱与最佳实践
陷阱 1:回调地狱
// ❌ 回调地狱
fs.readFile("file1.txt", (err, data1) => {
fs.readFile("file2.txt", (err, data2) => {
fs.readFile("file3.txt", (err, data3) => {
// 嵌套层级过深,难以维护
});
});
});
// ✅ 使用 async/await
async function readFiles() {
const data1 = await fs.promises.readFile("file1.txt");
const data2 = await fs.promises.readFile("file2.txt");
const data3 = await fs.promises.readFile("file3.txt");
return [data1, data2, data3];
}
陷阱 2:阻塞事件循环
// ❌ 同步计算阻塞事件循环
function heavyComputation() {
let result = 0;
for (let i = 0; i < 1e9; i++) {
result += i;
}
return result;
}
app.get("/compute", (req, res) => {
const result = heavyComputation(); // 阻塞整个服务器!
res.json({ result });
});
// ✅ 分片执行或使用 Worker
function heavyComputationAsync() {
return new Promise((resolve) => {
let result = 0;
let i = 0;
const batch = 1e6;
function processBatch() {
const end = Math.min(i + batch, 1e9);
for (; i < end; i++) {
result += i;
}
if (i < 1e9) {
setImmediate(processBatch); // 让出事件循环
} else {
resolve(result);
}
}
processBatch();
});
}
陷阱 3:未处理的 Promise 拒绝
// ❌ 未处理的拒绝
async function riskyOperation() {
throw new Error("Something went wrong");
}
riskyOperation(); // UnhandledPromiseRejection
// ✅ 正确处理
riskyOperation().catch((err) => {
console.error("Caught error:", err);
});
// 或全局处理
process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled Rejection:", reason);
});
陷阱 4:nextTick 导致 I/O 饥饿
// ❌ 可能导致 I/O 饥饿
function recursiveNextTick() {
process.nextTick(() => {
// 不断调用 nextTick
recursiveNextTick();
});
}
recursiveNextTick();
// I/O 回调永远得不到执行!
// ✅ 使用 setImmediate
function recursiveImmediate() {
setImmediate(() => {
// setImmediate 会让 I/O 有机会执行
recursiveImmediate();
});
}
性能优化技巧
1. 合理使用 queueMicrotask
// 需要在同步代码后、下一个宏任务前执行时使用
queueMicrotask(() => {
console.log("在所有同步代码后执行");
});
2. 批量处理避免频繁回调
// ❌ 每条数据一个回调
items.forEach((item) => {
setImmediate(() => processItem(item));
});
// ✅ 批量处理
function processBatch(items, batchSize = 100) {
let index = 0;
function processNext() {
const batch = items.slice(index, index + batchSize);
batch.forEach(processItem);
index += batchSize;
if (index < items.length) {
setImmediate(processNext);
}
}
processNext();
}
3. 使用 Worker Threads 处理 CPU 密集型任务
// main.js
const { Worker } = require("worker_threads");
function runHeavyTask(data) {
return new Promise((resolve, reject) => {
const worker = new Worker("./worker.js", {
workerData: data,
});
worker.on("message", resolve);
worker.on("error", reject);
});
}
// worker.js
const { parentPort, workerData } = require("worker_threads");
// 在独立线程中执行 CPU 密集型任务
const result = heavyComputation(workerData);
parentPort.postMessage(result);
本章小结
核心要点
- 单线程不意味着同步:JavaScript 通过事件循环实现异步
- 宏任务 vs 微任务:微任务优先级更高,在每个宏任务后执行
- 浏览器 vs Node.js:Node.js 有更复杂的阶段划分
- 避免阻塞:永远不要在事件循环中执行耗时的同步操作