JavaScript 队列与微任务

深入解析 JavaScript 任务队列与微任务队列的区别:Promise、MutationObserver、queueMicrotask 的执行时机,以及它们在事件循环中的角色。

为什么理解微任务是理解 JavaScript 并发的关键

JavaScript 是单线程的,通过事件循环(Event Loop)处理异步操作。但事件循环中有两种不同的队列:宏任务队列(Task Queue)微任务队列(Microtask Queue)

理解它们的区别,是理解 Promise、async/await、MutationObserver 等异步机制的基础。


一、任务队列 vs 微任务队列

1.1 基本区别

特性 宏任务(Task/Macrotask) 微任务(Microtask)
执行时机 当前任务完成后,下一个任务开始前 当前任务完成后,立即执行
队列数 多个 只有一个
包含 setTimeout、setInterval、I/O、UI 渲染 Promise、queueMicrotask、MutationObserver
每次迭代 执行一个宏任务 执行所有微任务

1.2 执行顺序示意

当前执行栈为空
    ↓
执行所有微任务(清空微任务队列)
    ↓
执行一个宏任务
    ↓
执行所有微任务(清空微任务队列)
    ↓
执行下一个宏任务
    ↓
...(重复)

二、微任务执行时机

2.1 Promise 的微任务

console.log('1');

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

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

console.log('4');

// 输出顺序:
// 1
// 4
// 3
// 2

分析:

  1. 同步代码先执行:1, 4
  2. 微任务(Promise)加入微任务队列
  3. 宏任务(setTimeout)加入宏任务队列
  4. 同步代码执行完毕,清空微任务队列:3
  5. 事件循环下一个迭代,取出并执行宏任务:2

2.2 嵌套的 Promise

console.log('start');

Promise.resolve()
  .then(() => console.log('promise1'))
  .then(() => console.log('promise2'));

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

console.log('end');

// 输出:
// start
// end
// promise1
// promise3
// promise2

2.3 微任务中抛出错误

Promise.resolve()
  .then(() => {
    throw new Error('error in microtask');
  })
  .catch(err => console.log('caught:', err.message));

// 错误在下一个微任务中被处理
// 输出:caught: error in microtask

三、queueMicrotask

3.1 基本用法

queueMicrotask 是标准的将回调加入微任务队列的方法:

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

3.2 与 Promise.resolve().then 的区别

两者基本等价:

// 这两种方式效果相同
queueMicrotask(() => console.log('microtask'));
Promise.resolve().then(() => console.log('microtask'));

queueMicrotask 的优势是语义更明确,且不需要创建 Promise。

3.3 实际应用:确保异步执行

let state = 'initial';
const obj = {};

// 同步代码无法观测到的修改
queueMicrotask(() => {
  state = 'updated';
  console.log('State updated to:', state);
});

console.log('Current state:', state);
// 输出:
// Current state: initial
// State updated to: updated

四、MutationObserver

4.1 MutationObserver 是微任务

MutationObserver 的回调在微任务队列中执行:

const observer = new MutationObserver(() => {
  console.log('mutation');
});

const el = document.createElement('div');
observer.observe(el, { attributes: true });

el.setAttribute('data-test', 'value');
console.log('after attribute change');

// 输出:
// after attribute change
// mutation

4.2 批量 DOM 变更

MutationObserver 会将多次 DOM 变更合并为一次回调:

const observer = new MutationObserver(() => {
  console.log('mutations:', mutations.length);
});

const el = document.createElement('div');
observer.observe(el, { attributes: true });

el.setAttribute('a', '1');
el.setAttribute('b', '2');
el.setAttribute('c', '3');
console.log('after all changes');

// 输出:
// after all changes
// mutations: 3(合并为一次)

五、async/await 与微任务

5.1 async 函数返回 Promise

async function foo() {
  console.log('2');
  return 'result';
}

console.log('1');
foo().then(console.log);
console.log('3');

// 输出:
// 1
// 2
// 3
// result

5.2 await 后的代码在微任务中执行

async function foo() {
  console.log('2');
  await Promise.resolve();
  console.log('4');
}

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

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

await 之后的代码相当于在 .then() 中执行,即微任务。


六、面试高频考点

考点 1:宏任务与微任务的区别

微任务在当前任务结束后、下一个宏任务开始前立即执行,且会清空整个队列。宏任务每次只执行一个。

考点 2:输出顺序问题

给定一段包含 setTimeout、Promise、async/await 的代码,能准确分析输出顺序。

考点 3:Promise.then vs setTimeout

Promise.then 是微任务,setTimeout 是宏任务。微任务优先于宏任务执行。


延展阅读