事件循环(Event Loop)详解:JavaScript 异步机制的核心
事件循环(Event Loop)可以理解为:JavaScript 用来“排队+按顺序处理任务”的机制。
因为 JavaScript 是单线程的(同一时间只能干一件事),所以它需要一套规则来决定:
“现在先执行谁?谁等会儿?谁插队?”
1. 为什么要有事件循环?
如果 JavaScript 只有同步代码还好,但现实中有很多异步操作:
- 定时器
setTimeout - 网络请求(
fetch/axios) - 事件回调(点击、滚动等)
- Promise
这些异步操作不可能让 JS “卡住等它们完成”,
所以 JS 选择:先继续执行同步代码,异步结果回来后再处理。
那么“回来后再处理”靠什么?答案就是——事件循环。
2. 事件循环在干什么?(一句话版)
不停地看:主线程空不空?空了就从任务队列里拿任务来执行。
3. 事件循环的关键结构
✅ 调用栈(Call Stack)
存放同步代码的地方。
执行函数时压栈,执行完弹栈。
✅ 宏任务队列(Task Queue / Macrotask Queue)
存放宏任务,比如:
setTimeout/setInterval- DOM 事件回调
postMessage- I/O 操作
✅ 微任务队列(Microtask Queue)
优先级更高,存放微任务,比如:
Promise.then/catch/finallyqueueMicrotaskMutationObserver
4. 执行顺序规则
一次事件循环的执行流程:
- 先执行 调用栈里的同步代码
- 调用栈空了 → 清空所有微任务
- 微任务清空后 → 执行 一个宏任务
- 重复 1~3
口诀:同步 → 微任务(清空) → 宏任务(一个) → 循环
5. 一个经典示例
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
Promise.resolve().then(() => {
console.log(3)
})
console.log(4)
执行顺序分析:
- 同步代码先执行:输出
1和4 - 同步执行完毕,清空微任务队列:输出
3 - 最后执行宏任务队列中的任务:输出
2
所以最终输出是:
1
4
3
2
6. 总结
事件循环是 JavaScript 处理异步的机制。
JS 先执行调用栈里的同步代码,栈空后会优先清空微任务队列(比如 Promise.then),然后执行一个宏任务(比如 setTimeout、事件回调),不断重复这个过程。 好的,接下来给你几个更有挑战性的事件循环面试题,让你在面试中可以轻松应对:
7. 例子 1:异步代码与事件循环
console.log('Start')
setTimeout(() => {
console.log('Timeout')
}, 0)
Promise.resolve().then(() => {
console.log('Promise')
})
console.log('End')
输出顺序是?
解释:
- 同步执行:
'Start'和'End' setTimeout是宏任务,会被放入宏任务队列,等主线程空了才会执行。Promise.resolve().then()是微任务,微任务队列在当前同步代码执行完后立刻执行。- 所以输出顺序是:
Start End Promise Timeout
8. 例子 2:多重微任务和宏任务
console.log('Start')
setTimeout(() => {
console.log('Timeout 1')
Promise.resolve().then(() => {
console.log('Promise 2')
})
}, 0)
Promise.resolve().then(() => {
console.log('Promise 1')
})
console.log('End')
输出顺序是?
解释:
- 首先,执行同步代码:
'Start'和'End' - 然后执行第一个微任务:
'Promise 1' - 接下来,主线程空了,开始执行宏任务:
'Timeout 1' - 宏任务中的异步代码生成了一个微任务:
'Promise 2' - 最后,执行
'Promise 2' - 所以输出顺序是:
Start End Promise 1 Timeout 1 Promise 2
9. 例子 3:嵌套的微任务和宏任务
console.log('Start')
setTimeout(() => {
console.log('Timeout 1')
Promise.resolve().then(() => {
console.log('Promise 2')
setTimeout(() => {
console.log('Timeout 2')
}, 0)
})
}, 0)
Promise.resolve().then(() => {
console.log('Promise 1')
})
console.log('End')
输出顺序是?
解释:
- 首先执行同步代码:
'Start'和'End' - 然后执行第一个微任务:
'Promise 1' - 接下来,主线程空了,开始执行宏任务:
'Timeout 1' - 在
'Timeout 1'中创建了一个微任务:'Promise 2',并且又创建了一个新的宏任务:'Timeout 2' - 执行
'Promise 2'后,由于事件循环的规则,它会继续执行微任务,所以'Timeout 2'会被推迟到当前宏任务执行完后。 - 所以输出顺序是:
Start End Promise 1 Timeout 1 Promise 2 Timeout 2
10. 例子 4:多次 setTimeout 和 Promise
console.log('Start')
setTimeout(() => {
console.log('Timeout 1')
}, 0)
Promise.resolve().then(() => {
console.log('Promise 1')
})
setTimeout(() => {
console.log('Timeout 2')
}, 0)
Promise.resolve().then(() => {
console.log('Promise 2')
})
console.log('End')
输出顺序是?
解释:
- 执行同步代码:
'Start'和'End' - 执行第一个微任务:
'Promise 1' - 然后执行第一个宏任务:
'Timeout 1' - 然后执行第二个微任务:
'Promise 2' - 然后执行第二个宏任务:
'Timeout 2' - 所以输出顺序是:
Start End Promise 1 Timeout 1 Promise 2 Timeout 2
总结
通过这些例子,你可以看到事件循环的基本工作原理。理解 同步代码、微任务、宏任务的顺序 是关键。如果理解了这些基本的规则,面对更复杂的异步逻辑时就能迅速推断出执行顺序。