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