事件循环与任务队列
JavaScript 单线程通过事件循环实现异步非阻塞,理解其调度机制是写出正确异步代码的关键。
事件循环机制
核心组件
JavaScript
调用栈 → 执行同步代码
↓ 遇到异步操作
Web APIs → 定时器/网络请求等(浏览器提供)
↓ 完成后
任务队列 → 等待回调入栈
↓ 调用栈空闲
事件循环 → 将回调推入调用栈
执行流程
JavaScript
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
console.log('3');
// 输出: 1 → 3 → 2
执行过程:
- 同步代码
console.log('1')入栈执行 setTimeout交给 Web API 定时,回调放入宏任务队列console.log('3')入栈执行- 调用栈清空后,事件循环从宏任务队列取出回调执行
宏任务与微任务
宏任务 (MacroTask)
| 来源 | 示例 |
|---|---|
| setTimeout | setTimeout(fn, 0) |
| setInterval | setInterval(fn, 1000) |
| I/O | 文件读取、网络请求 |
| UI 渲染 | 浏览器渲染帧 |
微任务 (MicroTask)
| 来源 | 示例 |
|---|---|
| Promise.then/catch/finally | Promise.resolve().then(fn) |
| MutationObserver | DOM 变化监听 |
| queueMicrotask | queueMicrotask(fn) |
执行优先级
JavaScript
同步代码 > 微任务 > 宏任务
每个宏任务执行完后,会清空微任务队列:
text
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => console.log('3'));
}, 0);
Promise.resolve().then(() => {
console.log('4');
setTimeout(() => console.log('5'), 0);
});
console.log('6');
// 输出: 1 → 6 → 4 → 2 → 3 → 5
| 步骤 | 操作 | 输出 |
|---|---|---|
| 1 | 同步代码 | 1, 6 |
| 2 | 清空微任务队列 | 4 |
| 3 | 取宏任务2 | 2 |
| 4 | 清空微任务(3入队) | 3 |
| 5 | 取宏任务5 | 5 |
嵌套示例
text
setTimeout(() => {
console.log('宏任务1');
Promise.resolve().then(() => {
console.log('微任务1');
});
}, 0);
setTimeout(() => {
console.log('宏任务2');
}, 0);
Promise.resolve().then(() => {
console.log('微任务2');
setTimeout(() => {
console.log('宏任务3');
}, 0);
});
console.log('开始');
// 输出: 开始 → 微任务2 → 宏任务1 → 微任务1 → 宏任务2 → 宏任务3
要点总结
- 同步代码先执行完,再处理异步队列
- 微任务优先级高于宏任务,每个宏任务后清空微任务队列
Promise.then是微任务,setTimeout是宏任务- 微任务队列不清空,渲染不会进行
- 理解事件循环有助于调试异步执行顺序问题