在上一篇关于 JS 事件循环的文章中,我们提到 “微任务优先级高于宏任务” 这一核心结论,但对于微任务本身的细节并未展开。作为事件循环中 “优先级最高的异步任务”,微任务的执行机制直接影响代码逻辑的正确性,比如Promise.then的触发时机、async/await的阻塞逻辑等,都与微任务密切相关。今天我们就聚焦微任务,从本质、类型、执行机制到实战误区,进行全方位拆解。

一、微任务的本质:为什么它比宏任务 “更快”?

首先要明确一个核心问题:同样是异步任务,为什么微任务的优先级高于宏任务?这需要从微任务的设计初衷和执行时机说起。

1. 微任务的定义

微任务(Microtask)是 JS 事件循环中一种特殊的异步任务,它的核心特征是:在当前宏任务执行完毕后、下一个宏任务开始前执行,且会 “阻塞” 下一个宏任务,直到所有微任务执行完毕。

简单来说,微任务是为了处理 “需要在当前同步代码结束后、页面重新渲染前快速执行的轻量级异步操作”,比如 Promise 的状态回调、DOM 更新后的后续处理等。相比宏任务(如setTimeout,需要等待浏览器的定时器模块触发),微任务的执行更 “急切”,不需要等待额外的浏览器模块调度,直接在 JS 引擎内部完成排队和执行。

2. 微任务的 “快” 体现在哪里?

我们用一个对比案例直观感受:

// 宏任务:setTimeout

setTimeout(() => {

 console.log("macro task"); // 宏任务回调

}, 0);

// 微任务:Promise.then

Promise.resolve().then(() => {

 console.log("micro task"); // 微任务回调

});

console.log("sync code"); // 同步代码

最终输出顺序是:sync codemicro taskmacro task

原因在于:

  • 同步代码执行完毕后,调用栈为空;

  • 事件循环先检查微任务队列,执行Promise.then回调;

  • 微任务队列清空后,才检查宏任务队列,执行setTimeout回调。

这就是微任务 “快” 的本质:它穿插在两个宏任务之间,优先占用 “宏任务间隙” 的执行时间。

二、常见微任务类型:这些操作都属于微任务

在实际开发中,我们常用的微任务主要有以下 4 类,需要准确识别,避免混淆:

1. Promise 相关回调(最常用)

Promisethencatchfinally方法注册的回调,是最典型的微任务。需要注意的是:Promise 构造函数内部的代码是同步的,只有回调函数才是微任务

示例:

new Promise((resolve, reject) => {

 console.log("同步代码:Promise构造函数内"); // 同步执行

 resolve("成功"); // 触发then回调

}).then((res) => {

 console.log("微任务:", res); // 微任务,同步代码执行完后触发

}).catch((err) => {

 console.log("微任务:", err); // 微任务,仅在reject时触发

});

2. async/await(语法糖本质)

async/await是 ES2017 引入的异步语法糖,其本质是基于 Promise 实现的,因此await后面的代码也属于微任务。

需要重点理解await的执行逻辑:

  • await会 “暂停” 当前async函数的执行,先执行await后面的表达式;

  • 如果表达式返回一个 Promise,会等待 Promise resolve 后,将await后续的代码(即 “恢复执行” 的逻辑)加入微任务队列;

  • 如果表达式返回非 Promise 值,会直接将后续代码加入微任务队列(相当于Promise.resolve(非Promise值).then(后续代码))。

示例:

async function asyncFn() {

 console.log("1:async函数内同步代码");

 // await后面是Promise,后续代码(console.log(3))加入微任务

 await Promise.resolve().then(() => {

   console.log("2:await内部的微任务");

 });

 console.log("3:await后续代码(微任务)");

}

asyncFn();

console.log("4:外部同步代码");

输出顺序:1423

解析:await会先让外部同步代码执行(输出 4),再执行内部微任务(输出 2),最后执行await后续的微任务(输出 3)。

3. queueMicrotask(显式创建微任务)

queueMicrotask是 ES2022 引入的 API,用于显式地将一个函数加入微任务队列,功能与Promise.resolve().then(函数)一致,但代码更简洁,语义更明确。

示例:

console.log("同步代码");

queueMicrotask(() => {

 console.log("显式创建的微任务");

});

// 输出:同步代码 → 显式创建的微任务

使用场景:当你需要确保一段代码在当前同步代码结束后、下一个宏任务前执行,且不想通过 Promise 间接实现时,queueMicrotask是更优选择。

4. MutationObserver(DOM 监听相关)

MutationObserver用于监听 DOM 元素的变化(如节点新增、属性修改、文本变化等),当 DOM 发生变化时,它的回调函数会被加入微任务队列。

示例:

// 创建一个DOM元素

const div = document.createElement("div");

// 监听div的文本变化

const observer = new MutationObserver((mutations) => {

 console.log("微任务:DOM发生变化", mutations[0].target.textContent);

});

observer.observe(div, { childList: true, characterData: true, subtree: true });

// 修改DOM文本(同步操作)

div.textContent = "Hello Microtask";

console.log("同步代码:DOM修改完成");

输出顺序:同步代码:DOM修改完成微任务:DOM发生变化 Hello Microtask

解析:DOM 修改是同步操作,但MutationObserver的回调会延迟到微任务中执行,避免频繁触发回调导致性能问题。

三、微任务的执行机制:3 个核心规则

理解微任务的执行机制,需要记住 3 个核心规则,这是解决复杂异步问题的关键:

规则 1:微任务队列 “先进先出”,且会一次性清空

当调用栈为空时,事件循环会依次取出微任务队列中的任务执行,直到队列完全为空,不会中途切换到宏任务。即使在执行微任务的过程中新增了新的微任务,也会加入当前队列的末尾,等待本次 “微任务清空阶段” 执行。

示例:

Promise.resolve().then(() => {

 console.log("微任务1");

 // 执行微任务1时,新增微任务2

 Promise.resolve().then(() => {

   console.log("微任务2");

 });

});

Promise.resolve().then(() => {

 console.log("微任务3");

});

console.log("同步代码");

输出顺序:同步代码微任务1微任务3微任务2

解析:

  1. 同步代码执行完后,微任务队列初始有两个任务:[微任务 1, 微任务 3];

  2. 执行微任务 1 时,新增微任务 2,队列变为 [微任务 3, 微任务 2];

  3. 继续执行队列中的微任务 3,最后执行微任务 2,直到队列清空。

规则 2:微任务在 “当前宏任务结束后” 执行

这里的 “当前宏任务” 指的是:

  • 如果是全局代码,“当前宏任务” 就是整个script标签的代码;

  • 如果是宏任务回调(如setTimeout回调),“当前宏任务” 就是该回调函数的代码。

简单来说:一个宏任务执行完毕后,必须先清空所有微任务,才能开始下一个宏任务

示例:

// 宏任务1:script标签全局代码

console.log("宏任务1:同步代码");

// 微任务1:在宏任务1内注册

Promise.resolve().then(() => {

 console.log("微任务1:宏任务1结束后执行");

});

// 宏任务2:setTimeout回调

setTimeout(() => {

 console.log("宏任务2:同步代码");

 // 微任务2:在宏任务2内注册

 Promise.resolve().then(() => {

   console.log("微任务2:宏任务2结束后执行");

 });

}, 0);

输出顺序:宏任务1:同步代码微任务1:宏任务1结束后执行宏任务2:同步代码微任务2:宏任务2结束后执行

解析:宏任务 1 执行完后,先清空微任务 1,再执行宏任务 2;宏任务 2 执行完后,再清空微任务 2。

规则 3:微任务不会阻塞当前同步代码

微任务虽然优先级高,但它仍然是 “异步任务”,不会阻塞当前同步代码的执行。只有当当前同步代码执行完毕、调用栈为空时,微任务才会开始执行。

示例:

console.log("同步代码1");

Promise.resolve().then(() => {

 console.log("微任务");

});

console.log("同步代码2");

输出顺序:同步代码1同步代码2微任务

解析:注册微任务后,JS 引擎会继续执行后续的同步代码(输出 “同步代码 2”),直到同步代码执行完、调用栈为空,才会执行微任务。

四、微任务与宏任务的核心差异(对比表)

为了更清晰地理解微任务,我们将它与宏任务的关键差异整理成表格,方便对比记忆:

对比维度 微任务(Microtask) 宏任务(Macrotask)
常见类型 Promise.then/catch/finally、async/await、queueMicrotask、MutationObserver setTimeout、setInterval、DOM 事件、script 标签、postMessage、fetch(回调)
执行时机 当前宏任务结束后、下一个宏任务开始前 所有微任务清空后
执行优先级 高(先于宏任务) 低(后于微任务)
队列处理方式 一次性清空所有任务 每次只执行一个任务,执行后检查微任务
是否阻塞页面渲染 可能(微任务执行时,页面会等待其完成再渲染) 不会(宏任务执行前,页面可能已完成渲染)

五、实战避坑:微任务的 3 个常见误区

在实际开发中,很多开发者会因为对微任务的理解不深入,写出不符合预期的代码。以下是 3 个最常见的误区,需要重点规避:

误区 1:认为 “await 会阻塞所有代码”

await的 “暂停” 是局部的,只会暂停当前async函数的执行,不会阻塞外部的同步代码或其他宏任务。

错误示例(预期输出:a→b→c,实际输出:a→c→b):

async function fn() {

 console.log("a");

 await Promise.resolve(); // 此处暂停fn函数,但不阻塞外部代码

 console.log("b"); // 微任务:需等待外部同步代码执行完

}

fn();

console.log("c"); // 外部同步代码:先于b执行

解析:await暂停fn函数后,JS 引擎会继续执行外部的同步代码(输出 “c”),直到同步代码执行完,才会执行await后续的微任务(输出 “b”)。

误区 2:混淆 “Promise 构造函数” 与 “then 回调” 的执行时机

Promise 构造函数内部的代码是同步执行的,只有then/catch/finally回调才是微任务。

错误示例(预期输出:1→3→2,实际输出:1→2→3):

console.log("1:同步代码");

new Promise((resolve) => {

 console.log("2:Promise构造函数内(同步)");

 resolve();

}).then(() => {

 console.log("3:then回调(微任务)");

});

解析:构造函数内的 “2” 是同步代码,会在 “1” 之后直接执行;“3” 是微任务,需等待同步代码执行完后才触发。

误区 3:认为 “多个微任务队列会按类型优先级执行”

有些开发者误以为 “不同类型的微任务有不同优先级”(如Promise.thenqueueMicrotask先执行),但实际上,所有微任务都在同一个队列中,按 “注册顺序” 执行,与类型无关。

示例:

// 先注册queueMicrotask

queueMicrotask(() => {

 console.log("微任务1:queueMicrotask");

});

// 后注册Promise.then

Promise.resolve().then(() => {

 console.log("微任务2:Promise.then");

});

输出顺序:微任务1:queueMicrotask微任务2:Promise.then

解析:微任务队列按 “注册时间” 排序,先注册的先执行,与类型无关。

六、总结:微任务的核心要点

  1. 本质:微任务是 “宏任务间隙” 执行的轻量级异步任务,优先级高于宏任务,旨在快速处理后续逻辑;

  2. 常见类型:Promise 回调、async/await 后续代码、queueMicrotask、MutationObserver;

  3. 执行机制

  • 一个宏任务结束后,必须清空所有微任务,再执行下一个宏任务;

  • 微任务队列按 “先进先出” 执行,执行过程中新增的微任务会追加到当前队列末尾;

  • 微任务不会阻塞当前同步代码,仅在调用栈为空时执行;

  1. 避坑关键:区分 Promise 构造函数(同步)与回调(微任务),理解await的局部暂停特性,牢记微任务按注册顺序执行。

掌握微任务的核心逻辑,不仅能解决 “代码执行顺序” 问题,更能在处理复杂异步场景(如并发请求、DOM 更新后的数据处理)时,写出更高效、更可靠的代码。如果对某个微任务类型或执行场景还有疑问,不妨动手写几个示例测试,实践是理解异步逻辑的最佳方式!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐