基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

JavaScript 中的宏任务(Macrotask)和微任务(Microtask)

知识点图片

在 JavaScript 中,宏任务(Macrotask)微任务(Microtask)是事件循环(Event Loop)机制的核心概念。理解它们对于掌握 JavaScript 的异步编程、解决代码执行顺序问题(尤其是面试中的输出顺序题)至关重要。

JavaScript 是一门单线程语言,为了不阻塞主线程,它引入了事件循环机制。所有的异步任务都会被放入任务队列中,而任务队列分为两种:宏任务队列和微任务队列。


一、 宏任务(Macrotask)

宏任务通常是由宿主环境(浏览器或 Node.js)发起的,代表一个个独立、离散的工作单元。

常见的宏任务包括:

  • 整体代码 script(可以看作是第一个宏任务)
  • setTimeout
  • setInterval
  • setImmediate(Node.js 独有)
  • I/O 操作(如文件读写、网络请求)
  • UI 渲染(浏览器)
  • DOM 事件回调(如 click 事件)

特点: 每次事件循环只执行一个宏任务。


二、 微任务(Microtask)

微任务通常是由 JavaScript 引擎本身发起的,用于处理需要在当前宏任务执行结束后、下一个宏任务开始前立即执行的较紧急任务。

常见的微任务包括:

  • Promise.then() / .catch() / .finally()
  • async/await (本质上是 Promise 的语法糖)
  • MutationObserver(浏览器环境下,用于监听 DOM 变化)
  • process.nextTick(Node.js 独有,且优先级高于其他微任务)

特点: 每次执行微任务队列时,会清空整个队列(包括在执行微任务过程中产生的新微任务)。


三、 事件循环的执行顺序(核心规律)

事件循环的运转遵循以下“黄金法则”:

  1. 执行同步代码: 从上到下执行当前所在的宏任务(通常是整体 script 代码),这期间如果遇到新的宏任务,将其加入宏任务队列;遇到新的微任务,将其加入微任务队列。
  2. 清空微任务队列: 当前宏任务的同步代码执行完毕后,主线程会去检查微任务队列。如果有微任务,就依次执行,直到微任务队列为空
  3. UI 渲染:(仅限浏览器)在微任务队列清空后,浏览器会根据需要进行页面的重新渲染。
  4. 执行下一个宏任务: 从宏任务队列中取出一个最早进入队列的宏任务,开始执行。
  5. 重复上述步骤: 宏任务 -> 微任务 -> 渲染 -> 宏任务 -> 微任务...

简记口诀: 同步代码优先 -> 耗尽微任务 -> 执行一个宏任务 -> 耗尽微任务 -> 循环。


四、 经典代码实战解析

示例 1:基础的宏任务与微任务

javascript
console.log('1. 同步代码开始');

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

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

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

输出结果: 1 -> 4 -> 3 -> 2
解析:

  1. 打印 1(同步)。
  2. 遇到 setTimeout,放入宏任务队列
  3. 遇到 Promise.then,放入微任务队列
  4. 打印 4(同步)。
  5. 当前宏任务执行完毕,检查微任务队列,发现有 Promise.then,执行并打印 3
  6. 微任务清空,开启下一个宏任务,执行 setTimeout,打印 2

示例 2:嵌套的复杂场景(高频面试题)

javascript
console.log('1');

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

new Promise((resolve) => {
    console.log('4'); // 注意:Promise 构造函数里的代码是同步执行的!
    resolve();
}).then(() => {
    console.log('5');
    setTimeout(() => {
        console.log('6');
    }, 0);
});

console.log('7');

输出结果: 1 -> 4 -> 7 -> 5 -> 2 -> 3 -> 6
逐步推导:

  1. [宏任务(整体代码)执行中]:打印 1。遇到 setTimeout,将其回调记为 Macro1
  2. 遇到 new Promise,内部代码同步执行,打印 4。调用 resolve() 后,将 .then 的回调记为 Micro1
  3. 打印 7
  4. [清空微任务队列]:执行 Micro1。打印 5。遇到内部的 setTimeout,将其回调记为 Macro2
  5. [执行下一个宏任务]:取出 Macro1 执行。打印 2。遇到 .then,记为 Micro2
  6. [清空微任务队列]:执行 Micro2。打印 3
  7. [执行下一个宏任务]:取出 Macro2 执行。打印 6

示例 3:引入 async/await

记住一点:await 后面的代码,相当于被放到了 Promise.then() 里面,属于微任务

javascript
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end'); // 这一句等于放进了微任务
}

async function async2() {
    console.log('async2');
}

console.log('script start');

async1();

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

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});

console.log('script end');

输出结果:
script start -> async1 start -> async2 -> promise1 -> script end -> async1 end -> promise2 -> setTimeout


五、 为什么要区分宏任务和微任务?

其实主要为了解决异步任务的优先级和插队问题

  • 宏任务是按顺序执行的。如果两个宏任务之间,DOM 发生了变化,浏览器会捕捉到并渲染。
  • 微任务的存在是为了在当前任务执行完后,立即执行一些高优先级的逻辑,而不必等待下一个周期的到来。如果在执行宏任务之后立刻进行 UI 渲染,那么在渲染前通过微任务更新 DOM(如 Vue 的 nextTick,或者批量状态更新),就能避免浏览器进行无用的多次渲染,提升性能。

六、 总结对比

特性 宏任务 (Macrotask) 微任务 (Microtask)
发起者 宿主环境(浏览器 / Node.js) JavaScript 引擎
包含内容 script, setTimeout, setInterval, I/O, UI交互 Promise.then, MutationObserver, process.nextTick
执行时机 在每个 Event Loop 的阶段执行一个 在当前宏任务结束后,下个宏任务开始前,或页面渲染前执行
清空机制 每次执行一个 每次执行队列中的所有(直到为空)
00:00
00:00