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 渲染之前完成。
立即学习“Java免费学习笔记(深入)”;
如何确保 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 将计算卸载到后台线程。


















