事件循环由宿主环境实现,JavaScript引擎只负责同步执行与调用栈;其核心是“轮询—清空微任务队列—执行宏任务”的严格顺序,宏任务包括setTimeout等,微任务包括Promise.then等,且Node.js与浏览器实现存在阶段差异。

事件循环不是 JavaScript 引擎自己写的代码
它是由宿主环境(比如浏览器或 Node.js)实现的运行时机制,JavaScript 引擎本身只负责执行同步代码和维护调用栈。你写的 setTimeout、Promise.then、fetch 这些异步操作,最终都依赖宿主环境把回调“塞进”事件循环的对应队列里。
关键点在于:事件循环不“主动调度”,它只是不断轮询——检查调用栈是否为空,如果空了,就从任务队列里取一个最老的任务执行。
宏任务和微任务队列必须分清
宏任务(macrotask)包括:setTimeout、setInterval、I/O、UI 渲染;微任务(microtask)包括:Promise.then/catch/finally、MutationObserver、queueMicrotask。
执行顺序严格是:一次宏任务 → 清空全部当前微任务队列 → 下一次宏任务。这意味着哪怕你在 setTimeout 回调里又 Promise.resolve().then(...),那个 then 也会在下一轮宏任务之前执行。
立即学习“Java免费学习笔记(深入)”;
console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve().then(() => console.log(3)); console.log(4); // 输出:1 → 4 → 3 → 2
async/await 只是 Promise 的语法糖,不改变队列本质
await 后面的表达式一旦返回的是 Promise,函数就会暂停,并把后续代码包装成微任务推入微任务队列。它不会让出线程,也不会变成宏任务。
-
await不等于 “等一会儿”,而是“等这个 Promise settle 后,把剩下逻辑作为微任务排队” - 连续多个
await不会插入宏任务间隙,除非你显式用了setTimeout或类似 API -
await Promise.resolve()和await Promise.reject()都立即进入微任务队列,区别只在错误是否被catch
Node.js 和浏览器的事件循环阶段有差异
浏览器事件循环相对简化,而 Node.js 的 libuv 实现了更细粒度的阶段划分:timers → pending callbacks → idle/prepare → poll → check → close callbacks。其中 setImmediate 属于 check 阶段,setTimeout(fn, 0) 属于 timers 阶段,二者执行顺序并不总是一致。
常见陷阱:
- 在 Node.js 中,
setTimeout(fn, 0)和setImmediate(fn)谁先执行,取决于进入事件循环 poll 阶段时是否有 I/O 回调待处理 -
process.nextTick()比所有微任务还优先,它会在当前操作结束后、任何微任务前执行,但滥用会导致 I/O 饥饿 - 浏览器没有
setImmediate和nextTick,别直接照搬 Node.js 代码
Promise.all 或 Web Worker 的误用。











