ui 渲染穿插在宏任务与微任务之间,具体做法是浏览器在每次事件循环迭代中,先执行完所有微任务,然后检查是否需要触发 ui 渲染(如重排和重绘),最后才执行下一个宏任务。这意味着如果微任务队列中有大量耗时操作,会阻塞后续的 ui 更新,导致页面卡顿。

微任务执行对 UI 渲染的阻塞机制是什么
微任务(如 Promise.then、MutationObserver)在当前宏任务结束后立即执行,且必须全部执行完毕才会进行 UI 渲染。如果在微任务中修改了 DOM 样式或结构,这些变更不会立即反映在屏幕上,而是等到微任务队列清空后,浏览器才会计算布局并绘制像素。这种机制保证了数据状态的一致性,但也意味着开发者无法在微任务中间获取最新的视觉反馈。

console.log('1. MacroTask Start');
setTimeout(() => {
console.log('2. setTimeout (MacroTask)');
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise (MicroTask)');
});
console.log('4. MacroTask End');
上述代码的执行顺序为:首先输出 1. MacroTask Start,接着输出 4. MacroTask End,此时当前宏任务结束。随后进入微任务阶段,输出 3. Promise (MicroTask)。在此之后,浏览器会检查是否有待处理的 UI 渲染请求,如果有则执行渲染。最后,事件循环进入下一轮,执行 setTimeout 回调,输出 2. setTimeout (MacroTask)。这证明了微任务总是在 UI 渲染之前完成。
如何确保 DOM 修改后立即触发 UI 渲染
若需强制在特定时刻触发 UI 渲染,应将 DOM 操作放入宏任务中,或利用 requestAnimationFrame。因为 requestAnimationFrame 的回调是在浏览器准备绘制下一帧之前执行的,它位于微任务之后、宏任务之前,专门用于同步动画和视觉更新。将逻辑放在 requestAnimationFrame 中可以确保在渲染前完成必要的计算,并获得准确的布局信息。

function updateUI() {
// 修改 DOM
document.body.style.backgroundColor = 'red';
// 强制触发重排/重绘
requestAnimationFrame(() => {
console.log('Rendered at frame:', performance.now());
// 此时屏幕已更新
});
}
// 模拟一个长耗时宏任务
setTimeout(updateUI, 0);
在这个示例中,updateUI 函数被包裹在 setTimeout 中,作为一个宏任务执行。当该宏任务运行时,它修改了背景色,并注册了一个 requestAnimationFrame 回调。这个回调会在当前宏任务结束后、下一次绘制前执行。由于 requestAnimationFrame 本身不属于微任务也不属于标准宏任务,而是由浏览器调度器在渲染阶段插入,因此它能确保在视觉更新时点执行逻辑。如果直接在微任务中做同样的事,虽然也能修改 DOM,但无法保证在渲染前的精确时机进行后续处理,且可能因微任务过长而阻塞渲染。
长时间运行的微任务会导致什么具体问题
长时间运行的微任务会导致主线程阻塞,进而延迟 UI 渲染和用户交互响应。例如,在处理大型数据集时,如果使用递归调用 Promise.resolve().then() 来分片处理,虽然避免了宏任务的排队延迟,但如果单批处理时间过长或批次间没有间隔,依然会占用整个事件循环周期,导致页面失去响应。正确的做法是使用 setTimeout 或 queueMicrotask 配合分片策略,或者使用 Web Workers 将计算卸载到后台线程。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











