JavaScript事件循环机制
约 1551 字大约 5 分钟
javascriptevent-loop
2025-07-21
概述
JavaScript 是单线程语言,事件循环(Event Loop)是其实现异步非阻塞 I/O 的核心机制。理解事件循环的运作方式,是正确编写异步代码、避免执行顺序陷阱的关键。
事件循环总体架构
1. 调用栈(Call Stack)
调用栈是 JavaScript 引擎执行同步代码的核心数据结构。
function first() {
console.log('first');
second();
console.log('first end');
}
function second() {
console.log('second');
third();
console.log('second end');
}
function third() {
console.log('third');
}
first();
// 调用栈变化:
// [first] → [first, second] → [first, second, third]
// → [first, second] → [first] → []
// 输出: first → second → third → second end → first end2. 任务队列(Task Queue / Macrotask Queue)
宏任务源包括:
| 来源 | 示例 |
|---|---|
| setTimeout / setInterval | 定时器回调 |
| I/O | 网络请求、文件读写回调 |
| UI 渲染 | 用户交互事件 |
| MessageChannel | port.postMessage |
| setImmediate | Node.js 专有 |
console.log('script start');
setTimeout(() => {
console.log('setTimeout 1');
}, 0);
setTimeout(() => {
console.log('setTimeout 2');
}, 0);
console.log('script end');
// 输出:
// script start
// script end
// setTimeout 1
// setTimeout 23. 微任务队列(Microtask Queue)
微任务在当前宏任务执行完毕后、下一个宏任务开始前全部清空。
微任务源包括:
Promise.then / catch / finallyMutationObserverqueueMicrotask()process.nextTick()(Node.js,优先级更高)
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
queueMicrotask(() => console.log('4'));
console.log('5');
// 输出: 1 → 5 → 3 → 4 → 2
// 同步代码 → 微任务 → 宏任务4. 经典执行顺序分析
示例一:Promise 与 setTimeout 混合
console.log('start');
setTimeout(() => {
console.log('timeout 1');
Promise.resolve().then(() => console.log('promise inside timeout'));
}, 0);
Promise.resolve().then(() => {
console.log('promise 1');
setTimeout(() => console.log('timeout inside promise'), 0);
});
Promise.resolve().then(() => console.log('promise 2'));
console.log('end');
// 执行顺序分析:
// 同步:start → end
// 微任务:promise 1 → promise 2
// 宏任务1:timeout 1 → (微任务) promise inside timeout
// 宏任务2:timeout inside promise示例二:async/await 的本质
async function asyncFunc() {
console.log('async start');
await Promise.resolve();
// await 之后的代码等价于 .then() 回调,是微任务
console.log('async after await');
}
console.log('script start');
setTimeout(() => console.log('setTimeout'), 0);
asyncFunc();
new Promise(resolve => {
console.log('promise executor'); // 同步执行
resolve();
}).then(() => {
console.log('promise then');
});
console.log('script end');
// 输出:
// script start
// async start
// promise executor
// script end
// async after await
// promise then
// setTimeout示例三:微任务嵌套
Promise.resolve().then(() => {
console.log('micro 1');
Promise.resolve().then(() => {
console.log('micro 1-1');
Promise.resolve().then(() => {
console.log('micro 1-1-1');
});
});
});
Promise.resolve().then(() => {
console.log('micro 2');
});
setTimeout(() => console.log('macro 1'), 0);
// 输出: micro 1 → micro 2 → micro 1-1 → micro 1-1-1 → macro 1
// 微任务队列在每轮完全清空,包括执行过程中新加入的微任务5. requestAnimationFrame
requestAnimationFrame(rAF)在浏览器每次重绘前执行,它既不属于宏任务,也不属于微任务,是独立的渲染阶段回调。
console.log('start');
requestAnimationFrame(() => {
console.log('rAF 1');
});
setTimeout(() => {
console.log('timeout');
}, 0);
Promise.resolve().then(() => {
console.log('microtask');
});
console.log('end');
// 典型输出: start → end → microtask → rAF 1 → timeout
// 注意:rAF 和 setTimeout 的相对顺序取决于浏览器的渲染时机
// 在不需要渲染的帧中,rAF 可能被延迟6. requestIdleCallback
requestIdleCallback 在浏览器空闲时段执行低优先级任务。
// requestIdleCallback 在帧渲染完成且有空闲时间时调用
requestIdleCallback((deadline) => {
// deadline.timeRemaining() 返回当前帧剩余时间(毫秒)
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
performTask(tasks.shift());
}
// 如果还有任务,注册下一次空闲回调
if (tasks.length > 0) {
requestIdleCallback(processRemainingTasks);
}
}, { timeout: 2000 }); // 超时后强制执行7. Node.js 事件循环差异
Node.js 的事件循环基于 libuv,分为多个阶段,与浏览器有显著不同。
Node.js 各阶段说明
| 阶段 | 说明 |
|---|---|
| timers | 执行 setTimeout / setInterval 回调 |
| pending callbacks | 执行延迟的 I/O 回调 |
| idle, prepare | 内部使用 |
| poll | 获取新的 I/O 事件,执行 I/O 回调 |
| check | 执行 setImmediate 回调 |
| close callbacks | 执行 socket.on('close') 等回调 |
Node.js 特有的 process.nextTick
// process.nextTick 优先级高于 Promise.then
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
// 输出: nextTick → promise
// nextTick 递归可能导致 I/O 饥饿
function recursiveNextTick() {
process.nextTick(() => {
console.log('tick');
recursiveNextTick(); // 危险:阻塞事件循环进入下一阶段
});
}
// 应使用 setImmediate 替代setTimeout vs setImmediate
// 在主模块中,顺序不确定
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// 可能输出 timeout → immediate 或 immediate → timeout
// 在 I/O 回调中,setImmediate 始终先执行
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});
// 始终输出: immediate → timeout8. 常见陷阱与最佳实践
长任务阻塞事件循环
// Bad: 长循环阻塞主线程
function processLargeArray(arr) {
for (let i = 0; i < arr.length; i++) {
heavyComputation(arr[i]); // 可能阻塞数百毫秒
}
}
// Good: 分片处理,让出控制权
async function processInChunks(arr, chunkSize = 100) {
for (let i = 0; i < arr.length; i += chunkSize) {
const chunk = arr.slice(i, i + chunkSize);
chunk.forEach(item => heavyComputation(item));
// 让出控制权给事件循环
await new Promise(resolve => setTimeout(resolve, 0));
}
}微任务死循环
// 危险:微任务无限递归会冻结页面
function infiniteMicrotask() {
Promise.resolve().then(() => {
infiniteMicrotask(); // 微任务永远清不空,宏任务和渲染永远无法执行
});
}
// 切勿在生产环境执行!利用微任务时机
// 合并多次状态更新(类似 Vue 的 nextTick 原理)
let pending = false;
let updates = [];
function scheduleUpdate(update) {
updates.push(update);
if (!pending) {
pending = true;
queueMicrotask(() => {
const batch = updates.slice();
updates = [];
pending = false;
applyBatchUpdate(batch); // 一次性应用所有更新
});
}
}总结
事件循环是 JavaScript 异步编程的基石。核心要点:每个宏任务执行后清空所有微任务,然后可能进行渲染(rAF + Style/Layout/Paint),再执行下一个宏任务。理解宏任务、微任务和渲染阶段的时序关系,是编写可预测异步代码的基础。Node.js 事件循环基于 libuv 多阶段模型,与浏览器有所不同,需要分别掌握。
贡献者
更新日志
2026/3/14 13:09
查看所有更新日志
9f6c2-feat: organize wiki content and refresh site setup于