微任务总在UI渲染前执行完,因事件循环规范要求宏任务后必须清空全部微任务队列才允许渲染;Promise.then会隐式包装值并创建新Promise,queueMicrotask语义更纯粹、无状态转换。

微任务为什么总在 UI 渲染前执行完?
因为浏览器的事件循环规范强制要求:每次宏任务(如 setTimeout 回调、事件处理函数)执行完毕后,必须同步清空当前所有微任务队列,然后才允许进行下一帧的 UI 渲染或进入下一个宏任务。
这不是 JS 引擎的“优化”,而是渲染管线的硬性阶段顺序。你可以把它理解为“宏任务 → 微任务(全部)→ 渲染 → 下一个宏任务”这一铁律。
Promise.then 和 queueMicrotask 的行为差异在哪?
两者都进微任务队列,但触发时机和可读性不同:
-
Promise.then会创建新 Promise,若链式调用中返回非 Promise 值,后续.then仍会包装成 resolved 状态,容易隐式延迟; -
queueMicrotask是纯调度 API,不涉及状态转换,语义更清晰,适合“就是想在这里插一条微任务”的场景; - 兼容性上:
queueMicrotask在 Node.js 11+ 和现代浏览器(Chrome 71+、Firefox 69+、Safari 15.4+)支持;老环境需 fallback 到Promise.resolve().then。
UI 渲染被阻塞的典型误判场景
很多人以为“只要用了微任务,UI 就一定不卡”,其实不然。关键看微任务里干了什么:
- 如果某个
Promise.then回调里执行了耗时 200ms 的计算(比如遍历大数组、正则匹配超长文本),它会阻塞渲染,因为微任务是同步逐个执行的; - 连续触发 100 次
queueMicrotask,每个都做一点事,也会让渲染延迟到这 100 个全部跑完; - 没有“微任务节流”机制——不像
requestIdleCallback那样能切片执行,微任务一旦入队,就注定要一口气跑完。
如何验证微任务是否真的在渲染前清空?
最直接的办法是结合 performance.now() 和 requestAnimationFrame 打点:
console.log('start');
Promise.resolve().then(() => console.log('microtask 1'));
Promise.resolve().then(() => console.log('microtask 2'));
requestAnimationFrame(() => console.log('rAF'));
setTimeout(() => console.log('timeout'), 0);
// 输出顺序一定是:
// start
// microtask 1
// microtask 2
// rAF
// timeout
注意:rAF 回调在微任务之后、渲染之前执行,但它本身不触发渲染;真正的渲染发生在 rAF 回调返回之后。如果你在 rAF 里修改了 DOM 样式,浏览器会在下一次帧开始前应用变更——而这个“下一次帧”,已经等不及任何尚未执行的微任务了。
真正容易被忽略的是:微任务队列清空是**单次宏任务粒度**的。跨宏任务的微任务不会合并执行,也不会提前透支执行。写异步逻辑时,别只盯着“是不是微任务”,更要盯住“它里面有没有重活”。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











