巧解事件循环:JavaScript 执行机制全解析
事件循环(Event Loop)是 JavaScript 最核心的概念之一,也是面试中的高频考点。
JavaScript 是单线程的,同一时间只能做一件事。那它是如何实现异步操作的?这就要靠事件循环。
一、为什么需要事件循环?
JavaScript 是单线程语言,意味着:
- 同一时间只能执行一段代码
- 如果一段代码执行时间过长,会阻塞后续代码执行
示例:
javascript
console.log('开始');
setTimeout(() => {
console.log('定时器执行');
}, 1000);
console.log('结束');
// 输出:开始 → 结束 → 定时器执行如果没有事件循环,定时器会阻塞程序等待 1 秒。但 JavaScript 是怎么做到不等定时器完成就继续执行后续代码的?
答案:事件循环 + 任务队列
二、事件循环的核心机制
事件循环由三部分组成:
┌─────────────────────────┐
│ 执行栈 │ ← 执行同步代码
│ (Call Stack) │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ 任务队列 │ ← 存放待执行的回调
│ (Task Queue) │
│ ├─ 宏任务队列 │
│ └─ 微任务队列 │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ 事件循环 │ ← 不断检查队列
│ (Event Loop) │
│ 执行栈空 → 取任务执行 │
└─────────────────────────┘执行流程:
- 执行栈执行同步代码
- 遇到异步任务,交给浏览器/Web API 处理
- 异步完成后,回调放入任务队列
- 事件循环不断检查执行栈
- 执行栈为空时,取队列中的任务执行
- 重复步骤 4-5
三、宏任务与微任务
任务队列分为两类:
| 类型 | 示例 | 执行时机 |
|---|---|---|
| 宏任务 | setTimeout、setInterval、I/O、UI渲染 | 当前宏任务执行完后 |
| 微任务 | Promise.then、MutationObserver、queueMicrotask | 下一轮事件循环前 |
执行顺序:
执行宏任务 → 执行完所有微任务 → 执行下一个宏任务 → ...示例:
javascript
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
// 输出顺序:1 → 4 → 3 → 2执行过程:
- 打印
1 - setTimeout 交给 WebAPI,callback 放入宏任务队列
- Promise.then 放入微任务队列
- 打印
4 - 执行栈为空,先执行所有微任务(打印
3) - 执行宏任务(打印
2)
四、经典面试题
题目 1:
javascript
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
}).then(() => {
console.log('promise2');
});
console.log('script end');答案:
script start
script end
promise1
promise2
setTimeout题目 2(async/await):
javascript
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
async1();
console.log('script end');答案:
script start
async1 start
async2
script end
async1 end解析:await 后面的代码相当于放在 Promise.then 中,属于微任务。
题目 3(综合):
javascript
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);
new Promise((resolve) => {
console.log('4');
resolve();
}).then(() => {
console.log('5');
});
setTimeout(() => {
console.log('6');
}, 0);
console.log('7');答案:
1
4
7
5
2
3
6五、常见面试题解析
Q1:setTimeout(fn, 0) 是立即执行吗?
A:不是。fn 会加入宏任务队列,等待当前执行栈清空后执行,最少延迟 4ms 左右。
Q2:Promise 和 setTimeout 谁先执行?
A:Promise.then 是微任务,setTimeout 是宏任务。微任务优先级更高,会先执行。
Q3:async/await 原理是什么?
A:await 暂停当前函数执行,等待 Promise 完成。await 后面的代码相当于 Promise.then 的回调,属于微任务。
javascript
async function foo() {
console.log('a');
await console.log('b');
console.log('c');
}
// 等价于:
function foo() {
console.log('a');
Promise.resolve(console.log('b')).then(() => {
console.log('c');
});
}六、实战建议
| 场景 | 推荐做法 |
|---|---|
| 异步操作完成后再执行 | 使用 Promise 或 async/await |
| 延迟执行 | 使用 setTimeout |
| 需要等待多个异步 | 使用 Promise.all |
| 需要排队执行 | 使用 async 串行 |
| 插队执行 | 使用 queueMicrotask |
七、总结
事件循环的核心要点:
- 单线程:JavaScript 同一时间只能做一件事
- 异步:通过 WebAPI + 回调实现
- 任务队列:宏任务和微任务
- 执行顺序:同步 → 微任务 → 宏任务
- 事件循环:不断检查执行栈和队列,循环往复
理解事件循环,才能写出正确的异步代码,也是面试必备技能!