如何使用 promise

Promise 是现代 JavaScript 中异步编程的基础。Promise 是异步函数返回的对象,它表示操作的当前状态。当 Promise 返回给调用者时,操作通常尚未完成,但 Promise 对象提供了处理操作最终成功或失败的方法。

¥Promises are the foundation of asynchronous programming in modern JavaScript. A promise is an object returned by an asynchronous function, which represents the current state of the operation. At the time the promise is returned to the caller, the operation often isn't finished, but the promise object provides methods to handle the eventual success or failure of the operation.

先决条件: 对 JavaScript 基础知识有一定的了解,包括事件处理。
目标: 了解如何在 JavaScript 中使用 Promise。

上一篇文章 中,我们谈到了使用回调来实现异步功能。通过这种设计,你可以调用异步函数,并传入回调函数。该函数立即返回并在操作完成后调用你的回调。

¥In the previous article, we talked about the use of callbacks to implement asynchronous functions. With that design, you call the asynchronous function, passing in your callback function. The function returns immediately and calls your callback when the operation is finished.

使用基于 Promise 的 API,异步函数启动操作并返回 Promise 对象。然后,你可以将处理程序附加到此 Promise 对象,这些处理程序将在操作成功或失败时执行。

¥With a promise-based API, the asynchronous function starts the operation and returns a Promise object. You can then attach handlers to this promise object, and these handlers will be executed when the operation has succeeded or failed.

使用 fetch() API

¥Using the fetch() API

注意:在本文中,我们将通过将代码示例从页面复制到浏览器的 JavaScript 控制台来探索 Promise。要进行此设置:

¥Note: In this article, we will explore promises by copying code samples from the page into your browser's JavaScript console. To set this up:

  1. 打开浏览器选项卡并访问 https://example.org
  2. 在该选项卡中,打开 浏览器的开发者工具 中的 JavaScript 控制台
  3. 当我们显示示例时,将其复制到控制台中。每次输入新示例时,你都必须重新加载页面,否则控制台会抱怨你已重新声明 fetchPromise

在此示例中,我们将从 https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json 下载 JSON 文件,并记录有关它的一些信息。

¥In this example, we'll download the JSON file from https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json, and log some information about it.

为此,我们将向服务器发出 HTTP 请求。在 HTTP 请求中,我们向远程服务器发送请求消息,然后远程服务器向我们发回响应。在本例中,我们将发送请求以从服务器获取 JSON 文件。还记得上一篇文章中我们使用 XMLHttpRequest API 发出 HTTP 请求吗?好吧,在本文中,我们将使用 fetch() API,它是 XMLHttpRequest 的现代、基于承诺的替代品。

¥To do this, we'll make an HTTP request to the server. In an HTTP request, we send a request message to a remote server, and it sends us back a response. In this case, we'll send a request to get a JSON file from the server. Remember in the last article, where we made HTTP requests using the XMLHttpRequest API? Well, in this article, we'll use the fetch() API, which is the modern, promise-based replacement for XMLHttpRequest.

将其复制到浏览器的 JavaScript 控制台中:

¥Copy this into your browser's JavaScript console:

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

我们到了:

¥Here we are:

  1. 调用 fetch() API,并将返回值赋给 fetchPromise 变量
  2. 之后立即记录 fetchPromise 变量。这应该输出类似:Promise { <state>: "pending" },告诉我们有一个 Promise 对象,它有一个 state,其值为 "pending""pending" 状态表示取操作仍在进行。
  3. 将处理函数传递到 Promise 的 then() 方法中。当(并且如果)获取操作成功时,promise 将调用我们的处理程序,传入一个 Response 对象,其中包含服务器的响应。
  4. 记录一条消息,表明我们已开始请求。

完整的输出应该类似于:

¥The complete output should be something like:

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

请注意,Started request… 在我们收到响应之前已被记录。与同步函数不同,fetch() 在请求仍在进行时返回,使我们的程序能够保持响应。响应显示 200 (OK) 状态码,意味着我们的请求成功。

¥Note that Started request… is logged before we receive the response. Unlike a synchronous function, fetch() returns while the request is still going on, enabling our program to stay responsive. The response shows the 200 (OK) status code, meaning that our request succeeded.

这可能看起来很像上一篇文章中的示例,其中我们向 XMLHttpRequest 对象添加了事件处理程序。相反,我们将一个处理程序传递到返回的 Promise 的 then() 方法中。

¥This probably seems a lot like the example in the last article, where we added event handlers to the XMLHttpRequest object. Instead of that, we're passing a handler into the then() method of the returned promise.

连锁承诺

¥Chaining promises

使用 fetch() API,一旦获得 Response 对象,就需要调用另一个函数来获取响应数据。在本例中,我们希望获取 JSON 格式的响应数据,因此我们将调用 Response 对象的 json() 方法。原来 json() 也是异步的。因此,在这种情况下,我们必须调用两个连续的异步函数。

¥With the fetch() API, once you get a Response object, you need to call another function to get the response data. In this case, we want to get the response data as JSON, so we would call the json() method of the Response object. It turns out that json() is also asynchronous. So this is a case where we have to call two successive asynchronous functions.

尝试这个:

¥Try this:

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

在这个例子中,和之前一样,我们将 then() 处理程序添加到 fetch() 返回的 Promise 中。但这一次,我们的处理程序调用 response.json(),然后将新的 then() 处理程序传递到 response.json() 返回的 Promise 中。

¥In this example, as before, we add a then() handler to the promise returned by fetch(). But this time, our handler calls response.json(), and then passes a new then() handler into the promise returned by response.json().

这应该记录 "烤豆"("products.json" 中列出的第一个产品的名称)。

¥This should log "baked beans" (the name of the first product listed in "products.json").

可是等等!还记得上一篇文章中,我们说过通过在另一个回调中调用一个回调,我们可以连续获得更多嵌套的代码级别吗?我们说这个 "回调地狱" 让我们的代码难以理解?这不是一样的吗,只是 then() 调用而已?

¥But wait! Remember the last article, where we said that by calling a callback inside another callback, we got successively more nested levels of code? And we said that this "callback hell" made our code hard to understand? Isn't this just the same, only with then() calls?

当然是这样。但 Promise 的优雅之处在于 then() 本身返回一个 Promise,该 Promise 将用传递给它的函数的结果来完成。这意味着我们可以(当然应该)像这样重写上面的代码:

¥It is, of course. But the elegant feature of promises is that then() itself returns a promise, which will be completed with the result of the function passed to it. This means that we can (and certainly should) rewrite the above code like this:

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

我们可以返回 json() 返回的 Promise,然后在该返回值上调用第二个 then(),而不是在第一个 then() 的处理程序中调用第二个 then()。这称为承诺链,意味着当我们需要进行连续的异步函数调用时,我们可以避免不断增加的缩进级别。

¥Instead of calling the second then() inside the handler for the first then(), we can return the promise returned by json(), and call the second then() on that return value. This is called promise chaining and means we can avoid ever-increasing levels of indentation when we need to make consecutive asynchronous function calls.

在我们继续下一步之前,还需要添加一个内容。在尝试读取请求之前,我们需要检查服务器是否接受并能够处理请求。我们将通过检查响应中的状态代码并在不是 "OK" 时抛出错误来完成此操作:

¥Before we move on to the next step, there's one more piece to add. We need to check that the server accepted and was able to handle the request, before we try to read it. We'll do this by checking the status code in the response and throwing an error if it wasn't "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);
  });

捕获错误

¥Catching errors

这给我们带来了最后一块:我们如何处理错误?fetch() API 可能会因多种原因引发错误(例如,因为没有网络连接或 URL 在某种程度上存在格式错误),如果服务器返回错误,我们自己也会引发错误。

¥This brings us to the last piece: how do we handle errors? The fetch() API can throw an error for many reasons (for example, because there was no network connectivity or the URL was malformed in some way) and we are throwing an error ourselves if the server returned an error.

在上一篇文章中,我们看到嵌套回调的错误处理会变得非常困难,这使得我们在每个嵌套级别处理错误。

¥In the last article, we saw that error handling can get very difficult with nested callbacks, making us handle errors at every nesting level.

为了支持错误处理,Promise 对象提供了 catch() 方法。这很像 then():你调用它并传入一个处理函数。但是,传递给 then() 的处理程序在异步操作成功时被调用,而传递给 catch() 的处理程序在异步操作失败时被调用。

¥To support error handling, Promise objects provide a catch() method. This is a lot like then(): you call it and pass in a handler function. However, while the handler passed to then() is called when the asynchronous operation succeeds, the handler passed to catch() is called when the asynchronous operation fails.

如果将 catch() 添加到 Promise 链的末尾,那么当任何异步函数调用失败时都会调用它。因此,你可以将一个操作实现为多个连续的异步函数调用,并有一个位置来处理所有错误。

¥If you add catch() to the end of a promise chain, then it will be called when any of the asynchronous function calls fail. So you can implement an operation as several consecutive asynchronous function calls, and have a single place to handle all errors.

尝试我们的 fetch() 代码的这个版本。我们使用 catch() 添加了一个错误处理程序,并且还修改了 URL,因此请求将失败。

¥Try this version of our fetch() code. We've added an error handler using catch(), and also modified the URL so the request will fail.

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() 处理程序记录的错误。

¥Try running this version: you should see the error logged by our catch() handler.

承诺术语

¥Promise terminology

Promise 附带了一些非常具体的术语,值得弄清楚。

¥Promises come with some quite specific terminology that it's worth getting clear about.

首先,承诺可以处于以下三种状态之一:

¥First, a promise can be in one of three states:

  • 待办的:Promise 已创建,并且与其关联的异步函数尚未成功或失败。这是你的 Promise 在从对 fetch() 的调用返回时所处的状态,并且请求仍在发出。
  • 已满足:异步函数成功。当承诺履行时,将调用其 then() 处理程序。
  • 拒绝了:异步函数失败。当 Promise 被拒绝时,将调用其 catch() 处理程序。

请注意,此处 "succeeded" 或 "failed" 的含义取决于相关 API。例如,如果(以及其他原因)网络错误阻止了请求的发送,则 fetch() 拒绝返回的承诺,但如果服务器发送响应,则履行承诺,即使响应是像 404 未找到 这样的错误。

¥Note that what "succeeded" or "failed" means here is up to the API in question. For example, fetch() rejects the returned promise if (among other reasons) a network error prevented the request being sent, but fulfills the promise if the server sent a response, even if the response was an error like 404 Not Found.

有时,我们使用“已解决”一词来涵盖已完成和已拒绝。

¥Sometimes, we use the term settled to cover both fulfilled and rejected.

如果一个 Promise 已解决,或者如果它已经 "锁定" 遵循另一个 Promise 的状态,则该 Promise 已解决。

¥A promise is resolved if it is settled, or if it has been "locked in" to follow the state of another promise.

我们来谈谈如何谈论承诺 篇文章对该术语的细节进行了很好的解释。

¥The article Let's talk about how to talk about promises gives a great explanation of the details of this terminology.

结合多个 Promise

¥Combining multiple promises

当你的操作由多个异步函数组成,并且你需要在开始下一个函数之前完成每个函数时,你就需要 Promise 链。但是你可能需要通过其他方式组合异步函数调用,Promise API 为它们提供了一些辅助程序。

¥The promise chain is what you need when your operation consists of several asynchronous functions, and you need each one to complete before starting the next one. But there are other ways you might need to combine asynchronous function calls, and the Promise API provides some helpers for them.

有时,你需要兑现所有的承诺,但它们并不相互依赖。在这种情况下,将它们全部一起启动,然后在它们全部完成时收到通知会更有效。Promise.all() 方法正是你所需要的。它接受一系列承诺并返回一个承诺。

¥Sometimes, you need all the promises to be fulfilled, but they don't depend on each other. In a case like that, it's much more efficient to start them all off together, then be notified when they have all fulfilled. The Promise.all() method is what you need here. It takes an array of promises and returns a single promise.

Promise.all() 返回的 promise 是:

¥The promise returned by Promise.all() is:

  • 当数组中的所有承诺都得到满足时,就完成了。在这种情况下,使用所有响应的数组调用 then() 处理程序,其顺序与将承诺传递到 all() 的顺序相同。
  • 当数组中的任何一个 Promise 被拒绝时,就会被拒绝。在这种情况下,将调用 catch() 处理程序,并显示被拒绝的 Promise 引发的错误。

例如:

¥For example:

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() 请求。如果全部成功,我们将记录每一个的响应状态。如果其中任何一个失败,那么我们就会记录失败。

¥Here, we're making three fetch() requests to three different URLs. If they all succeed, we will log the response status of each one. If any of them fail, then we're logging the failure.

使用我们提供的 URL,所有请求都应该得到满足,但对于第二个请求,服务器将返回 404(未找到)而不是 200(正常),因为请求的文件不存在。所以输出应该是:

¥With the URLs we've provided, all the requests should be fulfilled, although for the second, the server will return 404 (Not Found) instead of 200 (OK) because the requested file does not exist. So the output should be:

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 尝试相同的代码,如下所示:

¥If we try the same code with a badly formed URL, like this:

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() 处理程序运行,并且我们应该看到类似以下内容:

¥Then we can expect the catch() handler to run, and we should see something like:

Failed to fetch: TypeError: Failed to fetch

有时,你可能需要履行一组承诺中的任何一个,而不关心是哪一个。在这种情况下,你需要 Promise.any()。这与 Promise.all() 类似,只不过一旦满足任何一个 Promise 数组,它就会被满足,或者如果所有 Promise 都被拒绝,它就会被拒绝:

¥Sometimes, you might need any one of a set of promises to be fulfilled, and don't care which one. In that case, you want Promise.any(). This is like Promise.all(), except that it is fulfilled as soon as any of the array of promises is fulfilled, or rejected if all of them are rejected:

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

请注意,在这种情况下,我们无法预测哪个提取请求将首先完成。

¥Note that in this case we can't predict which fetch request will complete first.

这些只是用于组合多个 Promise 的额外 Promise 函数中的两个。要了解其余内容,请参阅 Promise 参考文档。

¥These are just two of the extra Promise functions for combining multiple promises. To learn about the rest, see the Promise reference documentation.

异步和等待

¥async and await

async 关键字为你提供了一种更简单的方法来使用基于异步 Promise 的代码。在函数开头添加 async 使其成为异步函数:

¥The async keyword gives you a simpler way to work with asynchronous promise-based code. Adding async at the start of a function makes it an async function:

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

在异步函数内,你可以在调用返回 Promise 的函数之前使用 await 关键字。这使得代码在此时等待,直到解决了 Promise,此时 Promise 的已实现值将被视为返回值,或者抛出被拒绝的值。

¥Inside an async function, you can use the await keyword before a call to a function that returns a promise. This makes the code wait at that point until the promise is settled, at which point the fulfilled value of the promise is treated as a return value, or the rejected value is thrown.

这使你能够编写使用异步函数但看起来像同步代码的代码。例如,我们可以使用它来重写我们的 fetch 示例:

¥This enables you to write code that uses asynchronous functions but looks like synchronous code. For example, we could use it to rewrite our fetch example:

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(),我们的调用者返回的是一个完全完整的 Response 对象,而不是获取 Promise,就像 fetch() 是一个同步函数一样!

¥Here, we are calling await fetch(), and instead of getting a Promise, our caller gets back a fully complete Response object, just as if fetch() were a synchronous function!

我们甚至可以使用 try...catch 块进行错误处理,就像代码是同步的一样。

¥We can even use a try...catch block for error handling, exactly as we would if the code were synchronous.

请注意,虽然异步函数总是返回一个承诺,所以你不能做类似的事情:

¥Note though that async functions always return a promise, so you can't do something like:

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

相反,你需要执行以下操作:

¥Instead, you'd need to do something like:

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();
promise.then((data) => console.log(data[0].name));

另请注意,你只能在 async 函数中使用 await,除非你的代码位于 JavaScript 模块 中。这意味着你无法在普通脚本中执行此操作:

¥Also, note that you can only use await inside an async function, unless your code is in a JavaScript module. That means you can't do this in a normal script:

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

你可能会经常使用 async 函数,否则你可能会使用 Promise 链,并且它们使 Promise 的使用更加直观。

¥You'll probably use async functions a lot where you might otherwise use promise chains, and they make working with promises much more intuitive.

请记住,就像承诺链一样,await 强制异步操作串行完成。如果下一个操作的结果取决于上一个操作的结果,则这是必要的,但如果不是这种情况,那么像 Promise.all() 这样的操作会性能更高。

¥Keep in mind that just like a promise chain, await forces asynchronous operations to be completed in series. This is necessary if the result of the next operation depends on the result of the last one, but if that's not the case then something like Promise.all() will be more performant.

结论

¥Conclusion

Promise 是现代 JavaScript 中异步编程的基础。它们使异步操作序列的表达和推断变得更加容易,而无需深层嵌套的回调,并且它们支持类似于同步 try...catch 语句的错误处理风格。

¥Promises are the foundation of asynchronous programming in modern JavaScript. They make it easier to express and reason about sequences of asynchronous operations without deeply nested callbacks, and they support a style of error handling that is similar to the synchronous try...catch statement.

asyncawait 关键字使从一系列连续的异步函数调用构建操作变得更容易,避免了创建显式承诺链的需要,并允许你编写看起来像同步代码的代码。

¥The async and await keywords make it easier to build an operation from a series of consecutive asynchronous function calls, avoiding the need to create explicit promise chains, and allowing you to write code that looks just like synchronous code.

Promise 适用于所有现代浏览器的最新版本;承诺支持唯一会出现问题的地方是 Opera Mini 和 IE11 及更早版本。

¥Promises work in the latest versions of all modern browsers; the only place where promise support will be a problem is in Opera Mini and IE11 and earlier versions.

在本文中,我们没有涉及 Promise 的所有功能,只是介绍最有趣和最有用的功能。当你开始更多地了解 Promise 时,你将遇到更多功能和技术。

¥We didn't touch on all features of promises in this article, just the most interesting and useful ones. As you start to learn more about promises, you'll come across more features and techniques.

许多现代 Web API 都是基于 Promise 的,包括 WebRTCWeb 音频 API多媒体捕获和流 API 等等。

¥Many modern Web APIs are promise-based, including WebRTC, Web Audio API, Media Capture and Streams API, and many more.

也可以看看