什么是事件循环_javascript的异步机制如何运行【教程】

JavaScript异步靠单线程+事件循环实现;宏任务(如setTimeout)每轮只执行一个,之后清空全部微任务(如Promise.then、queueMicrotask);微任务在本轮宏任务结束后立即、一次性执行完。

JavaScript 的异步不是靠多线程实现的,它靠的是单线程 + 事件循环(Event Loop)——这个机制决定了 setTimeoutPromise、用户点击、网络响应等何时真正执行。

宏任务和微任务怎么分?顺序怎么排?

事件循环每次只处理一个宏任务(macrotask),比如 script 全局代码、setTimeout 回调、setInterval 回调、I/O 回调;而每个宏任务执行完后,会清空全部当前轮次的微任务(microtask),比如 Promise.thenMutationObserver 回调、queueMicrotask

  • setTimeout(() => console.log('timeout'), 0) 是宏任务,下一轮才执行
  • Promise.resolve().then(() => console.log('promise')) 是微任务,本轮宏任务结束后立刻执行
  • 多个 Promise.then 会按注册顺序依次执行,不会被中间插入的 setTimeout 打断
  • queueMicrotaskPromise.then 属于同一级微任务队列,但前者更“原始”,无异常捕获逻辑

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

因为 setTimeout 的回调会被放入宏任务队列,必须等当前所有同步代码 + 当前轮次全部微任务执行完,才能轮到它。即使设为 0,也只是“尽快安排”,不等于“马上运行”。

  • 浏览器对最小延迟有强制限制(通常 ≥4ms),在非活跃标签页中可能升至 1000ms
  • Node.js 中 setTimeout(fn, 0) 实际等价于 setImmediate(fn)(在 I/O 队列之后、下次事件循环之前)
  • 想比 setTimeout 更快触发异步逻辑,用 queueMicrotask(fn)Promise.resolve().then(fn)

async/await 底层怎么跟事件循环配合?

async 函数本身是同步执行的,遇到 await 后,如果后面是个非 Promise 值,会立即包装成已 resolve 的 Promise,然后暂停函数,并把后续代码变成微任务推入队列。

  • await 123 等价于 await Prom

    ise.resolve(123)
    ,后续代码进入微任务
  • await fetch('/api') 会等待网络完成,完成后将 .then 回调作为微任务加入队列
  • 连续多个 await 不会“阻塞事件循环”,只是把每个 await 后的逻辑拆成独立微任务,仍受微任务清空规则约束
  • 不要在循环里密集写 await sleep(10),这会累积大量微任务,可能卡住 UI 或拖慢响应

真正容易被忽略的,是微任务队列在每次宏任务结束时“一次性清空”的特性——哪怕你在某个 Promise.then 里又注册了十个新的 Promise.then,它们都会在这轮里全部执行完,不会等到下一轮宏任务才开始。这个细节直接影响错误边界、状态更新节奏和性能敏感场景的表现。