Event Loop 是 JavaScript 处理异步的机制:同步代码在调用栈执行,异步回调交给 Web APIs,完成后进入任务队列。每执行完一个宏任务,引擎会先清空整个微任务队列,再取下一个宏任务——这就是 Promise 回调总比 setTimeout 先执行的原因。
最后更新:2026-01-03 · 基于 浏览器环境的 HTML 规范事件循环,Node.js 的阶段划分略有不同
Event Loop(事件循环)是前端面试里出镜率最高的概念之一,几乎每家公司都爱考。但说实话,我当初学的时候看了好几篇文章,还是一知半解——每篇都在讲”宏任务微任务”,却说不清楚它们跟 Event Loop 的关系到底是什么。
后来是通过多跑代码、观察输出顺序,才慢慢把这个东西搞清楚的。
先从一个问题开始
运行下面这段代码,输出顺序是什么?
1 | console.log('1') |
答案:1 → 4 → 3 → 2
如果你能马上说出来,原因也说得清楚,这篇文章可以跳过了。如果有点迷糊,接着看。
JavaScript 是单线程的
首先要明确:JavaScript 是单线程语言,一次只能做一件事。但我们写了大量异步代码——定时器、网络请求、事件监听——这些并不会阻塞主线程。原因就在 Event Loop。
几个核心概念
调用栈(Call Stack)
同步代码执行的地方。代码进栈、执行、出栈,先进后出。
1 | function a() { |
Web APIs
浏览器提供的能力,比如 setTimeout、fetch、DOM 事件。这些不在 JS 引擎里,是浏览器处理的。setTimeout(fn, 0) 并不是”立刻执行”,而是把 fn 交给浏览器,等时间到了再推进队列。
任务队列(Task Queue)
也叫宏任务队列(Macro Task Queue)。宏任务处理完了会放到这里等待。
微任务队列(Microtask Queue)
优先级比宏任务高,每次宏任务执行完后,先清空微任务队列,再取下一个宏任务。
Event Loop 的执行流程
说清楚流程就两句话:
- 执行当前调用栈里的同步代码,直到栈清空
- 栈清空后,先把微任务队列清空,再取宏任务队列的第一个执行
不断重复这个过程,就是事件循环。
宏任务和微任务都有哪些
宏任务(Macro Task):
setTimeoutsetIntervalsetImmediate(Node.js)- I/O 操作
- UI 渲染
微任务(Micro Task):
Promise.then / catch / finallyMutationObserverqueueMicrotask()async/await(await 后面的代码相当于 .then)
微任务比宏任务优先级高,这是关键。
回到开头的例子
1 | console.log('1') // 同步,进调用栈,输出 1,出栈 |
调用栈清空后:
- 先清微任务队列 → 输出
3 - 再取宏任务队列第一个 → 输出
2
所以是 1 → 4 → 3 → 2。
一个稍微复杂一点的例子
1 | console.log('start') |
一步步分析:
console.log('start')→ 输出startsetTimeout→ 宏任务注册,等待Promise.then× 2 → 微任务队列:[promise1回调, promise2回调]
(注意:第二个.then要等第一个执行完才进队列)console.log('end')→ 输出end- 调用栈清空,清微任务队列:
- 执行
promise1回调 → 输出promise1,此时promise2回调入队 - 执行
promise2回调 → 输出promise2
- 执行
- 微任务队列清空,取宏任务:
- 执行
timeout1回调 → 输出timeout1 - 遇到
Promise.resolve().then→ 微任务入队 - 宏任务执行完,清微任务队列 → 输出
promise in timeout
- 执行
最终输出:start → end → promise1 → promise2 → timeout1 → promise in timeout
async/await 的情况
async/await 是 Promise 的语法糖,await 之后的代码相当于 .then 里的回调。
1 | async function main() { |
输出:before → async start → after main call → after await
await 会暂停当前 async 函数的执行,把控制权还给外部,等微任务队列处理时再继续。
为什么要理解这个
说实话,理解 Event Loop 不只是为了面试。工作里很多 bug 都跟异步顺序有关:
- 明明已经 setState,但读到的还是旧值
- 事件回调里操作 DOM 但布局没更新
- 多个异步操作执行顺序跟预期不符
搞清楚 Event Loop,这类问题排查起来会顺很多。
常见问题
为什么 setTimeout 0 不是立即执行
setTimeout 不在 JS 引擎里执行,而是把回调交给浏览器。即使延时写 0,回调也要先等浏览器计时结束、进入宏任务队列,再等当前同步代码跑完、微任务清空之后才轮到它。
宏任务和微任务分别有哪些
常见宏任务有 setTimeout、setInterval、I/O、UI 渲染、事件回调;常见微任务有 Promise 的 then/catch/finally 回调、queueMicrotask、MutationObserver。async 函数中 await 之后的代码也属于微任务。
console.log 1 setTimeout 2 Promise 3 console.log 4 输出顺序是什么
输出 1 → 4 → 3 → 2。两个 console.log 是同步代码先执行,所以先 1 后 4;然后清空微任务队列,Promise 回调输出 3;最后才取宏任务队列里的 setTimeout 回调输出 2。
微任务队列会一次清空还是只取一个
一次清空。每个宏任务执行完后,引擎会反复取微任务直到队列为空才继续。所以如果微任务里又产生新的微任务,它们会在本轮一并执行完,而不是等到下一轮。






