文章首發自筆者的 Github。
Promise 關於 API 這塊大家應該都能熟練使用,但是和微任務相關的你可能還存在知識盲區。
前置知識
在開始正文前,我們先把本文涉及到的一些內容提前定個基調。
Promise 哪些 API 涉及了微任務?
Promise 中只有涉及到狀態變更後才需要被執行的回撥才算是微任務,比如說 then
、 catch
、finally
,其他所有的程式碼執行都是巨集任務(同步執行)。
上圖中藍色為同步執行,黃色為非同步執行(丟到微任務佇列中)。
這些微任務何時被加入微任務佇列?
這個問題我們根據 ecma 規範來看:
- 如果此時 Promise 狀態為 pending,那麼成功或失敗的回撥會分別被加入至
[[PromiseFulfillReactions]]
和[[PromiseRejectReactions]]
中。如果你看過手寫 Promise 的程式碼的話,應該能發現有兩個陣列儲存這些回撥函式。 - 如果此時 Promise 狀態為非 pending 時,回撥會成為 Promise Jobs,也就是微任務。
瞭解完以上知識後,正片開始。
同一個 then,不同的微任務執行
初級
Promise.resolve()
.then(() => {
console.log("then1");
Promise.resolve().then(() => {
console.log("then1-1");
});
})
.then(() => {
console.log("then2");
});
以上程式碼大家應該都能得出正確的答案:then1 → then1-1 → then2
。
雖然 then
是同步執行,並且狀態也已經變更。但這並不代表每次遇到 then
時我們都需要把它的回撥丟入微任務佇列中,而是等待 then
的回撥執行完畢後再根據情況執行對應操作。
基於此,我們可以得出第一個結論:鏈式呼叫中,只有前一個 then
的回撥執行完畢後,跟著的 then
中的回撥才會被加入至微任務佇列。
中級
大家都知道了 Promise resolve
後,跟著的 then
中的回撥會馬上進入微任務佇列。
那麼以下程式碼你認為的輸出會是什麼?
let p = Promise.resolve();
p.then(() => {
console.log("then1");
Promise.resolve().then(() => {
console.log("then1-1");
});
}).then(() => {
console.log("then1-2");
});
p.then(() => {
console.log("then2");
});
按照一開始的認知我們不難得出 then2
會在 then1-1
後輸出,但是實際情況卻是相反的。
基於此我們得出第二個結論:每個鏈式呼叫的開端會首先依次進入微任務佇列。
接下來我們換個寫法:
let p = Promise.resolve().then(() => {
console.log("then1");
Promise.resolve().then(() => {
console.log("then1-1");
});
}).then(() => {
console.log("then2");
});
p.then(() => {
console.log("then3");
});
上述程式碼其實有個陷阱,then
每次都會返回一個新的 Promise,此時的 p
已經不是 Promise.resolve()
生成的,而是最後一個 then
生成的,因此 then3
應該是在 then2
後列印出來的。
順便我們也可以把之前得出的結論優化為:同一個 Promise 的每個鏈式呼叫的開端會首先依次進入微任務佇列。
高階
以下大家可以猜猜 then1-2
會在何時列印出來?
Promise.resolve()
.then(() => {
console.log("then1");
Promise.resolve()
.then(() => {
console.log("then1-1");
return 1;
})
.then(() => {
console.log("then1-2");
});
})
.then(() => {
console.log("then2");
})
.then(() => {
console.log("then3");
})
.then(() => {
console.log("then4");
});
這題肯定是簡單的,記住第一個結論就能得出答案,以下是解析:
- 第一次
resolve
後第一個then
的回撥進入微任務佇列並執行,列印then1
- 第二次
resolve
後內部第一個then
的回撥進入微任務佇列,此時外部第一個then
的回撥全部執行完畢,需要將外部的第二個then
回撥也插入微任務佇列。 - 執行微任務,列印
then1-1
和then2
,然後分別再將之後then
中的回撥插入微任務佇列 - 執行微任務,列印
then1-2
和then3
,之後的內容就不一一說明了
接下來我們把 return 1
修改一下,結果可就大不相同啦:
Promise.resolve()
.then(() => {
console.log("then1");
Promise.resolve()
.then(() => {
console.log("then1-1");
return Promise.resolve();
})
.then(() => {
console.log("then1-2");
});
})
.then(() => {
console.log("then2");
})
.then(() => {
console.log("then3");
})
.then(() => {
console.log("then4");
});
當我們 return Promise.resolve()
時,你猜猜 then1-2
會何時列印了?
答案是最後一個才被列印出來。
為什麼在 then
中分別 return
不同的東西,微任務的執行順序竟有如此大的變化?以下是筆者的解析。
PS:then
返回一個新的 Promise,並且會用這個 Promise 去 resolve
返回值,這個概念需要大家先了解一下。
根據 Promise A+ 規範
根據規範 2.3.2,如果 resolve
了一個 Promise,需要為其加上一個 then
並 resolve
。
if (x instanceof MyPromise) {
if (x.currentState === PENDING) {
} else {
x.then(resolve, reject);
}
return;
}
上述程式碼節選自手寫 Promise 實現。
那麼根據 A+ 規範來說,如果我們在 then
中返回了 Promise.resolve
的話會多入隊一次微任務,但是這個結論還是與實際不符的,因此我們還需要尋找其他權威的文件。
根據 ECMA - 262 規範
根據規範 25.6.1.3.2,當 Promise resolve
了一個 Promise 時,會產生一個NewPromiseResolveThenableJob,這是屬於 Promise Jobs 中的一種,也就是微任務。
This Job uses the supplied thenable and its then method to resolve the given promise. This process must take place as a Job to ensure that the evaluation of the then method occurs after evaluation of any surrounding code has completed.
並且該 Jobs 還會呼叫一次 then
函式來 resolve Promise
,這也就又生成了一次微任務。
這就是為什麼會觸發兩次微任務的來源。
最後
文章到這裡就完結了,大家有什麼疑問都可以在評論區提出。
推薦關注我的微信公眾號【前端真好玩】,工作日推送高質量文章。
筆者就職於酷家樂,裝潢設計行業獨角獸。一流的視覺化、前端技術團隊,有興趣的可以簡歷投遞至 zx597813039@gmail.com