深入理解 JavaScript 事件循环
搞懂 JS 事件循环,Promise 为什么比 setTimeout 先执行
JavaScript 是单线程的:同一时刻只能做一件事。但我们写 setTimeout、发请求、监听事件,明明都是异步的,页面却不会卡死——秘密就是事件循环(Event Loop)。
三个核心角色
- 调用栈(Call Stack):正在执行的函数排队的地方,后进先出,执行完就出栈。
- 任务队列(Task Queue):异步回调的候车室,
setTimeout、I/O、UI 事件都在这等。 - 事件循环:不停问「调用栈空了没」,空了就把队列里最早的任务推进栈里跑。
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// 输出:1 → 3 → 2
1 和 3 是同步代码直接进栈,setTimeout 的回调被丢进任务队列,等栈清空后才轮到它。
宏任务 vs 微任务(最容易踩坑)
任务其实分两类:
| 类型 | 例子 | 优先级 |
|---|---|---|
| 微任务(microtask) | Promise.then、queueMicrotask、MutationObserver |
高,插队 |
| 宏任务(macrotask) | setTimeout、setInterval、I/O、UI 事件、requestAnimationFrame |
低,排队 |
规则:每执行完一个宏任务,会先把当前所有微任务清空,再取下一个宏任务。
console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => {
console.log('promise1');
Promise.resolve().then(() => console.log('promise2'));
});
console.log('end');
// 输出:start → end → promise1 → promise2 → timeout
关键在于:setTimeout 回调是宏任务,要等「下一轮」;Promise.then 是微任务,在当前同步代码结束后立即清空。所以哪怕 setTimeout(..., 0),也跑不过 Promise。
渲染时机
微任务清空之后、下一个宏任务之前,浏览器可能会渲染一次(约每 16.6ms,对应 60fps)。
这意味着:一帧里塞了一堆微任务,它们会在渲染前全跑完——页面要等它们结束才更新。所以别在微任务里写死循环或超长计算,否则页面假死。requestAnimationFrame 的回调属于「渲染前」的宏任务,适合做动画;requestIdleCallback 是「浏览器空闲时」才执行,适合非紧急后台活儿。
面试常客
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('promise3'); resolve(); })
.then(() => console.log('promise3 then'));
console.log('script end');
await 后面的代码会被包成微任务。整体顺序:
script start → async1 start → async2 → promise3 → script end
→ async1 end → promise3 then → setTimeout
记住一条主线就够了
同步代码 → 清空微任务 → 渲染(可能)→ 取一个宏任务 → 再清空微任务……
理解了它,Promise、async/await、setTimeout 的执行顺序就再也不会乱。

浙公网安备 33010602011771号