微任务总在宏任务结束后立即被彻底清空,即引擎持续执行微任务队列直至为空,期间新产生的微任务会追加到队尾并继续执行,之后才进行UI渲染或取下一个宏任务。

宏任务执行完立刻清空全部微任务,不是靠“检查一遍就停”,而是由 JavaScript 引擎在每次宏任务退出时主动轮询微任务队列,直到它变空——这个过程是同步、不可中断、且递归包容新加入任务的。
微任务队列是“执行中可追加”的活跃队列
微任务队列不是静态快照。当一个 Promise.then 回调正在执行时,它内部再创建新的 Promise 或调用 queueMicrotask,新微任务会**立即追加到当前队列末尾**,引擎不会跳过它们。这意味着:
- 引擎不按“初始长度”执行固定次数,而是持续取队首、执行、再看是否还有
- 只要队列非空,就继续执行;一旦为空,才退出微任务阶段
- 这种机制天然支持链式 Promise(.then().then().then())自动连贯执行
引擎层面强制“微任务耗尽”策略
V8、SpiderMonkey 等主流引擎在源码中明确实现:在宏任务返回后,进入一个 while 循环,反复调用 MicrotaskQueue::PerformMicrotaskCheckpoint,直到队列 size === 0。这不是调度优化,而是规范要求——ECMAScript 标准规定 “host must perform microtask checkpoint”,即宿主环境必须清空全部待处理微任务。
这和宏任务队列的 FIFO 调度有本质区别:宏任务每次只取一个;微任务是“一锅端”,且端得彻底。
清空动作发生在宏任务与渲染/下一个宏任务之间
这个“清空”不是独立阶段,而是事件循环流程中的确定环节:
- 同步代码(属于当前宏任务)执行完毕
- 立即转入微任务执行循环,期间不响应任何宏任务(包括刚到期的 setTimeout)
- 微任务队列清空后,浏览器才可能触发 UI 渲染(如有变更)
- 之后才从宏任务队列取下一个任务(如 setTimeout 回调)
因此,“清空所有”不是建议或优化,而是保障微任务高优先级和一致性的底层契约。
为什么不能只执行一个微任务?
如果只执行一个,就会破坏 Promise 链语义。例如:
Promise.resolve().then(() => console.log(1)).then(() => console.log(2));
第一个 then 是微任务 A,它返回新 Promise,其 then 回调是微任务 B。若只执行 A 就停,B 就要等到下一轮宏任务后——这违背 Promise 的“异步但紧邻”设计初衷。清空机制确保了链式逻辑的原子性与时序可控。

















