JavaScript事件循环完全解析—宏任务微任务与浏览器渲染时机|新宇宙博客Back to list宏任务、微任务与浏览器渲染循环
Site Owner
Published on 2026-05-21
系统讲解浏览器事件循环完整模型、Task Queue与Microtask Queue的清空时机、渲染时机优化及Node.js事件循环差异

宏任务、微任务与浏览器渲染循环
事件循环(Event Loop)是 JavaScript 实现异步非阻塞 I/O 的核心机制。它协调宏任务(macrotask)、微任务(microtask)和浏览器渲染三者的执行时序。理解事件循环对于优化页面性能、避免 UI 卡顿、正确预测代码执行顺序至关重要。Node.js 的事件循环与浏览器存在显著差异,本文将同时覆盖两种环境。
目录
- 事件循环的架构模型
- 宏任务队列详解
- 微任务队列与执行时机
- 浏览器渲染循环集成
- Node.js 事件循环的六个阶段
- 经典执行顺序题解析
- queueMicrotask 与调度策略
- 实战案例
- 深度追问
- 总结表格
1. 事件循环的架构模型
浏览器事件循环(HTML Living Standard §8.1.7)的核心流程:
┌─────────────────────────────────────────────┐
│ Event Loop │
│ │
│ ┌─────────────┐ │
│ │ Task Queue │ ← setTimeout, I/O, │
│ │ (macrotask) │ setInterval, UI events │
│ └──────┬──────┘ │
│ │ 取一个任务 │
│ ▼ │
│ ┌─────────────┐ │
│ │ 执行任务 │ ← 同步代码执行 │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Microtask Queue │ ← Promise.then, │
│ │ (全部清空) │ MutationObserver, │
│ └──────┬──────────┘ queueMicrotask │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Rendering │ ← rAF, Style, Layout, │
│ │ (可能跳过) │ Paint, Composite │
│ └─────────────────┘ │
└─────────────────────────────────────────────┘
console.log('1. 同步');
setTimeout(() => {
console.log('4. 宏任务');
}, 0);
Promise.resolve().then(() => {
console.log('3. 微任务');
});
console.log('2. 同步');
2. 宏任务队列详解
setTimeout(() => console.log('setTimeout'), 0);
const channel = new MessageChannel();
channel.port1.onmessage = () => console.log('MessageChannel');
channel.port2.postMessage('');
if (typeof setImmediate !== 'undefined') {
setImmediate(() => console.log('setImmediate'));
}
requestAnimationFrame(() => console.log('rAF'));
setTimeout(fn, 0) 的真实延迟
function measureMinDelay() {
const timestamps = [];
let count = 0;
function tick() {
timestamps.push(performance.now());
if (++count < 20) {
setTimeout(tick, 0);
} else {
for (let i = 1; i < timestamps.length; i++) {
console.log(`Tick ${i}: ${(timestamps[i] - timestamps[i-1]).toFixed(2)}ms`);
}
}
}
tick();
}
measureMinDelay();
3. 微任务队列与执行时机
微任务的关键特性:在当前任务结束后、下一个任务开始前,清空所有微任务
console.log('start');
Promise.resolve().then(() => {
console.log('micro 1');
Promise.resolve().then(() => {
console.log('micro 2');
Promise.resolve().then(() => {
console.log('micro 3');
});
});
});
setTimeout(() => {
console.log('macro 1');
}, 0);
console.log('end');
微任务饥饿问题
function microTaskStarvation() {
let count = 0;
function recursiveMicro() {
if (count++ < 100000) {
Promise.resolve().then(recursiveMicro);
}
}
recursiveMicro();
}
4. 浏览器渲染循环集成
const box = document.getElementById('box');
box.style.left = '0px';
box.style.left = '100px';
box.style.left = '200px';
requestAnimationFrame(() => {
const rect = box.getBoundingClientRect();
box.style.transform = `translateX(${rect.left + 10}px)`;
});
完整的帧流水线
requestAnimationFrame(() => {
console.log('rAF - 渲染前');
const height = box.offsetHeight;
box.style.height = height + 10 + 'px';
});
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 5) {
processNextItem();
}
}, { timeout: 2000 });
5. Node.js 事件循环的六个阶段
┌───────────────────────────┐
┌─►│ timers │ ← setTimeout/setInterval 回调
│ └──────────┬────────────────┘
│ ┌──────────▼────────────────┐
│ │ pending callbacks │ ← 系统级回调(TCP 错误等)
│ └──────────┬────────────────┘
│ ┌──────────▼────────────────┐
│ │ idle, prepare │ ← 内部使用
│ └──────────┬────────────────┘
│ ┌──────────▼────────────────┐
│ │ poll │ ← I/O 回调,可能阻塞
│ └──────────┬────────────────┘
│ ┌──────────▼────────────────┐
│ │ check │ ← setImmediate 回调
│ └──────────┬────────────────┘
│ ┌──────────▼────────────────┐
│ │ close callbacks │ ← socket.on('close')
│ └──────────┬────────────────┘
│ │
└─────────────┘
const fs = require('fs');
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
6. 经典执行顺序题解析
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
解析过程
7. queueMicrotask 与调度策略
queueMicrotask(() => console.log('queueMicrotask'));
Promise.resolve().then(() => console.log('Promise.then'));
setTimeout(() => console.log('setTimeout'), 0);
const mc = new MessageChannel();
mc.port1.onmessage = () => console.log('MessageChannel');
mc.port2.postMessage('');
requestAnimationFrame(() => console.log('rAF'));
requestIdleCallback(() => console.log('rIC'));
scheduler.postTask (Scheduler API)
if ('scheduler' in globalThis) {
scheduler.postTask(() => console.log('background'), { priority: 'background' });
scheduler.postTask(() => console.log('user-visible'), { priority: 'user-visible' });
scheduler.postTask(() => console.log('user-blocking'), { priority: 'user-blocking' });
}
8. 实战案例
实战案例 1:长任务拆分避免卡顿
function processLargeArray(items, processFn, options = {}) {
const { chunkSize = 100, onProgress, signal } = options;
let index = 0;
return new Promise((resolve, reject) => {
function processChunk() {
if (signal?.aborted) {
reject(new DOMException('Aborted', 'AbortError'));
return;
}
const start = performance.now();
while (index < items.length) {
processFn(items[index], index);
index++;
if (index % chunkSize === 0 || performance.now() - start > 5) {
onProgress?.(index / items.length);
const mc = new MessageChannel();
mc.port1.onmessage = processChunk;
mc.port2.postMessage('');
return;
}
}
onProgress?.(1);
resolve();
}
processChunk();
});
}
const items = new Array(100000).fill(null).map((_, i) => i);
await processLargeArray(items, (item) => {
Math.sqrt(item) * Math.random();
}, {
chunkSize: 500,
onProgress: (p) => console.log(`${(p * 100).toFixed(1)}%`)
});
实战案例 2:批量 DOM 更新优化
class BatchDOMUpdater {
#pending = [];
#scheduled = false;
schedule(updateFn) {
this.#pending.push(updateFn);
if (!this.#scheduled) {
this.#scheduled = true;
requestAnimationFrame(() => {
const reads = this.#pending.filter(fn => fn.type === 'read');
const writes = this.#pending.filter(fn => fn.type === 'write');
reads.forEach(fn => fn());
writes.forEach(fn => fn());
this.#pending = [];
this.#scheduled = false;
});
}
}
read(fn) {
fn.type = 'read';
this.schedule(fn);
}
write(fn) {
fn.type = 'write';
this.schedule(fn);
}
}
const batcher = new BatchDOMUpdater();
elements.forEach(el => {
batcher.read(() => {
const height = el.offsetHeight;
batcher.write(() => {
el.style.height = height * 2 + 'px';
});
});
});
实战案例 3:Promise 微任务与 Vue nextTick
const callbacks = [];
let pending = false;
function nextTick(cb) {
return new Promise((resolve) => {
callbacks.push(() => {
if (cb) {
try { cb(); resolve(); }
catch (e) { reject(e); }
} else {
resolve();
}
});
if (!pending) {
pending = true;
Promise.resolve().then(flushCallbacks);
}
});
}
function flushCallbacks() {
pending = false;
const copies = callbacks.slice();
callbacks.length = 0;
for (const fn of copies) {
fn();
}
}
9. 深度追问
Q1:为什么 Vue 从 macrotask 切换到 microtask 实现 nextTick?
Vue 2.5+ 将 nextTick 实现从 MessageChannel(宏任务)切换为 Promise.then(微任务),原因是宏任务在状态变更和渲染之间有一帧延迟,导致闪烁。微任务在渲染前执行,可以在同一帧内完成状态更新和 DOM 修改。
Q2:requestAnimationFrame 的回调在什么阶段执行?
rAF 在浏览器准备绘制新一帧时执行,位于样式计算和布局之前。如果页面在后台(不可见),rAF 会暂停调用以节省资源。注意:rAF 并非严格的宏任务或微任务,它有自己独立的回调队列。
Q3:await 后的代码为什么比 .then 晚一个微任务?
在早期规范中,await 需要额外的 Promise 包装(多一个微任务 tick)。ES2020 优化了这一点:如果 await 的值已经是 fulfilled Promise,V8 可以直接安排微任务而不需要额外包装。这意味着现代引擎中 await 和 .then 的时序差异已经消除。
10. 总结表格
| 任务类型 | API | 执行时机 | 使用场景 |
|---|
| 宏任务 | setTimeout | 下一轮事件循环 | 延迟执行、长任务拆分 |
| 宏任务 | MessageChannel | 下一轮(比 setTimeout 快) | 内部调度 |
| 宏任务 | setImmediate | check 阶段(Node.js) | I/O 后立即执行 |
| 微任务 | Promise.then | 当前任务后、渲染前 | 异步流程控制 |
| 微任务 | queueMicrotask | 同上 | 显式微任务调度 |
| 微任务 | MutationObserver | 同上 | DOM 变更监听 |
| 渲染 | requestAnimationFrame | 渲染前 | 动画、DOM 测量 |
| 空闲 | requestIdleCallback | 帧间空闲 | 低优先级工作 |
| 浏览器 vs Node.js | 浏览器 | Node.js |
|---|
| 微任务清空时机 | 每个宏任务后 | 每个阶段切换时 |
| nextTick | 不存在 | 优先于微任务 |
| setImmediate | 不存在 | check 阶段 |
| rAF | 渲染前 | 不存在 |
| 最小 setTimeout 延迟 | 4ms(嵌套≥5次) | 1ms |