promise回调在ui渲染前执行,因浏览器规定微任务队列必须在每次渲染前被完全清空;顺序为宏任务→全部微任务→(可能)渲染→下一宏任务。

Promise 的回调比 UI 渲染更早执行,不是因为它“抢在渲染前面”,而是因为浏览器明确规定:**微任务队列必须在每次渲染前被完全清空**。这是事件循环的硬性规则,不是优先级高低的主观选择。
微任务执行时机是固定的
只要当前宏任务(比如一次点击、一段 script 脚本、一个 setTimeout 回调)的同步代码执行完毕、调用栈变空,引擎立刻检查微任务队列,并连续执行其中所有任务——不管有多少个,也不管它们是最初就排队的,还是在执行过程中新加入的。
- 常见微任务包括:Promise.then/catch/finally、queueMicrotask()、MutationObserver 回调
- 这个过程不中断、不穿插、不延迟,也**不等待渲染信号**
- 哪怕你在 Promise.then 里又注册了一个 queueMicrotask,它也会被本轮执行
渲染只发生在微任务之后
浏览器不会在每个 DOM 修改后立即重绘。它只在“有渲染机会”时(如屏幕即将刷新、页面处于前台、布局已变更)才触发一次渲染,而这个机会**严格排在微任务清空之后**。
- 典型顺序是:宏任务 → 微任务(全部)→ (可能)渲染 → 下一个宏任务
- 这意味着:你用 JavaScript 修改了 innerHTML 或 class,这些变更已写入 DOM 树,但用户还看不到;紧接着 Promise.then 就能读取到最新 DOM 状态,且不会引发额外重排
- 如果改用 setTimeout,回调会落到下一个宏任务,中间可能插入渲染、其他事件或用户交互,状态可能已不同
为什么设计成这样?
这种安排是为了避免状态撕裂和逻辑不可预测。
- MutationObserver 必须在渲染前拿到完整的 DOM 变更记录,否则观察结果就不完整
- await 后续代码依赖微任务衔接,跳过会导致异步流程错乱
- 开发者可以确信:在 Promise.then 里读 DOM,看到的就是上一步同步操作后的最终结构
一个直观例子
运行以下代码:
console.log('A'); // 同步
document.body.innerHTML = '<div id="test">Hello</div>'; // 同步修改 DOM
Promise.resolve().then(() => console.log('B')); // 微任务
console.log('C'); // 同步
// 输出顺序一定是:A → C → B;而页面上 'Hello' 要等到 B 执行完才可能显示
这里 A、C 是同步代码,B 是微任务,它一定在同步结束后、渲染前执行。用户不会看到中间态,也不会看到 B 在 C 前输出。











