< 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
英文地址,請注明出處:http://en.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 如下: