js的事件循环机制

5 阅读4分钟

关于js的事件循环机制,这篇文章已经讲的很清晰了。

但在实操的过程中我又遇到了一个问题,在此记录一下。如有不对,还望大佬斧正。

宏任务的子队列是按照类型进行分类的,只有同类型宏任务才会进入同一个子队列,遵循该子队列的统一执行规则;不同类型宏任务(如定时器 vs DOM 事件)则按子队列优先级执行,无法直接比较。重要的是:同一类型的宏任务,无论在哪个阶段(同步代码、微任务、其他宏任务)创建,都会进入「同一个专属子队列」进行比较。

解释了以下代码宏任务2(微任务创建)会输出在 宏任务3 前面的原因了。

举例:

以下代码的输出结果是什么?

console.log('同步1'); 
// 宏任务1 
setTimeout(() => { 
    console.log('宏任务1'); 
    // 宏任务1中的微任务 
    Promise.resolve().then(() => { 
        console.log('宏任务1的微任务'); 
        // 微任务中创建宏任务2 
        setTimeout(() => { 
            console.log('宏任务2(微任务创建)'); 
        }, 0); 
      }); 
}, 0); 
// 宏任务3(直接创建)
setTimeout(() => { 
    console.log('宏任务3'); 
}, 3000); 
console.log('同步2');

最终执行结果:

同步1 → 同步2 → 宏任务1 → 宏任务1的微任务 → 宏任务2(微任务创建) → 宏任务3

执行流程:

阶段 1:执行全局同步代码(第一轮宏任务)

这是事件循环的起点,浏览器会先执行script标签内的所有同步代码:

console.log('同步1'); // 执行 → 输出「同步1」

// 宏任务1:setTimeout(延迟0ms)
setTimeout(() => { /* 回调 */ }, 0); 
// 行为:回调被加入「定时器宏任务子队列」,标记“最早可执行时间 = 当前时间 + 0ms”

// 宏任务3:setTimeout(延迟3000ms)
setTimeout(() => { /* 回调 */ }, 3000);
// 行为:回调被加入「定时器宏任务子队列」,标记“最早可执行时间 = 当前时间 + 3000ms”

console.log('同步2'); // 执行 → 输出「同步2」

✅ 此时全局状态:

  • 调用栈:空(同步代码执行完毕);
  • 微任务队列:空;
  • 定时器宏任务子队列:[宏任务1(延迟0ms)、宏任务3(延迟3000ms)]

阶段 2:执行微任务队列(空)→ 跳过 UI 渲染

同步代码执行完后,事件循环会先检查微任务队列 —— 此时微任务队列为空,浏览器直接进入下一轮事件循环,无需执行 UI 渲染。

阶段 3:执行定时器宏任务子队列(按规则筛选执行)

定时器子队列的核心是 “先筛选可执行任务,再按顺序执行”

子阶段 3.1:执行宏任务 1(延迟 0ms,已到期)

定时器子队列中,宏任务 1 的延迟时间已到,成为第一个可执行的宏任务:

console.log('宏任务1'); // 执行 → 输出「宏任务1」

// 宏任务1中的微任务
Promise.resolve().then(() => { /* 回调 */ });
// 行为:回调被加入「微任务队列」

✅ 此时全局状态:

  • 调用栈:空;
  • 微任务队列:[宏任务1的微任务]
  • 定时器宏任务子队列:[宏任务3(延迟3000ms,未到期)]

子阶段 3.2:执行微任务队列(清空所有微任务)

宏任务 1 的同步代码执行完后,立即执行微任务队列中的所有任务:

console.log('宏任务1的微任务'); // 执行 → 输出「宏任务1的微任务」

// 微任务中创建宏任务2:setTimeout(延迟0ms)
setTimeout(() => { /* 回调 */ }, 0);
// 行为:回调被加入「定时器宏任务子队列」,标记“最早可执行时间 = 当前时间 + 0ms”

✅ 此时全局状态:

  • 微任务队列:空(已清空);
  • 定时器宏任务子队列:[宏任务2(延迟0ms,已到期)、宏任务3(延迟3000ms,未到期)]

子阶段 3.3:执行宏任务 2(微任务创建,已到期)

微任务队列清空后,浏览器进入下一轮事件循环 —— 此时宏任务 2 的延迟时间已到,成为定时器子队列中第一个可执行任务:

console.log('宏任务2(微任务创建)'); // 执行 → 输出「宏任务2(微任务创建)」

✅ 此时全局状态:

  • 微任务队列:空;
  • 定时器宏任务子队列:[宏任务3(延迟3000ms,未到期)]

阶段 4:等待 3000ms → 执行宏任务 3

宏任务 2 执行完后,浏览器会持续检查定时器子队列 —— 直到宏任务 3 的「最早可执行时间」到期(约 3000ms 后),才会执行该任务:

console.log('宏任务3'); // 执行 → 输出「宏任务3」