JavaScript 中的微任务与宏任务详解

87 阅读7分钟

微任务和宏任务是 JavaScript 解决异步执行顺序的核心机制,基于「事件循环(Event Loop)」实现,目的是合理规划异步代码的执行优先级,避免高优先级异步操作被阻塞。

一、核心定义:什么是微任务和宏任务?

JavaScript 异步任务分为两大队列,二者执行优先级和场景有明确区别:

1. 宏任务(Macrotask / Task)

  • 核心定义:属于「宏观」异步任务,执行优先级较低,每次事件循环仅执行一个宏任务(或一个宏任务队列的批量任务)。

  • 本质特点:任务执行耗时相对较长(或执行时机较晚),会触发浏览器渲染 / UI 更新,不同宏任务之间会穿插浏览器的渲染流程。

  • 常见宏任务类型

    宏任务类型说明
    setTimeout延迟指定时间执行回调(最小延迟约 4ms)
    setInterval按指定时间间隔重复执行回调
    setImmediate(Node.js 环境)立即执行异步回调,优先级低于 process.nextTick
    requestAnimationFrame浏览器重绘前执行(与屏幕刷新率同步,属于浏览器宏任务)
    I/O 操作(Node.js 环境)文件读取、网络请求等异步 I/O
    脚本执行(主代码块)整个 JS 主程序代码(同步代码)属于第一个宏任务
    UI 交互事件点击、滚动、输入等浏览器事件回调(如 clickscroll

2. 微任务(Microtask / Job)

  • 核心定义:属于「微观」异步任务,执行优先级极高,每次宏任务执行完毕后,会立即清空当前所有微任务队列(按顺序执行所有微任务),再进入下一轮事件循环。

  • 本质特点:任务执行耗时短,不触发浏览器渲染,优先于任何宏任务执行,保证高优先级异步操作(如 Promise 回调)快速执行。

  • 常见微任务类型

    微任务类型说明
    Promise.then()/Promise.catch()/Promise.finally()Promise 状态改变后的回调(Promise 构造函数内是同步代码)
    process.nextTick(Node.js 环境)优先级最高的微任务,优先于所有其他微任务
    MutationObserver(浏览器环境)监听 DOM 变化的回调
    queueMicrotask()手动创建微任务(ES6 标准,浏览器 / Node.js 均支持)

二、核心执行规则:事件循环(Event Loop)流程

事件循环是 JS 执行异步任务的核心流程,微任务和宏任务的执行顺序遵循「先同步、后异步;先微任务、后宏任务」的原则,具体步骤如下:

1. 浏览器环境事件循环流程

  1. 执行「当前宏任务队列」中的第一个宏任务(默认是主代码块/同步代码);
  2. 执行完毕后,立即清空「当前微任务队列」中的所有微任务(按添加顺序依次执行);
  3. 微任务执行完毕后,浏览器进行「UI 渲染/重绘」(若有 DOM 变化);
  4. 进入下一轮事件循环,取出「下一个宏任务队列」中的任务重复步骤 1-3;
  5. 循环往复,直到所有宏任务和微任务执行完毕。

2. 关键补充(Node.js 环境差异)

Node.js 事件循环分为 6 个阶段(timers、pending callbacks 等),微任务会在「每个阶段执行完毕后」清空,且 process.nextTick 优先级高于 Promise 微任务,但核心规则仍遵循「宏任务 → 微任务 → 下一个宏任务」。

3. 通俗理解

可以把事件循环比作「餐厅点餐流程」:

  • 同步代码 = 顾客当场点的菜(优先制作);
  • 微任务 = 顾客加的「小菜」(当前菜做完后,立即制作,无需等下一桌);
  • 宏任务 = 下一桌顾客的订单(当前桌所有菜品(同步 + 小菜)上完后,再接待下一桌);
  • 浏览器渲染 = 服务员清理餐桌(微任务做完后,清理完再接待下一桌)。

三、代码示例:直观理解执行顺序

示例 1:基础顺序(同步 → 微任务 → 宏任务)

// 1. 同步代码(第一个宏任务的核心)
console.log('① 同步代码执行');

// 宏任务:setTimeout
setTimeout(() => {
  console.log('④ 宏任务:setTimeout 执行');
}, 0);

// 微任务:Promise.then
Promise.resolve().then(() => {
  console.log('③ 微任务:Promise.then 执行');
});

// 微任务:queueMicrotask
queueMicrotask(() => {
  console.log('② 微任务:queueMicrotask 执行');
});

// 执行结果顺序:① → ② → ③ → ④
// 解析:同步代码先执行 → 清空所有微任务(按添加顺序) → 执行宏任务

示例 2:嵌套异步(微任务内嵌套微任务)

console.log('① 同步代码');

setTimeout(() => {
  console.log('⑤ 宏任务:外层 setTimeout');
  // 宏任务内的微任务
  Promise.resolve().then(() => {
    console.log('⑥ 宏任务内的微任务:Promise.then');
    // 微任务内嵌套微任务(仍会在当前微任务队列清空)
    queueMicrotask(() => {
      console.log('⑦ 微任务内嵌套的微任务');
    });
  });
}, 0);

Promise.resolve().then(() => {
  console.log('② 微任务:外层 Promise');
  // 微任务内嵌套宏任务
  setTimeout(() => {
    console.log('⑧ 微任务内嵌套的宏任务');
  }, 0);
});

queueMicrotask(() => {
  console.log('③ 微任务:queueMicrotask 1');
  // 微任务内嵌套微任务
  queueMicrotask(() => {
    console.log('④ 微任务:queueMicrotask 2');
  });
});

// 执行结果顺序:① → ② → ③ → ④ → ⑤ → ⑥ → ⑦ → ⑧
// 解析:
// 1. 同步代码执行(①)
// 2. 清空外层微任务队列:② → ③ → ④(嵌套微任务仍在当前队列)
// 3. 执行第一个宏任务(⑤)
// 4. 清空该宏任务内的微任务队列:⑥ → ⑦(嵌套微任务也会被清空)
// 5. 执行下一个宏任务(⑧,由微任务嵌套添加)

示例 3:Promise 构造函数的同步性

console.log('① 同步代码');

new Promise((resolve) => {
  console.log('② Promise 构造函数内(同步)');
  resolve(); // 改变状态(同步操作)
}).then(() => {
  console.log('④ 微任务:Promise.then');
});

setTimeout(() => {
  console.log('⑤ 宏任务:setTimeout');
}, 0);

console.log('③ 同步代码结束');

// 执行结果顺序:① → ② → ③ → ④ → ⑤
// 解析:Promise 构造函数内是同步代码,then 回调才是微任务

四、核心区别:微任务 vs 宏任务

对比维度微任务(Microtask)宏任务(Macrotask)
执行优先级高(宏任务执行后立即执行)低(微任务队列清空后才执行)
执行时机同一宏任务执行完毕后,一次性清空所有微任务每轮事件循环执行一个 / 一批宏任务,之间穿插渲染
是否触发渲染不触发(微任务执行期间浏览器不渲染)不同宏任务之间会触发浏览器渲染
执行数量一次性执行所有待执行微任务每轮事件循环执行一个(或一个队列的批量任务)
常见类型Promise.then、queueMicrotask、process.nextTick(Node)setTimeout、setInterval、I/O、UI 事件、主代码块
嵌套执行微任务内嵌套的微任务,仍在当前微任务队列执行宏任务内嵌套的宏任务,进入下一轮事件循环队列

五、典型应用场景

1. 微任务的应用场景

  • Promise 异步回调:接口请求成功后的数据处理、状态更新(优先执行,保证数据快速同步);

    // 接口请求(异步),成功后用 then 处理(微任务,优先执行)
    fetch('/api/user')
      .then(res => res.json())
      .then(data => console.log('数据处理:', data));
    
  • DOM 变化后的后续操作:用 MutationObserver 监听 DOM 变化后,立即执行后续逻辑(无需等待宏任务);

  • 手动插队异步任务:用 queueMicrotask 将任务插入微任务队列,优先于宏任务执行;

  • Node.js 中高优先级异步process.nextTick 用于核心模块的异步回调,保证优先级最高。

2. 宏任务的应用场景

  • 延迟执行setTimeout 用于非紧急的延迟操作(如提示框自动关闭、防抖函数);

    // 防抖函数中的延迟执行(宏任务)
    function debounce(fn, delay) {
      let timer = null;
      return () => {
        clearTimeout(timer);
        timer = setTimeout(fn, delay);
      };
    }
    
  • 定时重复执行setInterval 用于轮询接口、定时更新数据;

  • 浏览器渲染同步requestAnimationFrame 用于动画效果,保证与屏幕刷新率同步;

  • UI 交互响应:点击、滚动等事件回调(宏任务,避免频繁触发阻塞页面)。

六、常见误区

  1. setTimeout(fn, 0) 立即执行:误区:认为延迟 0ms 会立即执行;正解:setTimeout 是宏任务,即使延迟 0ms,也会等待同步代码和所有微任务执行完毕后,才会执行。
  2. 微任务和宏任务的嵌套顺序:误区:微任务内嵌套的宏任务会立即执行;正解:微任务内嵌套的宏任务会进入下一轮事件循环队列,需等待当前所有微任务和本轮宏任务执行完毕后才会执行。
  3. Promise 全是异步:误区:认为 Promise 所有代码都是异步;正解:Promise 构造函数内的代码是同步执行的,只有 then/catch/finally 回调是微任务(异步)。

总结

  1. 核心顺序:同步代码 → 微任务(全部执行) → 宏任务(逐个执行) → 浏览器渲染 → 下一轮事件循环;
  2. 优先级:微任务 > 宏任务,微任务内嵌套的微任务优先于任何宏任务;
  3. 本质作用:微任务保证高优先级异步操作快速执行,宏任务规划低优先级异步操作,避免阻塞页面;
  4. 关键区分:Promise.then、queueMicrotask 是微任务;setTimeout、setInterval 是宏任务,Promise 构造函数内是同步代码。