前端开发
深入 JavaScript 事件循环:宏任务与微任务
<>
单线程的 JavaScript
JS 是单线程语言,但浏览器要同时处理用户交互、网络请求、渲染。靠的就是事件循环:主线程空闲时,从任务队列里取任务执行。理解事件循环 = 理解一切异步代码的执行时机。
调用栈与任务队列
调用栈(同步执行区)
↓ 空闲
事件循环 → 宏任务队列(setTimeout / setInterval / IO / 用户事件)
→ 微任务队列(Promise.then / queueMicrotask / MutationObserver)
规则只有两条:
- 微任务优先:每次执行完一个宏任务,立即清空整个微任务队列
- 宏任务逐个:宏任务队列每次只取一个执行,再回微任务
经典输出题
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);
事件循环是浏览器的心跳 -- 理解了宏任务与微任务的节奏,异步代码的顺序就不再是"背答案",而是"推公式"。