JS 入门通关手册(44):宏任务 / 微任务 / Event Loop(前端最难核心,面试必考

摘要

本文彻底拆解 JavaScript 单线程异步的核心机制 ------Event Loop(事件循环),详细讲解宏任务、微任务的定义、分类、执行顺序,结合大量可运行代码示例、执行流程图解,拆解面试高频代码输出题,帮你彻底搞懂 "JS 为什么能同时处理多个异步操作",轻松应对前端最难的 Event Loop 面试题,摆脱 "代码执行顺序混乱" 的困扰。


一、前言:为什么需要 Event Loop?

JavaScript 是单线程语言------ 同一时间只能执行一段代码,无法同时执行多个任务。但实际开发中,我们经常会遇到 "异步操作"(如定时器、接口请求、DOM 事件),如果单线程一直等待异步操作完成,会导致页面卡顿、无响应。

Event Loop(事件循环)就是 JavaScript 单线程处理异步操作的核心机制 ,它的作用是:协调同步代码、宏任务、微任务的执行顺序,让单线程也能高效处理多个异步操作,避免线程阻塞。

核心痛点示例(先看一道面试题)

javascript

运行

javascript 复制代码
console.log("同步1");

setTimeout(() => {
  console.log("定时器1");
}, 0);

Promise.resolve().then(() => {
  console.log("微任务1");
});

console.log("同步2");

请问:输出顺序是什么?(答案在后面,先思考)

很多开发者会误以为 "定时器延迟 0ms,会先执行",但实际输出顺序是:同步 1 → 同步 2 → 微任务 1 → 定时器 1。

这背后的核心逻辑,就是 Event Loop、宏任务与微任务的执行规则 ------ 这也是前端面试中,Event Loop 最常考的形式。


二、核心概念:同步代码、宏任务、微任务

要搞懂 Event Loop,必须先分清三个核心概念:同步代码、宏任务(Macro Task)、微任务(Micro Task),它们的执行优先级不同,决定了代码的执行顺序。

1. 同步代码

  • 定义:直接在主线程中执行,立即执行、无延迟,执行完一段再执行下一段,会阻塞主线程。
  • 示例:console.log、变量声明、函数调用(非异步)、条件判断、循环等。

javascript

运行

javascript 复制代码
// 所有同步代码,按顺序执行
console.log("同步1"); // 立即执行
let a = 10; // 立即执行
function fn() { console.log("同步函数"); }
fn(); // 立即执行
console.log("同步2"); // 立即执行

2. 宏任务(Macro Task)

  • 定义:异步操作,执行优先级低,会被放入 "宏任务队列",等待同步代码和微任务执行完毕后,再执行。
  • 核心特点:每次 Event Loop 循环,只执行一个宏任务(执行完一个,再检查微任务队列)。
  • 常见宏任务(必记,面试高频):
    1. setTimeout、setInterval(定时器)
    2. setImmediate(Node.js 特有,浏览器不支持)
    3. I/O 操作(如接口请求、文件读取)
    4. DOM 事件(如 click、scroll,本质是宏任务)
    5. script 标签(整个脚本的执行,属于宏任务)

3. 微任务(Micro Task)

  • 定义:异步操作,执行优先级高,会被放入 "微任务队列",同步代码执行完毕后,立即执行所有微任务,再执行宏任务。
  • 核心特点:同步代码执行完后,会清空整个微任务队列(所有微任务一次性执行完)。
  • 常见微任务(必记,面试高频):
    1. Promise.then、Promise.catch、Promise.finally(Promise 的回调)
    2. process.nextTick(Node.js 特有,优先级最高的微任务)
    3. MutationObserver(浏览器特有,监听 DOM 变化的回调)
    4. queueMicrotask(新增 API,手动添加微任务)

关键优先级排序(面试必背)

同步代码 > 微任务 > 宏任务

一句话记忆:先执行所有同步代码,再执行所有微任务,最后执行宏任务;每次执行完一个宏任务,都要先检查并清空微任务队列。


三、Event Loop 执行流程(图解 + 步骤,必懂)

Event Loop 的核心是 "循环执行",每次循环称为一个 "tick",每个 tick 的执行流程固定,分为 4 步(结合图解理解,面试可口述):

1. 执行流程图解(文字版)

plaintext

复制代码
开始 → 执行同步代码 → 清空微任务队列 → 执行一个宏任务 → 清空微任务队列 → 重复循环...

2. 详细执行步骤(必记,面试必考)

  1. 执行当前 "宏任务" 中的所有同步代码(如果是第一次执行,当前宏任务就是整个 script 脚本)。
  2. 同步代码执行完毕后,清空微任务队列(按顺序执行所有微任务,直到微任务队列为空)。
  3. 微任务队列清空后,从宏任务队列中取出第一个宏任务,执行其内部的同步代码。
  4. 该宏任务的同步代码执行完毕后,再次清空微任务队列
  5. 重复步骤 3-4,直到宏任务队列和微任务队列都为空,Event Loop 结束。

3. 结合前面的面试题,拆解执行流程

javascript

运行

javascript 复制代码
console.log("同步1"); // 同步代码,立即执行

setTimeout(() => {
  console.log("定时器1"); // 宏任务,放入宏任务队列
}, 0);

Promise.resolve().then(() => {
  console.log("微任务1"); // 微任务,放入微任务队列
});

console.log("同步2"); // 同步代码,立即执行
执行步骤拆解:
  1. 执行当前宏任务(script 脚本)中的同步代码:
    • 执行 console.log("同步1") → 输出:同步 1
    • 遇到 setTimeout,将其回调放入宏任务队列(宏任务队列:[定时器 1])
    • 遇到 Promise.then,将其回调放入微任务队列(微任务队列:[微任务 1])
    • 执行 console.log("同步2") → 输出:同步 2
  2. 同步代码执行完毕,清空微任务队列
    • 执行微任务队列中的 "微任务 1" → 输出:微任务 1
    • 微任务队列为空
  3. 微任务队列清空后,取出宏任务队列中的第一个宏任务(定时器 1),执行其内部同步代码:
    • 执行 console.log("定时器1") → 输出:定时器 1
  4. 该宏任务执行完毕,再次检查微任务队列(为空),继续循环(宏任务队列已空,循环结束)。
最终输出顺序:

同步 1 → 同步 2 → 微任务 1 → 定时器 1(和前面的答案一致)

4. 补充:多个宏任务、微任务的执行顺序

javascript

运行

javascript 复制代码
// 复杂示例,巩固执行流程
console.log("同步1");

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

// 微任务1
Promise.resolve().then(() => {
  console.log("微任务1");
  // 微任务中新增微任务
  Promise.resolve().then(() => {
    console.log("微任务1-1");
  });
  // 微任务中新增宏任务
  setTimeout(() => {
    console.log("微任务中的宏任务");
  }, 0);
});

// 宏任务2
setTimeout(() => {
  console.log("宏任务2");
}, 0);

console.log("同步2");
执行步骤拆解:
  1. 执行 script 同步代码:输出 同步 1、同步 2;宏任务队列:[宏任务 1, 宏任务 2];微任务队列:[微任务 1]
  2. 清空微任务队列:
    • 执行微任务 1 → 输出 微任务 1
    • 微任务 1 中新增微任务 1-1,放入微任务队列
    • 微任务 1 中新增宏任务,放入宏任务队列(宏任务队列:[宏任务 1, 宏任务 2, 微任务中的宏任务])
    • 继续执行微任务 1-1 → 输出 微任务 1-1;微任务队列为空
  3. 取出第一个宏任务(宏任务 1),执行其同步代码:输出 宏任务 1-1
  4. 清空微任务队列(宏任务 1 中新增的微任务):输出 宏任务 1 的微任务;微任务队列为空
  5. 取出第二个宏任务(宏任务 2),执行其同步代码:输出 宏任务 2
  6. 清空微任务队列(为空)
  7. 取出第三个宏任务(微任务中的宏任务),执行其同步代码:输出 微任务中的宏任务
  8. 循环结束
最终输出顺序:

同步 1 → 同步 2 → 微任务 1 → 微任务 1-1 → 宏任务 1-1 → 宏任务 1 的微任务 → 宏任务 2 → 微任务中的宏任务


四、浏览器 vs Node.js 的 Event Loop 区别(面试加分)

很多面试会问:"浏览器和 Node.js 的 Event Loop 有区别吗?"------ 答案是:有,核心区别在宏任务的执行顺序和微任务的优先级

1. 浏览器的 Event Loop(重点,前端面试主要考这个)

  • 宏任务队列:只有一个,按顺序执行(每次执行一个宏任务)。
  • 微任务队列:只有一个,同步代码执行完后,清空所有微任务。
  • 微任务优先级:Promise.then > MutationObserver。

2. Node.js 的 Event Loop(了解即可,面试提及加分)

  • 宏任务队列:有 6 个,按优先级顺序执行(不同类型的宏任务放入不同队列)。
  • 微任务队列:有 2 个,分为 "微任务队列(microtasks)" 和 "nextTick 队列"。
  • 微任务优先级:process.nextTick(最高)> Promise.then。
  • 执行流程:执行同步代码 → 清空 nextTick 队列 → 清空微任务队列 → 执行一个宏任务队列 → 重复。

3. 核心区别总结(面试必答)

  1. 宏任务队列:浏览器只有 1 个,Node.js 有 6 个,执行顺序不同。
  2. 微任务队列:浏览器只有 1 个,Node.js 有 2 个,process.nextTick 优先级最高。
  3. 定时器精度:Node.js 中 setTimeout 的延迟时间精度更高,浏览器中受 Event Loop 影响,精度较低。

五、高频面试题(代码输出题,必练)

Event Loop 面试最常考 "代码输出顺序",以下 3 道题覆盖所有高频场景,练会就能应对 90% 的面试题。

面试题 1:基础版(同步 + 微任务 + 宏任务)

javascript

运行

javascript 复制代码
console.log("1");

setTimeout(() => {
  console.log("2");
  Promise.resolve().then(() => {
    console.log("3");
  });
}, 0);

Promise.resolve().then(() => {
  console.log("4");
  setTimeout(() => {
    console.log("5");
  }, 0);
});

console.log("6");
输出顺序:1 → 6 → 4 → 2 → 3 → 5
解析:
  1. 同步代码:1、6 → 微任务队列:[4];宏任务队列:[2]
  2. 清空微任务:4 → 新增宏任务 5 → 宏任务队列:[2,5]
  3. 执行宏任务 2 → 同步代码 2 → 新增微任务 3 → 清空微任务 3
  4. 执行宏任务 5 → 5

面试题 2:进阶版(多个微任务 + 宏任务嵌套)

javascript

运行

javascript 复制代码
async function fn() {
  console.log("1");
  await Promise.resolve();
  console.log("2");
}

console.log("3");

setTimeout(() => {
  console.log("4");
  fn();
}, 0);

Promise.resolve().then(() => {
  console.log("5");
});

fn();

console.log("6");
输出顺序:3 → 1 → 6 → 5 → 2 → 4 → 1 → 2
解析:
  1. 同步代码:3 → 执行 fn (),输出 1 → await 后面的 Promise 是微任务,放入微任务队列 → 同步代码 6 → 微任务队列:[5, 2];宏任务队列:[4]
  2. 清空微任务:5 → 2
  3. 执行宏任务 4 → 输出 4 → 执行 fn (),输出 1 → await 微任务放入队列 → 清空微任务 2
  4. 循环结束

面试题 3:易错版(DOM 事件 + 微任务 + 宏任务)

javascript

运行

javascript 复制代码
document.addEventListener("click", () => {
  console.log("click1");
  Promise.resolve().then(() => {
    console.log("click1微任务");
  });
});

document.addEventListener("click", () => {
  console.log("click2");
  Promise.resolve().then(() => {
    console.log("click2微任务");
  });
});

// 手动触发点击事件
document.click();
输出顺序:click1 → click1 微任务 → click2 → click2 微任务
解析:
  • DOM 事件属于宏任务,手动触发 click 后,两个 click 事件会依次放入宏任务队列。
  • 执行第一个 click 宏任务:输出 click1 → 新增微任务,清空微任务(click1 微任务)。
  • 执行第二个 click 宏任务:输出 click2 → 新增微任务,清空微任务(click2 微任务)。

六、常见误区(面试避坑)

  1. 误区 1:setTimeout (fn, 0) 会立即执行 ------ 错!setTimeout 是宏任务,即使延迟 0ms,也会等到同步代码和微任务执行完毕后,才会执行。
  2. 误区 2:微任务和宏任务是同时执行的 ------ 错!优先级:同步 > 微任务 > 宏任务,微任务全部执行完,才会执行宏任务。
  3. 误区 3:多个宏任务会一次性执行完 ------ 错!每次 Event Loop 只执行一个宏任务,执行完后,必须先清空微任务队列,再执行下一个宏任务。
  4. 误区 4:async/await 中的 await 后面的代码是同步的 ------ 错!await 后面的代码属于 "微任务",会在 await 等待的 Promise 完成后,放入微任务队列。

七、总结(核心要点,面试速记)

  1. JS 是单线程,Event Loop 是单线程处理异步的核心机制。
  2. 三大执行优先级:同步代码 > 微任务 > 宏任务。
  3. 每个 Event Loop 循环:执行 1 个宏任务 → 清空所有微任务 → 重复。
  4. 微任务:Promise.then、queueMicrotask、MutationObserver(浏览器)、process.nextTick(Node.js)。
  5. 宏任务:setTimeout、setInterval、I/O、DOM 事件、script 脚本。
  6. 面试重点:代码输出顺序(掌握执行流程,就能轻松破解)。
相关推荐
We་ct2 小时前
LeetCode 172. 阶乘后的零:从暴力到最优,拆解解题核心
开发语言·前端·javascript·算法·leetcode·typescript
军军君012 小时前
数字孪生监控大屏实战模板:可视化数字统计展示
前端·javascript·vue.js·typescript·echarts·数字孪生·前端大屏
吴声子夜歌2 小时前
ES6——Iterator和for...of循环详解
前端·javascript·es6
小李子呢02112 小时前
前端八股3---ref和reactive
开发语言·前端·javascript
web_小码农3 小时前
CSS 3D动画 旋转木马示例(带弧度支持手动拖动)
javascript·css·3d
Armouy3 小时前
Electron:核心概念、性能优化与兼容问题
前端·javascript·electron
F2E_Zhangmo3 小时前
react native如何发送蓝牙命令
javascript·react native·react.js
博主花神3 小时前
【TypeScript】梳理
javascript·ubuntu·typescript
淡笑沐白3 小时前
ECharts入门指南:数据可视化实战
前端·javascript·echarts