返回首页

16. 说说你对宏任务和微任务的理解?

问题解析

宏任务(MacroTask)和微任务(MicroTask)是JavaScript异步编程的核心概念。理解它们的区别、执行顺序以及应用场景,对于掌握JavaScript的异步执行机制至关重要。

核心概念

宏任务(MacroTask)

宏任务是JavaScript中最基础的异步任务类型,由宿主环境(浏览器或Node.js)提供。每个宏任务代表一个完整的执行单元。

特点

  • 时间粒度较大,执行间隔不能精确控制
  • 每次事件循环只执行一个宏任务
  • 宏任务执行完毕后,会清空微任务队列

微任务(MicroTask)

微任务是在当前宏任务执行结束后、下一个宏任务开始之前执行的任务。微任务的执行优先级高于宏任务。

特点

  • 执行时机在当前宏任务结束后、渲染之前
  • 微任务队列会一次性全部执行完毕
  • 如果在执行微任务过程中又产生微任务,会继续执行直到队列为空

详细解答

常见的宏任务和微任务

类型 常见API
宏任务 script代码、setTimeout、setInterval、I/O、UI渲染、postMessage
微任务 Promise.then/catch/finally、MutationObserver、queueMicrotask、process.nextTick(Node.js)

执行顺序示例

console.log('1'); // 同步

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

Promise.resolve().then(() => {
  console.log('3'); // 微任务
});

console.log('4'); // 同步

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

微任务的优先级

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

Promise.resolve().then(() => {
  console.log('Promise1');
});

Promise.resolve().then(() => {
  console.log('Promise2');
});

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

// 输出: Promise1 -> Promise2 -> queueMicrotask -> setTimeout

深入理解

事件循环的完整流程

1. 执行同步代码(这本身是一个宏任务)
2. 执行完同步代码后,检查微任务队列
3. 执行所有微任务(如果执行过程中产生新微任务,继续执行)
4. 微任务队列清空后,执行渲染(如果需要)
5. 执行下一个宏任务
6. 重复步骤2-5

嵌套场景分析

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

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

// 输出: timeout1 -> promise1 -> timeout2 -> promise2

解析

  1. 第一个setTimeout(宏任务)执行,输出timeout1
  2. 遇到Promise.then,加入微任务队列
  3. 第一个宏任务结束,执行微任务,输出promise1
  4. 执行第二个setTimeout(宏任务),输出timeout2
  5. 遇到Promise.then,加入微任务队列
  6. 第二个宏任务结束,执行微任务,输出promise2

微任务递归问题

let count = 0;

function addMicroTask() {
  Promise.resolve().then(() => {
    count++;
    console.log('微任务', count);
    if (count < 3) {
      addMicroTask(); // 递归添加微任务
    }
  });
}

addMicroTask();

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

// 输出: 微任务 1 -> 微任务 2 -> 微任务 3 -> 宏任务

注意:如果微任务递归没有终止条件,会导致事件循环被阻塞,页面无法渲染和响应。

最佳实践

1. 合理使用微任务

// 使用queueMicrotask显式创建微任务
function doSomething() {
  // 做一些同步工作
  console.log('同步工作');

  // 将后续工作放入微任务
  queueMicrotask(() => {
    console.log('微任务工作');
  });
}

2. 避免微任务阻塞

// 错误:微任务过多会阻塞渲染
function badPractice() {
  for (let i = 0; i < 10000; i++) {
    Promise.resolve().then(() => {
      heavyComputation();
    });
  }
}

// 正确:使用宏任务让出主线程
function goodPractice() {
  function processChunk(i) {
    if (i >= 10000) return;

    heavyComputation();

    // 每处理一部分,让出主线程
    if (i % 100 === 0) {
      setTimeout(() => processChunk(i + 1), 0);
    } else {
      processChunk(i + 1);
    }
  }

  processChunk(0);
}

3. 使用MutationObserver

// MutationObserver也是微任务
const observer = new MutationObserver((mutations) => {
  console.log('DOM变化', mutations);
});

observer.observe(document.body, {
  childList: true,
  subtree: true
});

// 修改DOM
document.body.appendChild(document.createElement('div'));
// 回调会在同步代码执行完毕后立即执行

面试要点

  1. 执行优先级:同步代码 > 微任务 > 宏任务
  2. 队列清空:每个宏任务结束后,会清空整个微任务队列
  3. 产生时机:微任务可以在其他微任务执行过程中产生,会立即执行
  4. 浏览器渲染:微任务在渲染之前执行,宏任务在渲染之后执行
  5. 实际应用:Promise使用微任务确保异步回调尽快执行

经典面试题

console.log('start');

setTimeout(() => {
  console.log('timeout1');
}, 0);

new Promise((resolve) => {
  console.log('promise');
  resolve();
}).then(() => {
  console.log('then1');
});

Promise.resolve().then(() => {
  console.log('then2');
  return Promise.resolve();
}).then(() => {
  console.log('then3');
});

console.log('end');

// 输出: start -> promise -> end -> then1 -> then2 -> then3 -> timeout1

解析

  • Promise构造函数是同步执行的,所以先输出promise
  • then1、then2是同一轮的微任务
  • then3虽然返回Promise.resolve(),但在现代浏览器中,then的链式调用会在新的微任务中执行