如何使用 Promise

承諾是現代 JavaScript 中非同步程式設計的基礎。承諾是一個由非同步函式返回的物件,它表示操作的當前狀態。在承諾返回給呼叫者時,操作通常尚未完成,但承諾物件提供了一些方法來處理操作最終的成功或失敗。

先決條件 對 JavaScript 基礎知識有合理的理解,包括事件處理。
目標 瞭解如何在 JavaScript 中使用承諾。

上一篇文章中,我們討論了使用回撥來實現非同步函式。在這種設計中,您呼叫非同步函式,並傳入您的回撥函式。該函式立即返回,並在操作完成後呼叫您的回撥。

在基於承諾的 API 中,非同步函式啟動操作並返回一個Promise 物件。然後您可以將處理程式附加到此承諾物件,這些處理程式將在操作成功或失敗時執行。

使用 fetch() API

注意:在這篇文章中,我們將透過將程式碼示例從頁面複製到瀏覽器的 JavaScript 控制檯來探索承諾。要進行設定

  1. 開啟一個瀏覽器標籤頁並訪問 https://example.org
  2. 在該標籤頁中,在您的 瀏覽器開發者工具中開啟 JavaScript 控制檯
  3. 當我們顯示一個示例時,將其複製到控制檯中。每次輸入一個新示例時,您都需要重新載入頁面,否則控制檯會抱怨您重新聲明瞭 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 的現代基於承諾的替代品。

將此複製到您的瀏覽器 JavaScript 控制檯中

js
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…");

這裡我們

  1. 呼叫 fetch() API,並將返回值分配給 fetchPromise 變數
  2. 緊隨其後,記錄 fetchPromise 變數。這應該輸出類似於:Promise { <state>: "pending" } 的內容,告訴我們我們有一個 Promise 物件,並且它有一個值為 "pending"state"pending" 狀態意味著 fetch 操作仍在進行。
  3. 將處理程式函式傳遞到承諾的then() 方法中。當(以及如果)fetch 操作成功時,承諾將呼叫我們的處理程式,並傳入一個Response 物件,其中包含伺服器的響應。
  4. 記錄一條我們已經開始請求的訊息。

完整的輸出應該類似於

Promise { <state>: "pending" }
Started request…
Received response: 200

請注意,Started request… 在我們收到響應之前就被記錄下來。與同步函式不同,fetch() 在請求仍在進行時返回,使我們的程式保持響應性。響應顯示了 200(OK)狀態程式碼,這意味著我們的請求成功了。

這可能看起來很像上一篇文章中的示例,我們在其中向 XMLHttpRequest 物件添加了事件處理程式。與之不同的是,我們正在將處理程式傳遞到返回的承諾的 then() 方法中。

連結 Promise

使用 fetch() API,一旦您獲得了 Response 物件,您就需要呼叫另一個函式來獲取響應資料。在這種情況下,我們想將響應資料作為 JSON 獲取,因此我們將呼叫 Response 物件的 json() 方法。事實證明,json() 也是非同步的。所以這是一個我們必須連續呼叫兩個非同步函式的情況。

試試這個

js
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);
  });
});

在這個示例中,與之前一樣,我們向 fetch() 返回的承諾添加了一個 then() 處理程式。但這一次,我們的處理程式呼叫 response.json(),然後將一個新的 then() 處理程式傳遞到 response.json() 返回的承諾中。

這應該記錄“baked beans”(在“products.json”中列出的第一個產品的名稱)。

但是等等!還記得上一篇文章中,我們說過在另一個回撥中呼叫回撥會導致程式碼越來越巢狀嗎?我們還說過這種“回撥地獄”使我們的程式碼難以理解?這難道不是一樣,只是用 then() 呼叫來代替嗎?

當然,是這樣的。但承諾的優雅之處在於then() 本身會返回一個承諾,該承諾將使用傳遞給它的函式的結果完成。這意味著我們可以(而且當然應該)像這樣重寫上面的程式碼

js
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() 返回的承諾,並在該返回值上呼叫第二個 then()。這被稱為承諾鏈,這意味著當我們需要進行連續的非同步函式呼叫時,我們可以避免不斷增加的縮排級別。

在我們繼續下一步之前,還有一件事需要新增。在嘗試讀取之前,我們需要檢查伺服器是否接受並能夠處理請求。我們將透過檢查響應中的狀態程式碼,並在它不是“OK”時丟擲錯誤來做到這一點。

js
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(),那麼它將在任何非同步函式呼叫失敗時被呼叫。因此,您可以將操作實現為幾個連續的非同步函式呼叫,並在一個地方處理所有錯誤。

嘗試我們的 fetch() 程式碼的這個版本。我們已經使用 catch() 添加了一個錯誤處理程式,並且還修改了 URL,以便請求會失敗。

js
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 術語

承諾帶來了一些非常具體的術語,值得弄清楚。

首先,承諾可以處於三種狀態之一

  • pending:承諾已建立,並且與其關聯的非同步函式尚未成功或失敗。這是您在從 fetch() 呼叫返回承諾時承諾所處的狀態,並且請求仍在進行。
  • fulfilled:非同步函式已成功。當承諾完成時,將呼叫其 then() 處理程式。
  • rejected:非同步函式已失敗。當承諾被拒絕時,將呼叫其 catch() 處理程式。

請注意,這裡“成功”或“失敗”的含義取決於所涉及的 API。例如,fetch() 會拒絕返回的承諾,如果(除其他原因外)網路錯誤阻止請求傳送,但如果伺服器傳送了響應,即使響應是錯誤(例如 404 Not Found),也會完成該承諾。

有時,我們使用術語settled來涵蓋fulfilledrejected

如果承諾已結算,或者已“鎖定”以遵循另一個承諾的狀態,則該承諾已resolved

文章 讓我們談談如何談論承諾 對此術語的細節進行了很好的解釋。

組合多個 Promise

當您的操作由多個非同步函式組成,並且您需要每個函式都完成才能開始下一個函式時,您需要承諾鏈。但是,您可能需要以其他方式組合非同步函式呼叫,並且 Promise API 為它們提供了一些輔助工具。

有時,您需要所有承諾都完成,但它們彼此之間沒有依賴關係。在這種情況下,一起啟動它們要比通知它們都完成更有效。您需要的是 Promise.all() 方法。它接受一個承諾陣列並返回一個單一承諾。

Promise.all() 返回的承諾是

  • 當且僅當陣列中的所有承諾都完成時才完成。在這種情況下,then() 處理程式將使用所有響應的陣列呼叫,這些響應的順序與承諾傳遞到 all() 中的順序相同。
  • 當且僅當陣列中的任何一個承諾被拒絕時才拒絕。在這種情況下,catch() 處理程式將使用拒絕承諾丟擲的錯誤呼叫。

例如

js
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,所有請求都應該完成,儘管對於第二個請求,伺服器將返回 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 來執行相同的程式碼,例如

js
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.any()。這與 Promise.all() 相似,不同之處在於,它在承諾陣列中的任何一個完成時完成,或者在它們全部被拒絕時拒絕

js
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 參考文件。

async 和 await

async 關鍵字為您提供了一種更簡單的方法來使用基於承諾的非同步程式碼。在函式開頭新增 async 將使其成為非同步函式

js
async function myFunction() {
  // This is an async function
}

在非同步函式中,您可以在返回承諾的函式呼叫之前使用 await 關鍵字。這使得程式碼在該點等待,直到承諾結算,此時承諾的完成值將被視為返回值,或者拒絕值將被丟擲。

這使您能夠編寫使用非同步函式但看起來像同步程式碼的程式碼。例如,我們可以使用它來重寫我們的 fetch 示例

js
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 塊進行錯誤處理,就像程式碼是同步的一樣。

請注意,非同步函式總是返回一個承諾,因此您不能執行類似的操作

js
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

相反,您需要執行類似的操作

js
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函式內部捕獲錯誤的情況,導致dataundefined。將錯誤處理作為 promise 鏈的最後一步。

另外,請注意,除非您的程式碼位於JavaScript 模組中,否則您只能在async函式內使用await。這意味著您不能在普通指令碼中這樣做。

js
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;
}

您可能會在您可能使用 promise 鏈的地方經常使用async函式,它們使 promise 的使用更加直觀。

請記住,就像 promise 鏈一樣,await強制非同步操作按順序完成。如果下一個操作的結果依賴於上一個操作的結果,這是必要的,但如果不是這種情況,那麼像Promise.all()這樣的東西將更高效。

結論

promise 是現代 JavaScript 中非同步程式設計的基礎。它們使表達和推理非同步操作序列變得更容易,而無需深度巢狀的回撥,並且它們支援與同步try...catch語句類似的錯誤處理方式。

asyncawait關鍵字使從一系列連續的非同步函式呼叫構建操作變得更容易,避免了建立顯式 promise 鏈的需要,並允許您編寫看起來像同步程式碼的程式碼。

promise 在所有現代瀏覽器的最新版本中都有效;promise 支援唯一有問題的地方是 Opera Mini 和 IE11 及更早版本。

我們在這篇文章中沒有涉及 promise 的所有功能,只是最有趣和最有用的功能。當您開始瞭解有關 promise 的更多資訊時,您會遇到更多功能和技術。

許多現代 Web API 都是基於 promise 的,包括WebRTCWeb Audio APIMedia Capture and Streams API等等。

另請參閱