JS事件循环是调用栈、宏/微任务队列与执行上下文协同的实时调度机制;宏任务每执行完一个,即清空全部当前微任务队列,再取下一个宏任务。

JS 事件循环不是“等代码跑完再处理异步”,而是由调用栈、任务队列(宏任务/微任务)、执行上下文共同协作的实时调度机制。理解它,关键不在背概念,而在看清代码执行时“谁先谁后、谁插队、谁等谁”。
宏任务与微任务的执行顺序不能靠直觉猜
每次宏任务执行完毕后,会清空当前所有微任务队列,再取下一个宏任务——这不是“一次只执行一个微任务”,而是“一次性执行完本轮产生的全部微任务”。比如:
setTimeout(() => console.log('timer1'), 0);
Promise.resolve().then(() => console.log('p1')).then(() => console.log('p2'));
setTimeout(() => console.log('timer2'), 0);
Promise.resolve().then(() => console.log('p3'));
输出一定是:p1 → p2 → p3 → timer1 → timer2。因为两个 setTimeout 是宏任务,被推入宏任务队列末尾;而所有 then 是微任务,在第一个宏任务(同步脚本)结束后立刻批量执行。
async/await 的 await 并不“暂停整个函数”,只暂停后续代码进入微任务
await 后面的表达式一旦返回 Promise,当前函数剩余部分会被包装成微任务。注意:await 前的代码仍是同步执行的。
-
async function f() { console.log(1); await Promise.resolve(); console.log(2); }→ 先输出 1,再输出 2(在微任务中) -
console.log(0); f(); console.log(3);→ 输出顺序是 0 → 1 → 3 → 2,不是 0 → 1 → 2 → 3 - 多个 await 连续写,每个 await 后都可能插入其他微任务(比如其他 Promise.then),不要默认它们“串行阻塞”
定时器不准 + setImmediate / queueMicrotask 的兼容性陷阱
setTimeout(fn, 0) 不保证立即执行,实际延迟取决于当前调用栈是否清空、系统调度、浏览器最小间隔(通常 ≥4ms)。更危险的是:
-
setImmediate只存在于旧版 Node.js(已废弃),浏览器中完全不可用 -
queueMicrotask是标准 API,用于手动调度微任务,但 IE 和部分老安卓 WebView 不支持,需降级为Promise.resolve().then() - 不要用
setTimeout(fn, 0)模拟微任务,它无法替代queueMicrotask或Promise.then的时机
真实场景中容易踩的坑
-
DOM 更新未生效就取值:Vue/React 的响应式更新、
el.innerHTML = xxx后立即el.offsetHeight,可能拿不到新布局——用queueMicrotask或Promise.resolve().then()等到重排前,或用requestAnimationFrame等到重绘前 - 错误地认为“await fetch() 后 DOM 就渲染完了”:fetch 完成只是数据就位,渲染依赖后续 setState / reactivity 触发,且 setState 在 React 中是异步批处理,时机独立于 fetch
- 在 for 循环里写 setTimeout(i => console.log(i), 0),结果全输出 5(var 声明+闭包)——这不是事件循环问题,但常被误归类;本质是作用域和变量提升,用 let 或闭包修复


















