四大异步API执行时机详解
一、事件循环基础
完整的事件循环流程
┌─────────────────────────────────────────────────────┐
│ 事件循环一轮 │
├─────────────────────────────────────────────────────┤
│ │
│ 1️⃣ 执行同步代码 (Call Stack) │
│ ↓ │
│ 2️⃣ 执行所有微任务 (Microtasks) │
│ - Promise.then() │
│ - MutationObserver │
│ - queueMicrotask() │
│ ↓ │
│ 3️⃣ 渲染时机判断 (是否需要渲染) │
│ ↓ │
│ 【如果需要渲染,进入渲染流程】 │
│ ├─ requestAnimationFrame 回调 ⭐ │
│ ├─ 样式计算 (Recalc Styles) │
│ ├─ 布局 (Layout) │
│ ├─ 绘制 (Paint) │
│ ├─ 合成 (Composite) │
│ └─ requestIdleCallback (有空闲时间) ⭐ │
│ ↓ │
│ 4️⃣ 执行一个宏任务 (Macrotask) │
│ - setTimeout/setInterval │
│ - MessageChannel ⭐ │
│ - I/O │
│ - UI 交互事件 │
│ ↓ │
│ 5️⃣ 回到步骤 2,开始下一轮循环 │
│ │
└─────────────────────────────────────────────────────┘二、四大API详细对比
1. setTimeout
javascript
// 执行时机:宏任务队列
setTimeout(() => {
console.log('setTimeout');
}, 0);
// 特点:
// ✅ 在宏任务队列中
// ✅ 最小延迟 4ms(嵌套5层以上)
// ✅ 不精确,受其他任务影响
// ❌ 页面不可见时会被节流(最小1000ms)执行时间线:
同步代码 → 微任务 → [可能渲染] → setTimeout 回调 → 下一轮循环2. MessageChannel
javascript
// 执行时机:宏任务队列(比 setTimeout 优先)
const channel = new MessageChannel();
channel.port1.onmessage = () => {
console.log('MessageChannel');
};
channel.port2.postMessage('');
// 特点:
// ✅ 在宏任务队列中
// ✅ 比 setTimeout 更快(没有 4ms 限制)
// ✅ 不受页面可见性影响
// ✅ 更精确和可控执行时间线:
同步代码 → 微任务 → [可能渲染] → MessageChannel 回调 → 下一轮循环3. requestAnimationFrame
javascript
// 执行时机:渲染前
requestAnimationFrame(() => {
console.log('rAF');
});
// 特点:
// ✅ 在渲染流程中,渲染之前执行
// ✅ 每帧执行一次(约16.6ms)
// ✅ 与屏幕刷新率同步
// ❌ 页面不可见时暂停执行时间线:
同步代码 → 微任务 → rAF回调 → 样式计算 → 布局 → 绘制 → 下一轮循环4. requestIdleCallback
javascript
// 执行时机:浏览器空闲时
requestIdleCallback(() => {
console.log('rIC');
});
// 特点:
// ✅ 在渲染之后,如果有空闲时间
// ✅ 优先级最低
// ❌ 可能长时间不执行
// ✅ 不影响关键渲染执行时间线:
同步代码 → 微任务 → rAF → 渲染 → rIC(如有空闲) → 宏任务 → 下一轮循环三、实际测试代码
javascript
console.log('1. 同步代码开始');
// setTimeout - 宏任务
setTimeout(() => {
console.log('2. setTimeout');
}, 0);
// MessageChannel - 宏任务(更快)
const channel = new MessageChannel();
channel.port1.onmessage = () => {
console.log('3. MessageChannel');
};
channel.port2.postMessage('');
// Promise - 微任务
Promise.resolve().then(() => {
console.log('4. Promise 微任务');
});
// requestAnimationFrame - 渲染前
requestAnimationFrame(() => {
console.log('5. requestAnimationFrame');
});
// requestIdleCallback - 空闲时
requestIdleCallback(() => {
console.log('6. requestIdleCallback');
});
console.log('7. 同步代码结束');
/* 实际输出顺序(大多数情况):
1. 同步代码开始
7. 同步代码结束
4. Promise 微任务
5. requestAnimationFrame (渲染前)
3. MessageChannel (宏任务,较快)
2. setTimeout (宏任务,较慢)
6. requestIdleCallback (最后,空闲时)
*/四、详细执行时序图
时间线 →
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第一轮事件循环
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[同步代码]
│
├─ console.log('1')
├─ setTimeout 注册
├─ MessageChannel 注册
├─ Promise 注册
├─ rAF 注册
├─ rIC 注册
└─ console.log('7')
↓
[微任务队列] Microtask Queue
│
└─ Promise.then() ✅ 输出 '4'
↓
[渲染检查] 是否需要渲染?
│
└─ 是 → 进入渲染流程
↓
[渲染流程]
│
├─ requestAnimationFrame ✅ 输出 '5'
├─ 样式计算
├─ 布局
├─ 绘制
└─ 合成
↓
[空闲时间检查]
│
└─ requestIdleCallback ✅ 输出 '6' (如有时间)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第二轮事件循环
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[宏任务队列] Macrotask Queue
│
├─ MessageChannel ✅ 输出 '3' (优先)
└─ setTimeout ✅ 输出 '2'
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━八、关键总结
执行顺序(从快到慢)
1. 同步代码(立即)
2. 微任务 Promise(当前宏任务结束后)
3. rAF(渲染前,~16.6ms)
4. MessageChannel(宏任务,~4ms)
5. setTimeout(宏任务,≥4ms)
6. rIC(空闲时,不确定)事件循环的一次完整循环
// 一次事件循环的完整过程
1️⃣ 从宏任务队列取出一个任务执行
↓
2️⃣ 执行所有微任务(直到微任务队列清空)
↓
3️⃣ 判断是否需要渲染(大约 60fps = 16.6ms)
↓
4️⃣ 如果需要渲染:
- 执行 requestAnimationFrame
- 渲染 (Style → Layout → Paint)
- 执行 requestIdleCallback (如有空闲)
↓
5️⃣ 回到步骤 1,开始下一次循环完整架构
┌───────────────────────────────────────────────┐
│ JavaScript 运行时 │
├───────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 调用栈 (Call Stack) │ │
│ │ ┌──────────────────────────┐ │ │
│ │ │ function3() │ │ │
│ │ ├──────────────────────────┤ │ │
│ │ │ function2() │ │ │
│ │ ├──────────────────────────┤ │ │
│ │ │ function1() │ │ │
│ │ └──────────────────────────┘ │ │
│ └─────────────────────────────────────┘ │
│ ↑ │
│ │ │
│ ┌──────────┴──────────┐ │
│ │ │ │
│ │ 事件循环 Event Loop │ ⭐ │
│ │ │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌──────────┴──────────────────┐ │
│ │ │ │
│ ┌────▼─────┐ ┌───────▼───┐ │
│ │ 微任务队列 │ │ 宏任务队列 │ │
│ │Microtask │ │ Macrotask │ │
│ │ Queue │ │ Queue │ │
│ ├──────────┤ ├───────────┤ │
│ │Promise 1 │ │setTimeout │ │
│ │Promise 2 │ │MessageCh. │ │
│ │queueMT() │ │I/O Events │ │
│ └──────────┘ └───────────┘ │
│ │
│ ┌─────────────────────────────────┐ │
│ │ 渲染管线 (Render) │ │
│ ├─────────────────────────────────┤ │
│ │ rAF → Style → Layout → Paint │ │
│ └─────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Web APIs (浏览器提供) │ │
│ ├─────────────────────────────────┤ │
│ │ DOM API, setTimeout, fetch... │ │
│ └─────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────┘面试题
js
async function async1() {
console.log('async1 start'); // 2️⃣
await async2(); // 3️⃣ 调用 async2,然后暂停
console.log('async1 end'); // 7️⃣ 微任务
}
async function async2() {
console.log('async2'); // 4️⃣
}
console.log('script start'); // 1️⃣
setTimeout(() => console.log('setTimeout'), 0); // 9️⃣ 宏任务
async1(); // 调用
new Promise(resolve => {
console.log('promise1'); // 5️⃣ Promise 构造函数同步执行
resolve();
}).then(() => console.log('promise2')); // 8️⃣ 微任务
console.log('script end'); // 6️⃣