深入理解 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.thenqueueMicrotaskMutationObserver 高,插队
宏任务(macrotask) setTimeoutsetInterval、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

记住一条主线就够了

同步代码 → 清空微任务 → 渲染(可能)→ 取一个宏任务 → 再清空微任务……

理解了它,Promiseasync/awaitsetTimeout 的执行顺序就再也不会乱。


原文链接:https://blog.fateguy.com/posts/js-event-loop

posted @ 2026-08-19 15:03  adiynil  阅读(1)  评论(0)    收藏  举报