如何使用 Promise
承诺是现代 JavaScript 中异步编程的基础。承诺是一个由异步函数返回的对象,它表示操作的当前状态。在承诺返回给调用者时,操作通常尚未完成,但承诺对象提供了一些方法来处理操作最终的成功或失败。
先决条件 | 对 JavaScript 基础知识有合理的理解,包括事件处理。 |
---|---|
目标 | 了解如何在 JavaScript 中使用承诺。 |
在上一篇文章中,我们讨论了使用回调来实现异步函数。在这种设计中,您调用异步函数,并传入您的回调函数。该函数立即返回,并在操作完成后调用您的回调。
在基于承诺的 API 中,异步函数启动操作并返回一个Promise
对象。然后您可以将处理程序附加到此承诺对象,这些处理程序将在操作成功或失败时执行。
使用 fetch() API
注意:在这篇文章中,我们将通过将代码示例从页面复制到浏览器的 JavaScript 控制台来探索承诺。要进行设置
- 打开一个浏览器标签页并访问 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
的现代基于承诺的替代品。
将此复制到您的浏览器 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
对象,并且它有一个值为"pending"
的state
。"pending"
状态意味着 fetch 操作仍在进行。 - 将处理程序函数传递到承诺的
then()
方法中。当(以及如果)fetch 操作成功时,承诺将调用我们的处理程序,并传入一个Response
对象,其中包含服务器的响应。 - 记录一条我们已经开始请求的消息。
完整的输出应该类似于
Promise { <state>: "pending" } Started request… Received response: 200
请注意,Started request…
在我们收到响应之前就被记录下来。与同步函数不同,fetch()
在请求仍在进行时返回,使我们的程序保持响应性。响应显示了 200
(OK)状态代码,这意味着我们的请求成功了。
这可能看起来很像上一篇文章中的示例,我们在其中向 XMLHttpRequest
对象添加了事件处理程序。与之不同的是,我们正在将处理程序传递到返回的承诺的 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);
});
});
在这个示例中,与之前一样,我们向 fetch()
返回的承诺添加了一个 then()
处理程序。但这一次,我们的处理程序调用 response.json()
,然后将一个新的 then()
处理程序传递到 response.json()
返回的承诺中。
这应该记录“baked beans”(在“products.json”中列出的第一个产品的名称)。
但是等等!还记得上一篇文章中,我们说过在另一个回调中调用回调会导致代码越来越嵌套吗?我们还说过这种“回调地狱”使我们的代码难以理解?这难道不是一样,只是用 then()
调用来代替吗?
当然,是这样的。但承诺的优雅之处在于then()
本身会返回一个承诺,该承诺将使用传递给它的函数的结果完成。这意味着我们可以(而且当然应该)像这样重写上面的代码
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”时抛出错误来做到这一点。
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,以便请求会失败。
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来涵盖fulfilled 和rejected。
如果承诺已结算,或者已“锁定”以遵循另一个承诺的状态,则该承诺已resolved。
文章 让我们谈谈如何谈论承诺 对此术语的细节进行了很好的解释。
组合多个 Promise
当您的操作由多个异步函数组成,并且您需要每个函数都完成才能开始下一个函数时,您需要承诺链。但是,您可能需要以其他方式组合异步函数调用,并且 Promise
API 为它们提供了一些辅助工具。
有时,您需要所有承诺都完成,但它们彼此之间没有依赖关系。在这种情况下,一起启动它们要比通知它们都完成更有效。您需要的是 Promise.all()
方法。它接受一个承诺数组并返回一个单一承诺。
Promise.all()
返回的承诺是
- 当且仅当数组中的所有承诺都完成时才完成。在这种情况下,
then()
处理程序将使用所有响应的数组调用,这些响应的顺序与承诺传递到all()
中的顺序相同。 - 当且仅当数组中的任何一个承诺被拒绝时才拒绝。在这种情况下,
catch()
处理程序将使用拒绝承诺抛出的错误调用。
例如
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 来执行相同的代码,例如
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()
相似,不同之处在于,它在承诺数组中的任何一个完成时完成,或者在它们全部被拒绝时拒绝
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
将使其成为异步函数
async function myFunction() {
// This is an async function
}
在异步函数中,您可以在返回承诺的函数调用之前使用 await
关键字。这使得代码在该点等待,直到承诺结算,此时承诺的完成值将被视为返回值,或者拒绝值将被抛出。
这使您能够编写使用异步函数但看起来像同步代码的代码。例如,我们可以使用它来重写我们的 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 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 链的最后一步。
另外,请注意,除非您的代码位于JavaScript 模块中,否则您只能在async
函数内使用await
。这意味着您不能在普通脚本中这样做。
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
语句类似的错误处理方式。
async
和await
关键字使从一系列连续的异步函数调用构建操作变得更容易,避免了创建显式 promise 链的需要,并允许您编写看起来像同步代码的代码。
promise 在所有现代浏览器的最新版本中都有效;promise 支持唯一有问题的地方是 Opera Mini 和 IE11 及更早版本。
我们在这篇文章中没有涉及 promise 的所有功能,只是最有趣和最有用的功能。当您开始了解有关 promise 的更多信息时,您会遇到更多功能和技术。
许多现代 Web API 都是基于 promise 的,包括WebRTC、Web Audio API、Media Capture and Streams API等等。
另请参阅
Promise()
- 使用 promise
- 我们有一个关于 promise 的问题 作者:Nolan Lawson
- 让我们谈谈如何谈论 promise