
Microtask 并不是“优于”事件响应,而是在事件响应所触发的宏任务结束后立即执行。事件响应(如 click、input、load)本身属于宏任务,它执行完同步部分后,必须清空当前所有微任务,之后才可能处理下一个宏任务(包括后续事件或定时器)。这个机制决定了 Promise 回调、queueMicrotask 等总能比下一轮 UI 事件或 setTimeout 更早运行。
事件响应本身就是宏任务
用户点击按钮、页面加载完成、键盘输入等,均由浏览器发起并推入宏任务队列。一次点击对应一个宏任务,其内部同步代码(比如事件监听器函数体)会立即执行,但其中注册的微任务不会当场运行,而是排队等待该宏任务“收尾”。
- 例如:监听器里写
Promise.resolve().then(() => console.log('done')),这个then不会在监听器函数结束时立刻打印,而是在整个点击宏任务执行完毕、调用栈清空后,立刻执行 - 这意味着:哪怕两个监听器绑定在同一元素上,它们各自的微任务也会被合并到同一个微任务队列中,在第一个监听器执行完、第二个监听器执行完之后,统一清空
微任务清空发生在每次宏任务出口处
事件循环规则强制要求:每个宏任务执行结束后,必须一次性、无中断地执行完所有已排队的微任务。这个“清空”动作不可跳过、不可延迟,也不受新加入微任务影响——新加入的也会被本轮执行。
- 所以,
click触发的宏任务 → 执行监听器同步逻辑 → 遇到Promise.then入微任务队列 → 监听器函数返回 → 宏任务结束 → 立即执行全部微任务(包括刚加进来的) - 这解释了为什么在连续绑定多个 click 监听器时,所有同步日志先输出,然后所有
Promise.then才集中输出,而非“监听器1 → 微任务1 → 监听器2 → 微任务2”
UI 渲染发生在微任务之后、下个宏任务之前
浏览器会在一次宏任务 + 全部微任务执行完毕后,进入渲染阶段(重排重绘),然后再取下一个宏任务。这就让微任务天然具备“更新状态后、渲染前”的能力。
- 比如在
click中修改 DOM 并用Promise.then做后续逻辑,DOM 变更已生效,但页面尚未刷新;微任务内可安全读取最新布局信息,且不会触发额外渲染 - 而如果改用
setTimeout,就要等到下次宏任务,中间可能插入渲染、其他事件,状态可能已被覆盖或失效
不能靠微任务“抢在事件前”执行
微任务无法在事件发生前运行,也不能打断正在执行的事件监听器。它的优势是确定性时机:紧接在事件响应的同步部分之后、渲染之前。这不是抢占,而是事件循环设计赋予的精确控制点。
- 想在用户点击前做准备?得靠同步逻辑或提前注册的
MutationObserver或requestIdleCallback - 想确保某段逻辑在 DOM 更新后、界面刷新前运行?微任务是标准且可靠的选择
- 想跨事件批次协调状态?微任务提供比宏任务更紧凑的执行窗口,减少竞态风险










