如何使用 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等等。

另请参阅