如何理解Javascript的事件循环与异步编程机制?

12次阅读

事件循环每次清空调用后先执行所有微任务(promise.then、queueMicrotask等),再执行一个宏任务(setTimeout、requestAnimationFrame等);微任务按入队顺序执行且不可中断,宏任务每次仅取一个。

如何理解Javascript的事件循环与异步编程机制?

javaScript 的事件循环不是“等所有同步代码跑完再执行异步”,而是由调用、任务队列(宏任务/微任务)、以及执行上下文共同协作的实时调度机制。理解它关键不在于背流程,而在于知道 setTimeoutPromise.thenqueueMicrotask 这些 API 到底往哪塞、什么时候取、谁先谁后。

宏任务和微任务的执行顺序怎么定?

每次调用栈清空后,引擎会:先执行所有已入队的微任务(Promise.thenqueueMicrotaskMutationObserver),再取一个宏任务(setTimeoutsetIntervalI/Oui 渲染)执行。这个“一次只取一个宏任务”的规则,是很多嵌套 setTimeout 表现不符合直觉的根本原因。

常见错误现象:以为 setTimeout(() => console.log(1), 0) 一定在 Promise.resolve().then(() => console.log(2)) 后面输出 —— 实际上是 2 先,1 后,因为前者是微任务,后者是宏任务。

  • 微任务队列在每次宏任务结束后立即清空,且不会被新的微任务插入打断(即递归调用 queueMicrotask 也会等本轮全部跑完)
  • await 后的代码会被包装成微任务,等价于 Promise.then
  • requestAnimationFrame 是宏任务,但浏览器会在下一次重绘前执行,时机比 setTimeout(..., 0) 更早(不过仍晚于微任务)

Promise.thenqueueMicrotask 有啥区别

两者都进微任务队列,但语义和使用场景不同:Promise.then 绑定的是 Promise 状态变化后的回调,自带错误捕获和链式传递;queueMicrotask 是纯粹的“下一轮微任务调度”,无状态、无异常透传,适合解耦或避免 Promise 构造开销。

立即学习Java免费学习笔记(深入)”;

性能影响:频繁创建 Promise(比如在循环里写 Promise.resolve().then(...))会产生额外对象分配;queueMicrotask 更轻量,但无法像 Promise 那样自然处理异步错误传播。

queueMicrotask(() => console.log('micro1')); Promise.resolve().then(() => console.log('promise1')); queueMicrotask(() => console.log('micro2')); // 输出顺序一定是:micro1 → promise1 → micro2 // 因为所有微任务按入队顺序执行,不分来源

为什么 setTimeout(..., 0) 不是立刻执行?

因为 setTimeout 的回调被放入宏任务队列,必须等当前调用栈 + 所有微任务全部完成之后,才可能被执行。即使设为 0,它也要排队等“下一轮事件循环”的开始 —— 而不是“下一毫秒”。

容易踩的坑:

  • 误以为 setTimeout(fn, 0) 能让 dom 更新“立刻可见”:实际上它仍晚于微任务,也晚于当前帧的渲染时机;真要等渲染后执行,该用 requestAnimationFramesetTimeout 套两层
  • node.js 中,setImmediatesetTimeout(..., 0) 所处阶段不同(check 阶段 vs timer 阶段),执行顺序受当前事件循环阶段影响,不可假设一致
  • 浏览器对非常小的超时值(如 1ms)会自动节流到 4ms,这是规范要求,不是 bug

真正卡住理解的,往往不是“宏任务微任务谁先”,而是没意识到:事件循环每轮只取一个宏任务,但微任务可以无限嵌套(只要不爆栈),而且 UI 渲染本身就是一个隐式的宏任务节点 —— 它不会插在微任务中间,也不会跳过。这点在做动画、表单校验、或者调试 reactuseEffect 执行时机时,特别容易翻车。

text=ZqhQzanResources