宏任务与微任务的区别

深入理解宏任务与微任务的分类机制、优先级差异,以及它们在 JavaScript 异步编程中的实际应用场景。


为什么要区分宏任务和微任务

JavaScript 的异步编程模型看似复杂,实际上是由一个简单的规则驱动的:微任务总是在下一个宏任务之前执行

这个设计是有原因的。微任务通常涉及需要尽快完成的后续操作,比如 Promise 的回调——Promise 代表一个已经或即将完成的异步操作,它的 .then() 回调需要尽快执行,以响应这个已完成的状态变化。

而宏任务是较大的工作单元,比如用户交互事件、计时器回调、I/O 操作。这些不需要在当前宏任务结束后立即执行,可以在浏览器/Node.js 方便的时候执行。

理解这个区别,不只能帮你做对面试题,更能帮你在实际项目中写出正确的异步代码。


宏任务的完整分类

浏览器环境中的宏任务

API 阶段 说明
setTimeout / setInterval timers 计时器回调
setImmediate check Node.js 专用
I/O callbacks pending callbacks 上一轮 I/O 的回调
requestAnimationFrame before the next frame 浏览器渲染前
UI 事件(click, keydown) 宏任务 用户交互
requestIdleCallback 空档期 浏览器空闲时执行

Node.js 环境中的宏任务

Node.js 的宏任务阶段更细分:

  1. timerssetTimeoutsetInterval 回调
  2. pending callbacks:延迟到下一轮 I/O 的回调
  3. idle, prepare:内部使用
  4. poll:获取新的 I/O 事件
  5. checksetImmediate 回调
  6. close callbacks:关闭回调

requestAnimationFrame 的特殊性

requestAnimationFrame 是一个特殊的宏任务。它在浏览器中每一帧的渲染前被调用,大约是每秒 60 次(16.67ms 一帧)。

// 动画的正确实现方式
function animate() {
  // 更新动画状态
  updateAnimation();

  // 浏览器会在合适时机渲染
  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

相比之下,setTimeout(..., 16) 不能保证在每一帧执行,可能会跳帧或卡顿。


微任务的完整分类

微任务家族

API 环境 说明
Promise.then/catch/finally 通用 Promise 回调
queueMicrotask() 通用 手动入队微任务
MutationObserver 浏览器 DOM 变动观察
process.nextTick Node.js 最高优先级

queueMicrotask 的使用

queueMicrotask() 允许你手动将一个函数加入微任务队列:

queueMicrotask(() => {
  console.log('microtask');
});

console.log('sync');
// 输出: sync, microtask

这在你需要确保某个操作在当前任务完成后、渲染发生前执行时很有用。

Promise 的微任务特性

Promise 的 .then().catch().finally() 都是微任务。但要注意:Promise 的 executor 函数是同步执行的。

const promise = new Promise((resolve, reject) => {
  console.log('executor'); // 同步执行
  resolve('value');
});

promise.then(value => {
  console.log('then'); // 微任务
});

console.log('sync'); // 同步执行

// 输出: executor, sync, then

嵌套的微任务

微任务可以嵌套,但每个微任务都会在下一个宏任务开始前被完全执行:

Promise.resolve()
  .then(() => {
    console.log('microtask 1');
    Promise.resolve().then(() => {
      console.log('nested microtask');
    });
  })
  .then(() => {
    console.log('microtask 2');
  });

// 输出: microtask 1, nested microtask, microtask 2

注意 microtask 2 在嵌套微任务之后输出,因为第二个 .then() 本身也是一个微任务,它依赖于第一个 .then() 执行完毕才加入队列。


async/await 与微任务

async 函数返回一个 Promise,await 暂停当前函数的执行,将后续代码作为微任务加入队列:

async function foo() {
  console.log('1');
  await bar();
  console.log('2');
}

function bar() {
  return Promise.resolve();
}

console.log('3');
foo();
console.log('4');

// 输出: 3, 1, 4, 2

执行过程:

  1. console.log('3') 同步输出
  2. 调用 foo(),输出 1
  3. await Promise.resolve() 暂停 foo,将 console.log('2') 加入微任务队列
  4. console.log('4') 同步输出
  5. 微任务执行,输出 2

常见面试题解析

题目一

setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve().then(() => console.log('Promise'));
queueMicrotask(() => console.log('queueMicrotask'));
console.log('sync');

答案:sync, Promise, queueMicrotask, setTimeout

解析:

  • sync 是同步任务,最先输出
  • Promise.thenqueueMicrotask 都是微任务,在同步任务后执行
  • queueMicrotask 先于 Promise.then 因为它是同步代码后面第一个入队的微任务
  • setTimeout 是宏任务,在所有微任务执行完毕后执行

题目二

async function foo() {
  console.log('a');
  await Promise.resolve();
  console.log('b');
}

console.log('c');
setTimeout(() => console.log('d'), 0);

foo().then(() => console.log('e'));

Promise.resolve().then(() => console.log('f'));
console.log('g');

答案:c, a, g, b, f, e, d

解析:

  • c 同步输出
  • foo() 调用,输出 aawaitb 加入微任务队列
  • g 同步输出
  • 微任务检查:b 入队,然后 foo().then() 的回调入队,然后 Promise.resolve().then() 的回调入队
  • 微任务执行:b 输出,f 输出,e 输出
  • 宏任务执行:d 输出

实际开发中的应用

防抖与节流中的微任务

// 防抖:最后一次操作后才执行
function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// 节流:固定间隔执行
function throttle(fn, interval) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= interval) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

两者的区别在于:防抖用 setTimeout,是宏任务;节流如果用 requestAnimationFrame,会在渲染前执行。

React 中的微任务应用

React 的 flushSync 用于强制同步执行:

import { flushSync } from 'react-dom';

flushSync(() => {
  setState({ count: 1 });
});
console.log('同步更新');

React 18 的自动批处理(automatic batching)也涉及微任务——状态更新会被合并,在微任务结束时一起应用。


这一章想说的

宏任务和微任务的区别是 JavaScript 异步编程的核心。

宏任务是宿主环境发起的大任务,每次事件循环迭代执行一个;微任务是当前宏任务结束后立即执行的任务,优先级更高。

常见的宏任务:setTimeoutsetIntervalI/OrequestAnimationFrame。 常见的微任务:Promise.thenqueueMicrotaskMutationObserver

记住一个核心规则:微任务总是在下一个宏任务之前全部执行。理解这个规则,你就能分析任何复杂的异步执行顺序问题。


延展阅读


实践练习

练习:分析并验证以下代码的执行顺序

async function test() {
  console.log('1');

  await new Promise(resolve => {
    console.log('2');
    resolve();
  }).then(() => console.log('3'));

  console.log('4');

  await new Promise(resolve => {
    console.log('5');
    resolve();
  }).then(() => console.log('6'));
}

setTimeout(() => {
  console.log('7');

  Promise.resolve().then(() => console.log('8'));
}, 0);

test().then(() => console.log('9'));

console.log('10');

先自己分析输出顺序,然后运行代码验证。重点理解 await 的执行流程——await 后面的代码是在微任务里执行的。