Skip to content

四大异步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️⃣

Released under the MIT License.