为什么需要事件循环?
JavaScript 是单线程语言,意味着同一时间只能做一件事。但如果有一个任务需要等待(比如网络请求),难道要让整个页面卡住吗?
当然不会。这就是事件循环发挥作用的地方。
一句话解释事件循环
事件循环是一个不断运行的机制,它负责执行调用栈中的代码,并将异步任务的回调放入恰当的时间执行。
核心组件
┌─────────────────────────┐
│ 调用栈 (Call Stack) │ ← 同步代码在这里执行
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ Web APIs / 任务队列 │ ← 异步任务等待的地方
│ (setTimeout, fetch等) │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ 回调队列 (Callback Queue)│ ← 等待进入调用栈的任务
└─────────────────────────┘
经典例子:猜猜输出顺序?
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
// 输出: 1, 4, 3, 2
// 是不是和你猜的一样?
宏任务与微任务
事件循环中有两种任务队列,优先级不同:
| 类型 | 常见API | 优先级 |
|---|---|---|
| 微任务 | Promise.then, async/await, MutationObserver | 高 |
| 宏任务 | setTimeout, setInterval, I/O, UI渲染 | 低 |
执行规则:每执行一个宏任务前,会先清空所有微任务队列。
setTimeout(() => console.log('宏任务1'), 0);
setTimeout(() => console.log('宏任务2'), 0);
Promise.resolve().then(() => {
console.log('微任务1');
Promise.resolve().then(() => console.log('微任务2'));
});
console.log('同步代码');
// 输出: 同步代码 → 微任务1 → 微任务2 → 宏任务1 → 宏任务2
setTimeout(() => console.log('宏任务1'), 0);
setTimeout(() => console.log('宏任务2'), 0);
Promise.resolve().then(() => {
console.log('微任务1');
Promise.resolve().then(() => console.log('微任务2'));
});
console.log('同步代码');
// 输出: 同步代码 → 微任务1 → 微任务2 → 宏任务1 → 宏任务2
async/await 的本质
async/await 是 Promise 的语法糖,理解它能帮助你更好地掌握事件循环:
async function demo() {
console.log('A');
await Promise.resolve();
console.log('B');
}
console.log('C');
demo();
console.log('D');
// 输出: C, A, D, B
常见面试题:输出顺序
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
async1();
new Promise((resolve) => {
console.log('promise1');
resolve();
}).then(() => {
console.log('promise2');
});
console.log('script end');
// 答案:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
实用技巧:让 setTimeout 变成“同步”
// 等待指定时间(同步阻塞版本,不推荐)
function sleep(ms) {
const start = Date.now();
while (Date.now() - start < ms) {}
}
// 更好的异步版本
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function demo() {
console.log('开始');
await delay(2000);
console.log('2秒后');
}
事件循环对性能的影响
❌ 坏:长时间阻塞调用栈
// 这会卡死页面
function heavyTask() {
let count = 0;
for (let i = 0; i < 1000000000; i++) {
count++;
}
console.log(count);
}
✅ 好:分片处理
function processLargeArray(items, chunkSize = 100, callback) {
let index = 0;
function processChunk() {
const end = Math.min(index + chunkSize, items.length);
for (let i = index; i < end; i++) {
// 处理 items[i]
}
index = end;
if (index < items.length) {
setTimeout(processChunk, 0);
} else {
callback();
}
}
processChunk();
}
记住这张图就够了
┌─────┐ ┌─────────┐
│同步代码│ ──▶ │ 微任务队列 │ ──┐
└─────┘ └─────────┘ │
│
┌─────┐ ┌─────────┐ │
│宏任务│ ◀── │ 宏任务队列 │ ◀─┘
└─────┘ └─────────┘
一句话总结:先同步、再清空微任务、再取一个宏任务,重复循环。
文章评论