如何使用 Promise
Promise 是現代 JavaScript 中非同步程式設計的基礎。Promise 是一個由非同步函式返回的物件,它代表了操作的當前狀態。當 Promise 返回給呼叫者時,操作通常還沒有完成,但 Promise 物件提供了處理操作最終成功或失敗的方法。
| 預備知識 | 對本模組前面課程中介紹的 JavaScript 基礎知識和非同步概念有紮實的理解。 |
|---|---|
| 學習成果 |
|
在上一篇文章中,我們討論了使用回撥函式實現非同步函式。透過這種設計,你呼叫非同步函式,並傳入你的回撥函式。該函式會立即返回,並在操作完成時呼叫你的回撥函式。
使用基於 Promise 的 API 時,非同步函式會啟動操作並返回一個 Promise 物件。然後,你可以將處理程式附加到此 Promise 物件,當操作成功或失敗時,這些處理程式將被執行。
使用 fetch() API
注意: 在本文中,我們將透過將頁面中的程式碼示例複製到瀏覽器的 JavaScript 控制檯中來探索 Promise。要進行此設定:
- 開啟一個瀏覽器標籤頁並訪問 https://example.org
- 在該標籤頁中,在你的瀏覽器開發者工具中開啟 JavaScript 控制檯
- 當我們展示一個示例時,將其複製到控制檯中。每次輸入新示例時,你都必須重新載入頁面,否則控制檯會抱怨你重新聲明瞭
fetchPromise。
在此示例中,我們將從 https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json 下載 JSON 檔案,並記錄一些關於它的資訊。
為此,我們將向伺服器發出 HTTP 請求。在 HTTP 請求中,我們向遠端伺服器傳送一個請求訊息,它會向我們發回一個響應。在這種情況下,我們將傳送一個請求以從伺服器獲取一個 JSON 檔案。還記得上一篇文章中,我們使用 XMLHttpRequest API 發出 HTTP 請求嗎?嗯,在本文中,我們將使用 fetch() API,它是 XMLHttpRequest 的現代的、基於 Promise 的替代品。
將此複製到瀏覽器的 JavaScript 控制檯中
const fetchPromise = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
console.log(fetchPromise);
fetchPromise.then((response) => {
console.log(`Received response: ${response.status}`);
});
console.log("Started request…");
這裡我們正在
- 呼叫
fetch()API,並將返回值賦給fetchPromise變數 - 緊接著,記錄
fetchPromise變數。這應該輸出類似Promise { <state>: "pending" }的內容,告訴我們我們有一個Promise物件,並且它有一個state,其值為"pending"。"pending"狀態表示 fetch 操作仍在進行中。 - 將一個處理函式傳遞給 Promise 的
then()方法。當(如果)fetch 操作成功時,Promise 將呼叫我們的處理程式,傳入一個Response物件,其中包含伺服器的響應。 - 記錄一條訊息,表示我們已開始請求。
完整的輸出應該類似於
Promise { <state>: "pending" }
Started request…
Received response: 200
請注意,Started request… 在我們收到響應之前被記錄。與同步函式不同,fetch() 在請求仍在進行時返回,使我們的程式能夠保持響應。響應顯示 200 (OK) 狀態碼,這意味著我們的請求成功了。
這可能看起來很像上一篇文章中的示例,我們在 XMLHttpRequest 物件上添加了事件處理程式。不同的是,我們正在將處理程式傳遞到返回的 Promise 的 then() 方法中。
鏈式呼叫 Promise
使用 fetch() API,一旦你獲得了一個 Response 物件,你需要呼叫另一個函式來獲取響應資料。在這種情況下,我們希望將響應資料獲取為 JSON,因此我們會呼叫 Response 物件的 json() 方法。結果 json() 也是非同步的。所以這是一個我們必須呼叫兩個連續非同步函式的情況。
嘗試這個
const fetchPromise = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
fetchPromise.then((response) => {
const jsonPromise = response.json();
jsonPromise.then((data) => {
console.log(data[0].name);
});
});
在此示例中,和以前一樣,我們將一個 then() 處理程式新增到 fetch() 返回的 Promise 中。但這次,我們的處理程式呼叫 response.json(),然後將一個新的 then() 處理程式傳遞給 response.json() 返回的 Promise。
這應該會記錄 "baked beans"("products.json" 中列出的第一個產品的名稱)。
但是等等!還記得上一篇文章中,我們說過透過在一個回撥函式內呼叫另一個回撥函式,我們會得到越來越深的巢狀程式碼層次嗎?我們還說過這種“回撥地獄”使我們的程式碼難以理解嗎?這不就是一樣的情況嗎,只是使用了 then() 呼叫?
當然是。但是 Promise 的優雅之處在於 then() 本身返回一個新的 Promise,該 Promise 使用回撥函式的返回值(如果函式成功執行)來解決。這意味著我們可以(也應該)將上述程式碼改寫成這樣
const fetchPromise = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
fetchPromise
.then((response) => response.json())
.then((data) => {
console.log(data[0].name);
});
我們可以不是在第一個 then() 的處理程式內部呼叫第二個 then(),而是返回 json() 返回的 Promise,並在該返回值上呼叫第二個 then()。這被稱為 Promise 鏈式呼叫,意味著當我們需要進行連續的非同步函式呼叫時,我們可以避免不斷增加的縮排級別。
在進入下一步之前,還有最後一步要補充。我們需要檢查伺服器是否接受並能夠處理請求,然後才能嘗試讀取它。我們將透過檢查響應中的狀態碼來做到這一點,如果狀態碼不是“OK”,則丟擲錯誤。
const fetchPromise = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
fetchPromise
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
})
.then((data) => {
console.log(data[0].name);
});
捕獲錯誤
這引出了最後一點:我們如何處理錯誤?fetch() API 可以因為多種原因丟擲錯誤(例如,因為沒有網路連線或 URL 格式不正確),如果伺服器返回錯誤,我們也會自己丟擲錯誤。
在上一篇文章中,我們看到錯誤處理在巢狀回撥中會變得非常困難,這使得我們必須在每個巢狀級別處理錯誤。
為了支援錯誤處理,Promise 物件提供了一個 catch() 方法。這與 then() 非常相似:你呼叫它並傳入一個處理函式。然而,當非同步操作成功時,傳遞給 then() 的處理程式會被呼叫;而當非同步操作失敗時,傳遞給 catch() 的處理程式會被呼叫。
如果你將 catch() 新增到 Promise 鏈的末尾,那麼當任何非同步函式呼叫失敗時,它都會被呼叫。因此,你可以將一個操作實現為幾個連續的非同步函式呼叫,並擁有一個處理所有錯誤的單一位置。
嘗試我們 fetch() 程式碼的這個版本。我們已經使用 catch() 添加了一個錯誤處理程式,並且還修改了 URL,以便請求將失敗。
const fetchPromise = fetch(
"bad-scheme://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
fetchPromise
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
})
.then((data) => {
console.log(data[0].name);
})
.catch((error) => {
console.error(`Could not get products: ${error}`);
});
嘗試執行此版本:您應該會看到由我們的 catch() 處理程式記錄的錯誤。
Promise 術語
Promise 附帶了一些非常具體的術語,值得弄清楚。
首先,一個 Promise 可以處於三種狀態之一:
- pending(待定):初始狀態。操作尚未完成(成功或失敗)。
- fulfilled(已成功):操作成功。此時會呼叫 Promise 的
.then()處理程式。 - rejected(已拒絕):操作失敗。此時會呼叫 Promise 的
.catch()處理程式。
請注意,這裡“成功”或“失敗”的含義取決於所討論的 API。例如,如果(除其他原因外)網路錯誤阻止了請求的傳送,fetch() 會拒絕返回的 Promise,但即使響應是 404 Not Found 這樣的錯誤,如果伺服器傳送了響應,它也會兌現 Promise。
我們還使用其他一些術語來描述 Promise 的狀態:
- completed(已完成):Promise 不再處於待定狀態;它已成功或已拒絕。
- resolved(已解決):Promise 已完成,或者它已“鎖定”以遵循另一個 Promise 的狀態。這是一個更高階的概念,當一個 Promise 依賴於另一個 Promise 時相關。
我們來談談如何談論 Promise 一文對這些術語的細節做了很好的解釋。
組合多個 Promise
當你的操作由多個非同步函式組成,並且你需要每個函式在開始下一個函式之前完成時,你就需要 Promise 鏈。但是,還有其他方法可以組合非同步函式呼叫,Promise API 為它們提供了一些幫助。
有時,你需要所有 Promise 都被兌現,但它們之間沒有依賴關係。在這種情況下,將它們全部同時啟動,然後在它們全部兌現時得到通知會更有效率。Promise.all() 方法就是你所需要的。它接受一個 Promise 陣列並返回一個單一的 Promise。
Promise.all() 返回的 Promise 是
- 當陣列中的所有 Promise 都被兌現時,才會被兌現。在這種情況下,
then()處理程式會被呼叫,並帶有一個包含所有響應的陣列,其順序與 Promise 傳遞給all()的順序相同。 - 當陣列中的任何 Promise 被拒絕時,它都會被拒絕。在這種情況下,
catch()處理程式會被呼叫,並帶有一個由被拒絕的 Promise 丟擲的錯誤。
例如
const fetchPromise1 = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
const fetchPromise2 = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found",
);
const fetchPromise3 = fetch(
"https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json",
);
Promise.all([fetchPromise1, fetchPromise2, fetchPromise3])
.then((responses) => {
for (const response of responses) {
console.log(`${response.url}: ${response.status}`);
}
})
.catch((error) => {
console.error(`Failed to fetch: ${error}`);
});
在這裡,我們向三個不同的 URL 發出了三個 fetch() 請求。如果它們都成功,我們將記錄每個請求的響應狀態。如果其中任何一個失敗,那麼我們將記錄失敗。
使用我們提供的 URL,所有請求都應該得到 fulfilled 狀態,儘管對於第二個請求,伺服器將返回 404(未找到)而不是 200(OK),因為請求的檔案不存在。所以輸出應該如下:
https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json: 200 https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found: 404 https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json: 200
如果我們用一個格式錯誤的 URL 嘗試相同的程式碼,例如:
const fetchPromise1 = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
const fetchPromise2 = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found",
);
const fetchPromise3 = fetch(
"bad-scheme://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json",
);
Promise.all([fetchPromise1, fetchPromise2, fetchPromise3])
.then((responses) => {
for (const response of responses) {
console.log(`${response.url}: ${response.status}`);
}
})
.catch((error) => {
console.error(`Failed to fetch: ${error}`);
});
那麼我們可以預期 catch() 處理程式將執行,我們應該會看到類似以下內容:
Failed to fetch: TypeError: Failed to fetch
有時,你可能需要一組 Promise 中的任何一個被兌現,而不在乎是哪一個。在這種情況下,你需要 Promise.any()。這類似於 Promise.all(),不同之處在於它會在 Promise 陣列中的任何一個 Promise 被兌現時立即被兌現,或者在所有 Promise 都被拒絕時被拒絕。
const fetchPromise1 = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
const fetchPromise2 = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found",
);
const fetchPromise3 = fetch(
"https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json",
);
Promise.any([fetchPromise1, fetchPromise2, fetchPromise3])
.then((response) => {
console.log(`${response.url}: ${response.status}`);
})
.catch((error) => {
console.error(`Failed to fetch: ${error}`);
});
請注意,在這種情況下,我們無法預測哪個 fetch 請求會首先完成。
這些只是用於組合多個 Promise 的額外 Promise 函式中的兩個。要了解其餘的,請參閱 Promise 參考文件。
async 和 await
async 關鍵字提供了一種更簡單的方式來處理非同步的、基於 Promise 的程式碼。在函式開頭新增 async 會使其成為一個非同步函式:
async function myFunction() {
// This is an async function
}
在非同步函式內部,你可以在呼叫返回 Promise 的函式之前使用 await 關鍵字。這會使程式碼在該點等待,直到 Promise 解決,此時 Promise 的已解決值被視為返回值,或者已拒絕值被丟擲。
這使你能夠編寫使用非同步函式但看起來像同步程式碼的程式碼。例如,我們可以用它來重寫我們的 fetch 示例:
async function fetchProducts() {
try {
// after this line, our function will wait for the `fetch()` call to be settled
// the `fetch()` call will either return a Response or throw an error
const response = await fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
// after this line, our function will wait for the `response.json()` call to be settled
// the `response.json()` call will either return the parsed JSON object or throw an error
const data = await response.json();
console.log(data[0].name);
} catch (error) {
console.error(`Could not get products: ${error}`);
}
}
fetchProducts();
這裡,我們呼叫了 await fetch(),但我們的呼叫者得到的不是一個 Promise,而是一個完全完成的 Response 物件,就像 fetch() 是一個同步函式一樣!
我們甚至可以使用 try...catch 塊進行錯誤處理,就像程式碼是同步的一樣。
但請注意,async 函式總是返回一個 Promise,所以你不能這樣做:
async function fetchProducts() {
try {
const response = await fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error(`Could not get products: ${error}`);
}
}
const promise = fetchProducts();
console.log(promise[0].name); // "promise" is a Promise object, so this will not work
相反,你需要做類似以下的事情:
async function fetchProducts() {
const response = await fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
return data;
}
const promise = fetchProducts();
promise
.then((data) => {
console.log(data[0].name);
})
.catch((error) => {
console.error(`Could not get products: ${error}`);
});
這裡,我們將 try...catch 移回了返回的 Promise 的 catch 處理程式。這意味著我們的 then 處理程式不必處理 fetchProducts 函式內部捕獲到錯誤導致 data 為 undefined 的情況。將錯誤處理作為 Promise 鏈的最後一步。
另外請注意,你只能在 async 函式內部使用 await,除非你的程式碼在 JavaScript 模組中。這意味著你不能在普通指令碼中這樣做:
try {
// using await outside an async function is only allowed in a module
const response = await fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
console.log(data[0].name);
} catch (error) {
console.error(`Could not get products: ${error}`);
throw error;
}
你可能會大量使用 async 函式,而這原本可能會使用 Promise 鏈,並且它們使得使用 Promise 更加直觀。
請記住,就像 Promise 鏈一樣,await 強制非同步操作按系列完成。如果下一個操作的結果依賴於上一個操作的結果,這是必要的,但如果不是這種情況,那麼像 Promise.all() 這樣的方法會更高效。
總結
Promise 是現代 JavaScript 中非同步程式設計的基礎。它們使表達和推理非同步操作序列變得更容易,而無需深度巢狀的回撥,並且它們支援一種類似於同步 try...catch 語句的錯誤處理方式。
async 和 await 關鍵字使從一系列連續的非同步函式呼叫構建操作變得更容易,避免了建立顯式 Promise 鏈的需要,並允許你編寫看起來就像同步程式碼的程式碼。
Promise 在所有現代瀏覽器的最新版本中都有效;唯一可能出現 Promise 支援問題的地方是 Opera Mini 和 IE11 及更早版本。
本文並未涉及 Promise 的所有功能,只介紹了最有趣和最有用的部分。隨著你對 Promise 瞭解的深入,你還會遇到更多功能和技術。
許多現代 Web API 都是基於 Promise 的,包括 WebRTC、Web Audio API、Media Capture and Streams API 等等。
另見
Promise()- 使用 Promise
- Nolan Lawson 的我們對 Promise 有個問題
- 我們來談談如何談論 Promise