JavaScript 可視化:Promise執(zhí)行徹底搞懂
共 5244字,需瀏覽 11分鐘
·
2024-04-26 09:17
大廠技術(shù) 高級前端 Node進階
點擊上方 程序員成長指北,關(guān)注公眾號
回復1,加入高級Node交流群
深入探討了 JavaScript 中 Promise 的內(nèi)部機制,解釋了它們?nèi)绾问巩惒饺蝿?wù)以非阻塞方式執(zhí)行,并展示了 Promise 的創(chuàng)建、狀態(tài)變化以及與事件循環(huán)的關(guān)系。
正文從這開始~~
JavaScript 中的 Promise 一開始可能會讓人感到有些難以理解,但是如果我們能夠理解其內(nèi)部的工作原理,就會發(fā)現(xiàn)它們其實是非常易于掌握的。
在這篇博客文章中,我們將深入探討 Promise 的一些內(nèi)部機制,并探索它們是如何使得 JavaScript 能夠執(zhí)行非阻塞的異步任務(wù)。
一種創(chuàng)建 Promise 的方式是使用 new Promise 構(gòu)造函數(shù),它接收一個執(zhí)行函數(shù),該函數(shù)帶有 resolve 和 reject 參數(shù)。
new Promise((resolve, reject) => {
// TODO(Lydia): Some async stuff here
});
當 Promise 構(gòu)造函數(shù)被調(diào)用時,會發(fā)生以下幾件事情:
創(chuàng)建一個 Promise 對象。這個 Promise 對象包含幾個內(nèi)部槽,包括
[[PromiseState]]、[[PromiseResult]]、[[PromiseIsHandled]]、[[PromiseFulfillReactions]]和[[PromiseRejectReactions]]。創(chuàng)建一個 Promise 能力記錄。這個記錄 “封裝” 了 Promise,并增加了額外的功能來 resolve 或 reject promise。這些功能可控制 promise 的最終
[[PromiseState]]和[[PromiseResult]],并啟動異步任務(wù)。
我們可以通過調(diào)用 resolve 來解決這個 Promise,這是通過執(zhí)行函數(shù)可以實現(xiàn)的。當我們調(diào)用 resolve 時:
[[PromiseState]]被設(shè)置為 “已實現(xiàn)”(fulfilled)。[[PromiseResult]]被設(shè)置為我們傳遞給 resolve 的值,在這種情況下是 “完成!”(Done!)。
調(diào)用 reject 時的過程類似,現(xiàn)在 [[PromiseState]] 被設(shè)置為 “已拒絕”(rejected),并且 [[PromiseResult]] 被設(shè)置為我們傳遞給 reject 的值,這是 “失??!”(Fail!)。
當然很好。但是,使用函數(shù)來改變對象內(nèi)部屬性有什么特別的呢?
答案就在與我們目前跳過的兩個內(nèi)部槽相關(guān)的行為中:[[PromiseFulfillReactions]] 和 [[PromiseRejectReactions]]。
[[PromiseFulfillReactions]] 字段包含 Promise Reactions。這是一個通過將 then 處理程序鏈接到 Promise 而創(chuàng)建的對象。
此 Promise Reaction 包含一個 [[Handler]] 屬性,其中包含我們傳遞給它的回調(diào)。當 promise resolve 時,該處理程序會被添加到微任務(wù)隊列中,并可訪問 promise 解析時的值。
當 promise 解析時,這個處理程序接收到 [[PromiseResult]] 的值作為其參數(shù),然后將其推送到 Microtask Queue 微任務(wù)隊列。
這就是 promise 的異步部分發(fā)揮作用的地方!
微任務(wù)隊列是事件循環(huán)(event loop)中的一個專門隊列。當調(diào)用棧(Call Stack)為空時,事件循環(huán)首先處理微任務(wù)隊列中等待的任務(wù),然后再處理來自常規(guī)任務(wù)隊列(也稱為 “回調(diào)隊列” 或 “宏任務(wù)隊列”)的任務(wù)。
如果你想了解更多,可以查看我的事件循環(huán)視頻!
類似地,我們可以通過鏈式 catch 來創(chuàng)建一個 Promise Reaction 記錄來處理 Promise Reject。當 Promise 被拒絕時,這個回調(diào)會被添加到微任務(wù)隊列。
到目前為止,我們只是在執(zhí)行函數(shù)內(nèi)直接調(diào)用 resolve 或 reject。雖然這是可能的,但它并沒有充分利用 Promise 的全部功能(和主要目的)!
在大多數(shù)情況下,我們希望在稍后的某個時間點(通常是異步任務(wù)完成時)進行 resolve 或 reject。
異步任務(wù)在主線程之外執(zhí)行,例如讀取文件(如 fs.readFile)、提出網(wǎng)絡(luò)請求(如 https.get 或 XMLHttpRequest),或者像定時器(setTimeout)這樣簡單的任務(wù)。
當這些任務(wù)在未來某個未知的時間點完成時,我們可以使用此類異步操作通常提供的回調(diào)功能,要么使用異步任務(wù)返回的數(shù)據(jù)進行 resolve,要么在發(fā)生錯誤時進行 reject。
為了直觀地說明這一點,讓我們一步步來執(zhí)行。為了讓這個演示簡單但仍然真實,我們將使用 setTimeout 來添加一些異步行為。
new Promise((resolve) => {
setTimeout(() => resolve("Done!"), 100);
}).then(result => console.log(result))
首先,將 new Promise 構(gòu)造函數(shù)添加到調(diào)用棧,并創(chuàng)建 Promise 對象。
然后,執(zhí)行函數(shù)被執(zhí)行。在函數(shù)體的第一行,我們調(diào)用了 setTimeout,并將其添加到調(diào)用堆棧中。
setTimeout 負責在 Timers Web API 中調(diào)度計時器,延遲時間為 100 毫秒,之后我們傳遞給 setTimeout 的回調(diào)將被推送到任務(wù)隊列。
這里的異步行為與 setTimeout 有關(guān),與 promise 無關(guān)。我在這里展示這個是為了展示承諾的常見用法 —— 在一些延遲后解決一個 promise。
然而,延遲本身并不是由 promise 引起的。promise 被設(shè)計為與異步操作一起工作,但這些異步操作可以來自不同的來源,如定時器或網(wǎng)絡(luò)請求。
在定時器和構(gòu)造函數(shù)從調(diào)用棧中彈出后,引擎遇到了 then。
then 被添加到調(diào)用棧,并創(chuàng)建了一個 Promise Reaction 記錄,該處理程序就是我們作為回調(diào)傳遞給 then 處理程序的代碼。
由于 [[PromiseState]] 仍然是 “掛起”(pending),這個 Promise Reaction 記錄會被添加到 [[PromiseFulfillReactions]] 列表中。
100 毫秒過后,setTimeout 回調(diào)被推送到任務(wù)隊列。
腳本已經(jīng)運行完畢,因此調(diào)用棧為空,這意味著該任務(wù)現(xiàn)在是從 Task Queue 中取出放到 Call Stack 上,它調(diào)用了 resolve。
調(diào)用 resolve 將 [[PromiseState]] 設(shè)置為 “fulfilled”,將 [[PromiseResult]] 設(shè)置為 “Done!”,并與 Promise Reaction 處理程序相關(guān)的代碼被添加到 Microtask Queue 中。
resolve 和回調(diào)從調(diào)用棧中彈出。
由于調(diào)用棧為空,事件循環(huán)首先檢查微任務(wù)隊列,那里 then 處理程序的回調(diào)正在等待。
回調(diào)現(xiàn)在被添加到調(diào)用棧,并記錄 result 的值,即 [[PromiseResult]] 的值;字符串 "Done!"。
一旦回調(diào)執(zhí)行完畢并從調(diào)用棧中彈出,程序就完成了!
除了創(chuàng)建一個 Promise Reaction 之外,then 還返回一個 Promise。這意味著我們可以將多個 then 鏈接在一起,例如:
new Promise((resolve) => {
resolve(1);
})
.then(result => result * 2)
.then(result => result * 2)
.then(result => console.log(result));
執(zhí)行這段代碼時,在調(diào)用 Promise 構(gòu)造函數(shù)時會創(chuàng)建一個 Promise 對象。之后,每當引擎遇到 then 時,就會創(chuàng)建一個 Promise Reaction 記錄和一個 Promise Object。
在這兩種情況下,then 的回調(diào)都將接收到的 [[PromiseResult]] 值乘以 2。then 的 [[PromiseResult]] 被設(shè)置為計算的結(jié)果,這個結(jié)果又被下一個 then 的處理程序使用。
最終,結(jié)果被記錄下來。由于我們沒有顯式地返回一個值,所以最后一個 then promise 的 [[PromiseResult]] 是未定義的,這意味著它隱式地返回了未定義的值。
當然,使用數(shù)字并不是最現(xiàn)實的場景。相反,您可能希望逐步改變 promise 的結(jié)果,就像逐步改變圖片的外觀一樣。
例如,您可能希望采取一系列增量的步驟,通過操作(如調(diào)整大小、應(yīng)用濾鏡、添加水印等)來改變圖像的外觀。
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
}
loadImage(src)
.then(image => resizeImage(image))
.then(image => applyGrayscaleFilter(image))
.then(image => addWatermark(image))
這類型的任務(wù)通常涉及異步操作,這使得 promise 成為以非阻塞方式管理這些操作的良好選擇。
結(jié)論
長話短說,Promise 只是具有一些額外功能來改變其內(nèi)部狀態(tài)的對象。
Promises 最酷的地方在于,如果通過 then 或 catch 附加了處理程序,就可以觸發(fā)異步操作。由于處理程序被推送到微任務(wù)隊列,因此可以以非阻塞的方式處理最終結(jié)果。這樣就能更輕松地處理錯誤、將多個操作連鎖在一起,并使代碼更具可讀性和可維護性!
Promise 然是一個基礎(chǔ)概念,對每個 JavaScript 開發(fā)人員來說都很重要。如果您有興趣了解更多,async/await 語法(承諾的語法糖)等其他特性以及 Async Generators(異步生成器)等特性將為異步代碼的使用提供更多方法。
關(guān)于本文
譯者:@飄飄
作者:@Lydia Hallie
原文:https://www.lydiahallie.com/blog/promise-execution
最后
Node 社群
我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學習感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關(guān)的交流、學習、共建。下方加 考拉 好友回復「Node」即可。
“分享、點贊、在看” 支持一下
