浏览器里的 JavaScript 是单线程的,但页面并不会因为一个网络请求卡住。原因在于调用栈只执行同步代码,异步任务完成后会把回调放进队列,等调用栈空了再取出执行。
一轮事件循环做了什么
每一轮大致按这个顺序:先执行当前调用栈里的同步代码,调用栈清空后取出所有微任务逐个执行,微任务队列清空后才可能发生渲染,然后进入下一个宏任务。
JavaScript
console.log("1 同步");
setTimeout(() => console.log("4 宏任务"), 0);
Promise.resolve().then(() => console.log("3 微任务"));
console.log("2 同步");
// 输出顺序:1 同步 → 2 同步 → 3 微任务 → 4 宏任务微任务先于下一次渲染
如果把耗时逻辑放进微任务,它会排在渲染之前执行,页面的样式更新会被推迟。这也是为什么大循环里连续使用 Promise 会让人感觉页面卡顿,而不是分批更新。
- 微任务:Promise 回调、queueMicrotask、MutationObserver
- 宏任务:setTimeout、事件回调、网络请求完成回调
- 需要让页面先刷新:把逻辑放进 setTimeout 或 requestAnimationFrame
await 之后的代码为什么像微任务
async 函数里的 await 会把后面的代码挂起,等 Promise 落定后再恢复执行,而恢复的过程正是排进微任务队列。所以 await 之后的语句虽然写在函数里,执行时机依然晚于当前的同步代码。
记住清空顺序就够了:同步执行完,微任务清空,再渲染,再取下一个宏任务。


