< JS事件循环系列【二】> 微任务深度解析:从本质到实战避坑

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

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

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

1. 微任务的定义

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

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

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

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

// 宏任务:setTimeoutsetTimeout(() => {console.log("macro task"); // 宏任务回调}, 0);// 微任务:Promise.thenPromise.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时,新增微任务2Promise.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、MutationObserversetTimeout、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先执行),但实际上,所有微任务都在同一个队列中,按 “注册顺序” 执行,与类型无关。

示例:

// 先注册queueMicrotaskqueueMicrotask(() => {console.log("微任务1:queueMicrotask");});// 后注册Promise.thenPromise.resolve().then(() => {console.log("微任务2:Promise.then");});

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

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

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

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

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

  3. 执行机制

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

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

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/98974.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/98974.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

STM32 单片机开发 - SPI 总线

一、SPI 总线概念SPI 总线 --- Serial Peripheral Interface,即串行外设接口SPI 是摩托罗拉公司设计的一款 串行、同步、全双工总线;SPI 总线是三线 / 四线制总线,分别是:SPI_SCK(时钟线)、S…

区域医院云HIS系统源码,云医院管理系统源码,云诊所源码

云HIS源码,云医院管理系统源码,云诊所源码,二级专科医院云HIS系统源代码,有演示云HIS,即云医院管理系统,是一种运用云计算、大数据、物联网等新兴信息技术的医疗信息化解决方案。它重新定义了传统的医院信息…

Java基础 9.11

1.第三代日期类前面两代日期类的不足分析JDK 1.0中包含了一个java.uti.Date类,但是它的大多数方法已经在JDK1.1引Calendar类之后被弃用了。而Calendar也存在问题是:可变性:像日期和时间这样的类应该是不可变的偏移性:Date中的年份…

JavaScript 数组过滤方法

在 JavaScript 编程中,数组是最常用的数据结构之一,而数组过滤则是处理数据集合的关键操作。filter() 方法提供了一种高效的方式来从数组中筛选出符合特定条件的元素,返回一个新的数组,而不改变原始数组。这种方法在处理对象数组时…

《sklearn机器学习——数据预处理》离散化

sklearn 数据预处理中的离散化(Discretization) 离散化是将连续型数值特征转换为离散区间(分箱/bins)的过程,常用于简化模型、增强鲁棒性、处理非线性关系或满足某些算法对离散输入的要求(如朴素贝叶斯、决…

PTA算法简析

ArkAnalyzer源码初步分析I:https://blog.csdn.net/2302_80118884/article/details/151627341?spm1001.2014.3001.5501 首先,我们必须明确 PTA 的核心工作:它不再关心变量的“声明类型”,而是为程序中的每一个变量和每一个对象字段…

Vue 3 中监听多个数据变化的几种方法

1. 使用 watch监听多个 ref/reactive 数据import { ref, watch } from vueexport default {setup() {const count ref(0)const name ref()const user reactive({ age: 20 })// 监听多个数据源watch([count, name, () > user.age], // 数组形式传入多个数据源([newCount, …

第 2 篇:Java 入门实战(JDK8 版)—— 编写第一个 Java 程序,理解基础运行逻辑

用 IntelliJ IDEA 写第一个 Java 8 程序:Hello World 实操指南 作为 Java 初学者,“Hello World” 是你接触这门语言的第一个里程碑。本文会聚焦 Java 8(经典 LTS 版本,企业级开发常用) 和 IntelliJ IDEA(当…

【GPT入门】第67课 多模态模型实践: 本地部署文生视频模型和图片推理模型

【GPT入门】第67课 多模态模型实践: 本地部署文生视频模型和图片推理模型1. 文生视频模型CogVideoX-5b 本地部署1.1 模型介绍1.2 环境安装1.3 模型下载1.4 测试2.ollama部署图片推理模型 llama3.2-vision2.1 模型介绍2.2 安装ollama2.3 下载模型2.4 测试模型2.5 测试…

C++初阶(6)类和对象(下)

1. 再谈构造函数(构造函数的2个深入使用技巧) 1.1 构造函数体赋值 在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。 虽然上述构造函数调用之后,对象中已经有了一个初始值,…

容器文件描述符热迁移在云服务器高可用架构的实施标准

在云计算环境中,容器文件描述符热迁移技术正成为保障业务连续性的关键解决方案。本文将深入解析该技术在云服务器高可用架构中的实施标准,涵盖技术原理、实现路径、性能优化等核心维度,为构建稳定可靠的容器化基础设施提供系统化指导。 容器文…

毫米波雷达液位计如何远程监控水位?

引言毫米波雷达液位计作为一种高精度、非接触式的水位监测设备,正逐渐成为智慧水务、环境监测等领域的关键工具。其通过先进的调频连续波(FMCW)技术,实现5mm的测量精度,并支持多种远程通信方式,使用户能够实…

关于 C++ 编程语言常见问题及技术要点的说明

关于 C 编程语言常见问题及技术要点的说明C 作为一门兼具高效性与灵活性的静态编译型编程语言,自 1985 年正式发布以来,始终在系统开发、游戏引擎、嵌入式设备、高性能计算等领域占据核心地位。随着 C 标准(如 C11、C17、C20)的持…

【Qt QSS样式设置】

Qt中的QSS样式设置流程 Qt Style Sheets (QSS) 是Qt框架中用于自定义控件外观的样式表语言,其语法类似于CSS。以下是QSS的设置流程和示例。 QSS设置流程 1. 创建QSS样式表文件或字符串 首先,需要创建QSS样式表,可以是一个单独的.qss文件&…

使用 Apollo TransformWrapper 生成相机到各坐标系的变换矩阵

使用 Apollo TransformWrapper 生成相机到各坐标系的变换矩阵一、背景二、原理1、什么是变换矩阵?2、为什么需要变换矩阵?3、Apollo 中的坐标系4、Apollo TransformWrapper三、操作步骤1. 设置车辆参数2. 启动静态变换发布3. 查看变换信息4. 播放记录数据…

硬件(十)IMX6ULL 中断与时钟配置

一、OCP 原则(开闭原则)对代码扩展是开放的,允许通过新增代码来扩展功能;对代码的修改是关闭的,尽量避免直接修改已有稳定运行的代码,以此保障代码的稳定性与可维护性。二、中断处理(一&#xf…

打工人日报#20250913

打工人日报#20250913 周六,回杭州了,这边居然下雨。 阅读 《小米创业思考》 第七章 技术为本 其中的技术介绍算是比较详细的,架构也很清晰,有一种对自己家产品如数家珍的感觉,对于架构也是经常思考的感觉感恩 和namwei…

【面试题】RAG核心痛点

1. 文档切分粒度不好把控,既担心噪声太多又担心语义信息丢失 这是一个经典难题。切分粒度过大,单个chunk包含过多无关信息(噪声),会干扰LLM理解核心内容;切分过小,则可能割裂句子或段落的完整语…

网络安全与iptables防火墙配置

iptables基本概念iptables是Linux系统中强大的防火墙工具,它工作在用户空间,通过命令行界面与内核空间的netfilter框架交互,实现数据包过滤、网络地址转换(NAT)等功能。Web服务器防火墙配置实例以下是针对Web服务器的iptables配置步骤&#x…

qt中给QListWidget添加上下文菜单(快捷菜单)

步骤 添加customContextMenuRequested信号的槽函数,添加后,在QListWidget上单击右键,无法响应,还必须执行下面操作;设置QListWidget上下文菜单策略为Qt::CustomContextMenu 如下: