前端开发

深入 JavaScript 事件循环:宏任务与微任务

2025-05-08 wangjun 14 min read
<>

单线程的 JavaScript

JS 是单线程语言,但浏览器要同时处理用户交互、网络请求、渲染。靠的就是事件循环:主线程空闲时,从任务队列里取任务执行。理解事件循环 = 理解一切异步代码的执行时机。

调用栈与任务队列

调用栈(同步执行区)
  ↓ 空闲
事件循环 → 宏任务队列(setTimeout / setInterval / IO / 用户事件)
           → 微任务队列(Promise.then / queueMicrotask / MutationObserver)

规则只有两条:

  1. 微任务优先:每次执行完一个宏任务,立即清空整个微任务队列
  2. 宏任务逐个:宏任务队列每次只取一个执行,再回微任务

经典输出题

console.log("1");                     // 同步

setTimeout(() => console.log("2"), 0); // 宏任务

Promise.resolve().then(() => {
  console.log("3");                    // 微任务
}).then(() => console.log("4"));      // 微任务

console.log("5");                     // 同步


推导:同步先跑完(1、5),然后清空微任务(3、4),最后才轮到宏任务 setTimeout(2)。

async/await 的本质

await 不是"暂停",而是把后续代码包装成 .then 里的微任务:

async function foo() {
  console.log("A");
  await bar();          // bar() 立即执行,返回值后的代码变微任务
  console.log("C");
}

function bar() {
  console.log("B");
  return Promise.resolve();
}

foo();                  // A → B → C(C 在微任务阶段)

与渲染的时机

渲染(Paint)发生在宏任务之间。所以:

  • 想在"下一帧渲染前"更新动画,用 requestAnimationFrame(它比 setTimeout 更贴近渲染时机)
  • 把大量微任务塞在同一个宏任务里,可能阻塞渲染 -- 长任务要用 setTimeout 分片
  • setTimeout(fn, 0) 的实际延迟约 4ms(浏览器最小节流),不适合做精确计时
// 用 rAF 驱动动画,保证与帧同步
function animate() {
  update(performance.now());
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
事件循环是浏览器的心跳 -- 理解了宏任务与微任务的节奏,异步代码的顺序就不再是"背答案",而是"推公式"。