在 JavaScript 中,事件循环(Event Loop) 是 JS 引擎实现异步编程的核心机制 —— 由于 JS 是单线程语言(同一时间只能执行一个任务),事件循环通过 “任务队列” 调度同步 / 异步任务的执行顺序,让 JS 既能保持单线程的简单性,又能处理异步操作(如网络请求、定时器、DOM 事件)。

一、核心前提:JS 的单线程与任务分类

1. 单线程的本质

JS 设计为单线程(避免多线程操作 DOM 导致冲突),但单线程的问题是:若所有任务同步执行,耗时操作(如网络请求)会阻塞后续代码,导致页面卡死。因此 JS 将任务分为两类:

2. 任务分类(关键)

任务类型 执行时机 包含典型操作
同步任务 立即执行,进入 “执行栈” 变量声明、函数调用、基本运算等同步代码
异步任务 不立即执行,进入 “任务队列” 又分为「宏任务(Macrotask)」和「微任务(Microtask)」,异步操作完成后进入对应队列
(1)宏任务(Macrotask)
  • 特点:执行优先级低,每次事件循环仅执行一个宏任务,执行完后清空所有微任务;
  • 常见类型:
    • 全局代码(script 标签)
    • 定时器(setTimeout/setInterval/setImmediate
    • DOM 事件(click/resize 等)
    • 网络请求回调(fetch/XMLHttpRequest
    • requestAnimationFrame(浏览器专属)
    • I/O 操作(Node.js 专属)
(2)微任务(Microtask)
  • 特点:执行优先级高,宏任务执行后立即清空所有微任务(微任务队列空了才会执行下一个宏任务);
  • 常见类型:
    • Promise.then/catch/finally
    • async/await(本质是 Promise 语法糖,await 后的代码属于微任务)
    • queueMicrotask()(手动添加微任务)
    • MutationObserver(浏览器专属)
    • process.nextTick(Node.js 专属,优先级高于普通微任务)

二、事件循环的执行流程(浏览器环境)

事件循环的核心是 “执行栈 + 宏任务队列 + 微任务队列” 的协作,流程可拆解为 5 步:

1
2
3
4
5
6
7
8
1. 执行全局同步代码(属于第一个宏任务),同步任务进入执行栈,执行完毕出栈;
2. 执行过程中遇到异步任务:
- 宏任务:异步操作完成后,将回调加入「宏任务队列」;
- 微任务:异步操作完成后,将回调加入「微任务队列」;
3. 全局同步代码执行完毕(第一个宏任务执行完),立即执行「微任务队列」中的所有微任务(按顺序);
- 若微任务执行过程中产生新的微任务,继续加入队列并立即执行(直到微任务队列为空);
4. 微任务队列清空后,执行浏览器的「UI 渲染」(更新 DOM);
5. 渲染完成后,事件循环取下一个宏任务执行,重复步骤 2-5;

直观示例(浏览器):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
console.log("同步1"); // 同步任务,立即执行

setTimeout(() => {
console.log("宏任务:setTimeout"); // 宏任务,加入宏任务队列
}, 0);

Promise.resolve().then(() => {
console.log("微任务:Promise.then"); // 微任务,加入微任务队列
queueMicrotask(() => {
console.log("微任务:queueMicrotask"); // 微任务执行中新增的微任务
});
});

console.log("同步2"); // 同步任务,立即执行

// 执行顺序:
// 同步1 → 同步2 → 微任务:Promise.then → 微任务:queueMicrotask → 宏任务:setTimeout

三、关键细节:微任务的 “插队” 特性

微任务的优先级远高于宏任务,且微任务队列会被一次性清空(包括执行微任务时新增的微任务),这是事件循环的核心考点:

示例:微任务嵌套微任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
console.log("同步");

setTimeout(() => {
console.log("宏任务1");
Promise.resolve().then(() => console.log("微任务2"));
}, 0);

Promise.resolve().then(() => {
console.log("微任务1");
setTimeout(() => console.log("宏任务2"), 0);
});

// 执行顺序:
// 同步 → 微任务1 → 宏任务1 → 微任务2 → 宏任务2

解析:

  1. 同步代码执行完,先清空微任务队列 → 执行 “微任务 1”;
  2. “微任务 1” 中新增的 “宏任务 2” 进入宏任务队列;
  3. 微任务队列空后,执行下一个宏任务 “宏任务 1”;
  4. “宏任务 1” 执行完,立即清空其产生的微任务 “微任务 2”;
  5. 最后执行 “宏任务 2”。

四、浏览器 vs Node.js 事件循环的差异

Node.js 也实现了事件循环,但针对服务端场景做了调整,核心差异如下:

特性 浏览器事件循环 Node.js 事件循环
宏任务执行顺序 无细分,按队列顺序执行 分 6 个阶段(timers → I/O → idle → poll → check → close),按阶段执行
微任务优先级 普通微任务(Promise)优先 process.nextTick > 普通微任务(Promise)
触发时机 宏任务执行后 → 微任务 → 渲染 每个阶段执行完 → 清空微任务队列 → 进入下一个阶段

Node.js 事件循环的核心阶段(简化):

  1. timers:执行 setTimeout/setInterval 回调;
  2. I/O callbacks:执行 I/O 操作(如文件、网络)的回调;
  3. idle/prepare:内部阶段,忽略;
  4. poll:等待新的 I/O 事件,是核心阶段;
  5. check:执行 setImmediate 回调;
  6. close callbacks:执行 close 事件回调(如 socket.on('close'))。

五、经典面试题(事件循环执行顺序)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
console.log('start');

setTimeout(() => {
console.log('timeout1');
Promise.resolve().then(() => {
console.log('promise1');
});
}, 0);

Promise.resolve().then(() => {
console.log('promise2');
setTimeout(() => {
console.log('timeout2');
}, 0);
});

console.log('end');

// 执行顺序:start → end → promise2 → timeout1 → promise1 → timeout2

解析:

  1. 同步代码:startend
  2. 清空微任务队列:执行 promise2,并新增 timeout2 到宏任务队列;
  3. 执行下一个宏任务 timeout1
  4. 清空 timeout1 产生的微任务:promise1
  5. 执行下一个宏任务 timeout2

六、async/await 与事件循环

async/await 是 Promise 的语法糖,await 后的代码会被包裹为 Promise.then 的微任务:

1
2
3
4
5
6
7
8
9
10
11
async function fn() {
console.log('async1');
await Promise.resolve(); // await 后的代码进入微任务队列
console.log('async2');
}

console.log('sync1');
fn();
console.log('sync2');

// 执行顺序:sync1 → async1 → sync2 → async2

解析:

  1. 同步代码:sync1 → 执行 fn() 输出 async1sync2
  2. await 后的 async2 进入微任务队列,同步代码执行完后清空微任务 → 输出 async2

七、核心总结

  1. 事件循环的目的:解决 JS 单线程下异步任务的调度问题;

  2. 核心规则

    • 同步任务优先执行,异步任务分宏任务 / 微任务;
    • 宏任务执行完 → 清空所有微任务(包括新增的)→ 渲染(浏览器)→ 下一个宏任务;
    • 微任务优先级 > 宏任务,process.nextTick(Node)> 普通微任务;
  3. 常见误区

    • setTimeout(fn, 0) 不是立即执行,而是 “当前宏任务执行完后尽快执行”;
    • 微任务队列会被一次性清空,而非只执行一个;
  4. 应用价值:理解事件循环能精准预判异步代码的执行顺序,解决回调地狱、异步竞态等问题。

简单记:同步先执行,微任务插队宏任务前,宏任务排队挨个来