JavaScript异步靠单线程+事件循环实现;宏任务(如setTimeout)每轮只执行一个,之后清空全部微任务(如Promise.then、queueMicrotask);微任务在本轮宏任务结束后立即、一次性执行完。
JavaScript 的异步不是靠多线程实现的,它靠的是单线程 + 事件循环(Event Loop)——这个机制决定了 setTimeout、Promise、用户点击、网络响应等何时真正执行。
事件循环每次只处理一个宏任务(macrotask),比如 script 全局代码、setTimeout 回调、setInterval 回调、I/O 回调;而每个宏任务执行完后,会清空全部当前轮次的微任务(microtask),比如 Promise.、
MutationObserver 回调、queueMicrotask。
setTimeout(() => console.log('timeout'), 0) 是宏任务,下一轮才执行Promise.resolve().then(() => console.log('promise')) 是微任务,本轮宏任务结束后立刻执行Promise.then 会按注册顺序依次执行,不会被中间插入的 setTimeout 打断queueMicrotask 和 Promise.then 属于同一级微任务队列,但前者更“原始”,无异常捕获逻辑因为 setTimeout 的回调会被放入宏任务队列,必须等当前所有同步代码 + 当前轮次全部微任务执行完,才能轮到它。即使设为 0,也只是“尽快安排”,不等于“马上运行”。
setTimeout(fn, 0) 实际等价于 setImmediate(fn)(在 I/O 队列之后、下次事件循环之前)setTimeout 更快触发异步逻辑,用 queueMicrotask(fn) 或 Promise.resolve().then(fn)
async 函数本身是同步执行的,遇到 await 后,如果后面是个非 Promise 值,会立即包装成已 resolve 的 Promise,然后暂停函数,并把后续代码变成微任务推入队列。
await 123 等价于 await Promise.resolve(123),后续代码进入微任务await fetch('/api') 会等待网络完成,完成后将 .then 回调作为微任务加入队列await 不会“阻塞事件循环”,只是把每个 await 后的逻辑拆成独立微任务,仍受微任务清空规则约束await sleep(10),这会累积大量微任务,可能卡住 UI 或拖慢响应真正容易被忽略的,是微任务队列在每次宏任务结束时“一次性清空”的特性——哪怕你在某个 Promise.then 里又注册了十个新的 Promise.then,它们都会在这轮里全部执行完,不会等到下一轮宏任务才开始。这个细节直接影响错误边界、状态更新节奏和性能敏感场景的表现。