在上一篇關于 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 code
→ micro task
→ macro task
。
原因在于:
-
同步代碼執行完畢后,調用棧為空;
-
事件循環先檢查微任務隊列,執行
Promise.then
回調; -
微任務隊列清空后,才檢查宏任務隊列,執行
setTimeout
回調。
這就是微任務 “快” 的本質:它穿插在兩個宏任務之間,優先占用 “宏任務間隙” 的執行時間。
二、常見微任務類型:這些操作都屬于微任務
在實際開發中,我們常用的微任務主要有以下 4 類,需要準確識別,避免混淆:
1. Promise 相關回調(最常用)
Promise
的then
、catch
、finally
方法注冊的回調,是最典型的微任務。需要注意的是: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:外部同步代碼");
輸出順序:1
→ 4
→ 2
→ 3
。
解析: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, 微任務 3];
-
執行微任務 1 時,新增微任務 2,隊列變為 [微任務 3, 微任務 2];
-
繼續執行隊列中的微任務 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.then
比queueMicrotask
先執行),但實際上,所有微任務都在同一個隊列中,按 “注冊順序” 執行,與類型無關。
示例:
// 先注冊queueMicrotaskqueueMicrotask(() => {console.log("微任務1:queueMicrotask");});// 后注冊Promise.thenPromise.resolve().then(() => {console.log("微任務2:Promise.then");});
輸出順序:微任務1:queueMicrotask
→ 微任務2:Promise.then
。
解析:微任務隊列按 “注冊時間” 排序,先注冊的先執行,與類型無關。
六、總結:微任務的核心要點
-
本質:微任務是 “宏任務間隙” 執行的輕量級異步任務,優先級高于宏任務,旨在快速處理后續邏輯;
-
常見類型:Promise 回調、async/await 后續代碼、queueMicrotask、MutationObserver;
-
執行機制:
-
一個宏任務結束后,必須清空所有微任務,再執行下一個宏任務;
-
微任務隊列按 “先進先出” 執行,執行過程中新增的微任務會追加到當前隊列末尾;
-
微任務不會阻塞當前同步代碼,僅在調用棧為空時執行;
- 避坑關鍵:區分 Promise 構造函數(同步)與回調(微任務),理解
await
的局部暫停特性,牢記微任務按注冊順序執行。
掌握微任務的核心邏輯,不僅能解決 “代碼執行順序” 問題,更能在處理復雜異步場景(如并發請求、DOM 更新后的數據處理)時,寫出更高效、更可靠的代碼。如果對某個微任務類型或執行場景還有疑問,不妨動手寫幾個示例測試,實踐是理解異步邏輯的最佳方式!