JavaScript 事件循环详解
1. 单线程与异步的必要性
JavaScript 是单线程语言,意味着它一次只能执行一个任务。为了避免耗时操作(如网络请求、定时器)阻塞主线程,JavaScript 使用事件循环机制处理异步任务,确保非阻塞执行。
2. 事件循环的核心组成
- 调用栈(Call Stack):主要针对函数,如果函数里面调用函数,遵循后进先出(LIFO)原则。
- 任务队列(Task Queue/Macrotask Queue):存放宏任务(如
setTimeout
、I/O
操作)。 - 微任务队列(Microtask Queue):存放微任务(如
Promise.then
、MutationObserver
)。 - Web APIs:浏览器提供的线程(如定时器线程、DOM 事件线程),处理异步操作后将回调推入队列。
3. 宏任务(Macrotasks)与微任务(Microtasks)
- 宏任务:包括
setTimeout
、setInterval
、I/O
、UI 渲染、requestAnimationFrame
(部分浏览器)。 - 微任务:包括
Promise.then
、MutationObserver
、process.nextTick
(Node.js)。
4. 事件循环流程
- 执行同步代码:直至调用栈清空。
- 处理微任务队列:依次执行所有微任务(包括执行过程中新生成的微任务)。
- 渲染页面(如需要):执行
requestAnimationFrame
回调,更新 UI。 - 取一个宏任务执行:从宏任务队列中取出并执行,重复整个循环。
5. 执行顺序示例
示例 1:基础顺序
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
)执行。
示例 2:嵌套任务
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => console.log('Promise inside Timeout'));
}, 0);
setTimeout(() => {
console.log('Timeout 2');
Promise.resolve().then(() => console.log('Promise inside Timeout 2'));
}, 0);
输出:
Timeout 1 → Promise inside Timeout → Timeout 2 → Promise inside Timeout 2
解析:
- 每个宏任务执行后立即处理其产生的微任务。
示例 3:微任务递归
function loopMicrotasks() {
Promise.resolve().then(loopMicrotasks);
}
loopMicrotasks();
// 主线程被阻塞,无法执行后续任务。
解析:微任务队列永不空,导致无限循环,阻塞后续任务。
6. 与渲染相关的行为
requestAnimationFrame
在渲染前执行,属于宏任务,但优先级高于普通宏任务:
setTimeout(() => console.log('Timeout'), 0);
requestAnimationFrame(() => console.log('RAF'));
// 可能输出:RAF → Timeout(取决于浏览器渲染时机)
7. Node.js 与浏览器差异
- Node.js 事件循环:分阶段(Timers、Poll、Check 等),
setImmediate
和process.nextTick
优先级不同。 - 浏览器:更简化的宏任务/微任务模型,
requestAnimationFrame
在渲染前触发。
8. 关键点总结
- 微任务优先:每个宏任务后必须清空微任务队列。
- 避免阻塞:长时间运行的微任务会阻塞渲染。
- API 分类:正确区分宏任务和微任务来源(如
Promise
为微任务,setTimeout
为宏任务)。
9. 综合示例
console.log('Script start');
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => console.log('Promise 1'));
}, 0);
Promise.resolve().then(() => {
console.log('Promise 2');
setTimeout(() => console.log('Timeout 2'), 0);
});
console.log('Script end');
输出:
Script start → Script end → Promise 2 → Timeout 1 → Promise 1 → Timeout 2
解析:
- 同步代码执行。
- 微任务
Promise 2
触发,添加宏任务Timeout 2
。 - 宏任务
Timeout 1
执行,触发微任务Promise 1
。 - 所有微任务处理完毕后,执行下一个宏任务
Timeout 2
。