JavaScript 权威指南第七版(GPT 重译)(五)(1)https://developer.aliyun.com/article/1485371
13.1.3 网络事件
JavaScript 编程中另一个常见的异步来源是网络请求。在浏览器中运行的 JavaScript 可以使用以下代码从 Web 服务器获取数据:
function getCurrentVersionNumber(versionCallback) { // Note callback argument // Make a scripted HTTP request to a backend version API let request = new XMLHttpRequest(); request.open("GET", "http://www.example.com/api/version"); request.send(); // Register a callback that will be invoked when the response arrives request.onload = function() { if (request.status === 200) { // If HTTP status is good, get version number and call callback. let currentVersion = parseFloat(request.responseText); versionCallback(null, currentVersion); } else { // Otherwise report an error to the callback versionCallback(response.statusText, null); } }; // Register another callback that will be invoked for network errors request.onerror = request.ontimeout = function(e) { versionCallback(e.type, null); }; }
客户端 JavaScript 代码可以使用 XMLHttpRequest 类加上回调函数来进行 HTTP 请求,并在服务器响应到达时异步处理。¹ 这里定义的getCurrentVersionNumber()
函数(我们可以想象它被假设的checkForUpdates()
函数使用,我们在§13.1.1 中讨论过)发出 HTTP 请求,并定义在接收到服务器响应或超时或其他错误导致请求失败时将被调用的事件处理程序。
请注意,上面的代码示例不像我们之前的示例那样调用addEventListener()
。对于大多数 Web API(包括此示例),可以通过在生成事件的对象上调用addEventListener()
并传递感兴趣的事件名称以及回调函数来定义事件处理程序。通常,您也可以通过将其直接分配给对象的属性来注册单个事件监听器。这就是我们在这个示例代码中所做的,将函数分配给onload
、onerror
和ontimeout
属性。按照惯例,像这样的事件监听器属性总是以on开头的名称。addEventListener()
是更灵活的技术,因为它允许注册多个事件处理程序。但在确保没有其他代码需要为相同的对象和事件类型注册监听器的情况下,直接将适当的属性设置为您的回调可能更简单。
在这个示例代码中关于getCurrentVersionNumber()
函数的另一点需要注意的是,由于它发出了一个异步请求,它无法同步返回调用者感兴趣的值(当前版本号)。相反,调用者传递一个回调函数,当结果准备就绪或发生错误时调用。在这种情况下,调用者提供了一个期望两个参数的回调函数。如果 XMLHttpRequest 正常工作,那么getCurrentVersionNumber()
会用null
作为第一个参数,版本号作为第二个参数调用回调。或者,如果发生错误,那么getCurrentVersionNumber()
会用错误详细信息作为第一个参数,null
作为第二个参数调用回调。
13.1.4 Node 中的回调和事件
Node.js 服务器端 JavaScript 环境是深度异步的,并定义了许多使用回调和事件的 API。例如,读取文件内容的默认 API 是异步的,并在文件内容被读取后调用回调函数:
const fs = require("fs"); // The "fs" module has filesystem-related APIs let options = { // An object to hold options for our program // default options would go here }; // Read a configuration file, then call the callback function fs.readFile("config.json", "utf-8", (err, text) => { if (err) { // If there was an error, display a warning, but continue console.warn("Could not read config file:", err); } else { // Otherwise, parse the file contents and assign to the options object Object.assign(options, JSON.parse(text)); } // In either case, we can now start running the program startProgram(options); });
Node 的fs.readFile()
函数将一个两参数回调作为其最后一个参数。它异步读取指定的文件,然后调用回调。如果文件成功读取,它将文件内容作为第二个回调参数传递。如果出现错误,它将错误作为第一个回调参数传递。在这个例子中,我们将回调表达为箭头函数,这是一种简洁和自然的语法,适用于这种简单操作。
Node 还定义了许多基于事件的 API。以下函数展示了如何在 Node 中请求 URL 的内容。它有两层通过事件监听器处理的异步代码。请注意,Node 使用on()
方法来注册事件监听器,而不是addEventListener()
:
const https = require("https"); // Read the text content of the URL and asynchronously pass it to the callback. function getText(url, callback) { // Start an HTTP GET request for the URL request = https.get(url); // Register a function to handle the "response" event. request.on("response", response => { // The response event means that response headers have been received let httpStatus = response.statusCode; // The body of the HTTP response has not been received yet. // So we register more event handlers to to be called when it arrives. response.setEncoding("utf-8"); // We're expecting Unicode text let body = ""; // which we will accumulate here. // This event handler is called when a chunk of the body is ready response.on("data", chunk => { body += chunk; }); // This event handler is called when the response is complete response.on("end", () => { if (httpStatus === 200) { // If the HTTP response was good callback(null, body); // Pass response body to the callback } else { // Otherwise pass an error callback(httpStatus, null); } }); }); // We also register an event handler for lower-level network errors request.on("error", (err) => { callback(err, null); }); }
13.2 承诺
现在我们已经在客户端和服务器端 JavaScript 环境中看到了回调和基于事件的异步编程的示例,我们可以介绍承诺,这是一个旨在简化异步编程的核心语言特性。
承诺是表示异步计算结果的对象。该结果可能已经准备好,也可能尚未准备好,承诺 API 故意对此保持模糊:没有同步获取承诺值的方法;您只能要求承诺在值准备好时调用回调函数。如果您正在定义一个类似前一节中的getText()
函数的异步 API,但希望将其基于承诺,省略回调参数,而是返回一个承诺对象。调用者可以在这个承诺对象上注册一个或多个回调,当异步计算完成时,它们将被调用。
因此,在最简单的层面上,承诺只是一种与回调一起工作的不同方式。然而,使用它们有实际的好处。基于回调的异步编程的一个真正问题是,通常会出现回调内嵌在回调内嵌在回调中的情况,代码行缩进如此之深,以至于难以阅读。承诺允许将这种嵌套回调重新表达为更线性的承诺链,这样更容易阅读和推理。
回调函数的另一个问题是,它们可能会使处理错误变得困难。如果异步函数(或异步调用的回调)抛出异常,那么这个异常就无法传播回异步操作的发起者。这是关于异步编程的一个基本事实:它破坏了异常处理。另一种方法是通过回调参数和返回值来细致地跟踪和传播错误,但这样做很繁琐,很难做到正确。承诺在这里有所帮助,通过标准化处理错误的方式,并提供一种让错误正确传播通过一系列承诺的方法。
请注意,承诺代表单个异步计算的未来结果。然而,它们不能用于表示重复的异步计算。在本章的后面,我们将编写一个基于承诺的setTimeout()
函数的替代方案。但我们不能使用承诺来替代setInterval()
,因为该函数会重复调用回调函数,而这是承诺设计上不支持的。同样地,我们可以使用承诺来替代 XMLHttpRequest 对象的“load”事件处理程序,因为该回调只会被调用一次。但通常情况下,我们不会使用承诺来替代 HTML 按钮对象的“click”事件处理程序,因为我们通常希望允许用户多次点击按钮。
接下来的小节将:
- 解释承诺术语并展示基本承诺用法
- 展示 Promises 如何被链式调用
- 展示如何创建自己的基于 Promise 的 API
重要
起初,Promise 似乎很简单,事实上,Promise 的基本用例确实简单明了。但是,对于超出最简单用例的任何情况,它们可能变得令人惊讶地令人困惑。Promise 是异步编程的强大习语,但你需要深入理解才能正确自信地使用它们。然而,花时间深入了解是值得的,我敦促你仔细研究这一长章节。
13.2.1 使用 Promises
随着 Promises 在核心 JavaScript 语言中的出现,Web 浏览器已经开始实现基于 Promise 的 API。在前一节中,我们实现了一个getText()
函数,该函数发起了一个异步的 HTTP 请求,并将 HTTP 响应的主体作为字符串传递给指定的回调函数。想象一个这个函数的变体,getJSON()
,它将 HTTP 响应的主体解析为 JSON,并返回一个 Promise,而不是接受一个回调参数。我们将在本章后面实现一个getJSON()
函数,但现在,让我们看看如何使用这个返回 Promise 的实用函数:
getJSON(url).then(jsonData => { // This is a callback function that will be asynchronously // invoked with the parsed JSON value when it becomes available. });
getJSON()
启动一个异步的 HTTP 请求,请求指定的 URL,然后,在该请求挂起期间,它返回一个 Promise 对象。Promise 对象定义了一个then()
实例方法。我们不直接将回调函数传递给getJSON()
,而是将其传递给then()
方法。当 HTTP 响应到达时,该响应的主体被解析为 JSON,并将解析后的值传递给我们传递给then()
的函数。
你可以将then()
方法看作是一个回调注册方法,类似于用于在客户端 JavaScript 中注册事件处理程序的addEventListener()
方法。如果多次调用 Promise 对象的then()
方法,每个指定的函数都将在承诺的计算完成时被调用。
与许多事件侦听器不同,Promise 代表一个单一的计算,每个注册到then()
的函数只会被调用一次。值得注意的是,无论何时调用then()
,你传递给then()
的函数都会异步调用,即使异步计算在调用then()
时已经完成。
在简单的语法层面上,then()
方法是 Promise 的独特特征,习惯上直接将.then()
附加到返回 Promise 的函数调用上,而不是将 Promise 对象分配给变量的中间步骤。
习惯上,将返回 Promises 的函数和使用 Promises 结果的函数命名为动词,这些习惯导致的代码特别易于阅读:
// Suppose you have a function like this to display a user profile function displayUserProfile(profile) { /* implementation omitted */ } // Here's how you might use that function with a Promise. // Notice how this line of code reads almost like an English sentence: getJSON("/api/user/profile").then(displayUserProfile);
使用 Promises 处理错误
异步操作,特别是涉及网络的操作,通常会以多种方式失败,必须编写健壮的代码来处理不可避免发生的错误。
对于 Promises,我们可以通过将第二个函数传递给then()
方法来实现:
getJSON("/api/user/profile").then(displayUserProfile, handleProfileError);
Promise 代表在 Promise 对象创建后发生的异步计算的未来结果。因为计算是在 Promise 对象返回给我们后执行的,所以传统上计算无法返回一个值或抛出我们可以捕获的异常。我们传递给then()
的函数提供了替代方案。当同步计算正常完成时,它只是将其结果返回给调用者。当基于 Promise 的异步计算正常完成时,它将其结果传递给作为then()
的第一个参数的函数。
当同步计算出现问题时,它会抛出一个异常,该异常会向上传播到调用堆栈,直到有一个catch
子句来处理它。当异步计算运行时,其调用者不再在堆栈上,因此如果出现问题,就不可能将异常抛回给调用者。
相反,基于 Promise 的异步计算将异常(通常作为某种类型的 Error 对象,尽管这不是必需的)传递给then()
的第二个函数。因此,在上面的代码中,如果getJSON()
正常运行,它会将结果传递给displayUserProfile()
。如果出现错误(用户未登录、服务器宕机、用户的互联网连接中断、请求超时等),那么getJSON()
会将一个 Error 对象传递给handleProfileError()
。
在实践中,很少看到两个函数传递给then()
。在处理 Promise 时,有一种更好的、更符合习惯的处理错误的方式。要理解这一点,首先考虑一下如果getJSON()
正常完成,但displayUserProfile()
中出现错误会发生什么。当getJSON()
返回时,回调函数会异步调用,因此它也是异步的,不能有意义地抛出异常(因为没有代码在调用堆栈上处理它)。
在这段代码中处理错误的更符合习惯的方式如下:
getJSON("/api/user/profile").then(displayUserProfile).catch(handleProfileError);
使用这段代码,getJSON()
的正常结果仍然会传递给displayUserProfile()
,但是getJSON()
或displayUserProfile()
中的任何错误(包括displayUserProfile
抛出的任何异常)都会传递给handleProfileError()
。catch()
方法只是调用then()
的一种简写形式,第一个参数为null
,第二个参数为指定的错误处理函数。
当我们讨论下一节的 Promise 链时,我们将会更多地谈到catch()
和这种错误处理习惯。
13.2.2 链式 Promise
Promise 最重要的好处之一是它们提供了一种自然的方式来将一系列异步操作表达为then()
方法调用的线性链,而无需将每个操作嵌套在前一个操作的回调中。例如,这里是一个假设的 Promise 链:
fetch(documentURL) // Make an HTTP request .then(response => response.json()) // Ask for the JSON body of the response .then(document => { // When we get the parsed JSON return render(document); // display the document to the user }) .then(rendered => { // When we get the rendered document cacheInDatabase(rendered); // cache it in the local database. }) .catch(error => handle(error)); // Handle any errors that occur
这段代码说明了一系列 Promise 如何简单地表达一系列异步操作的过程。然而,我们不会讨论这个特定的 Promise 链。不过,我们将继续探讨使用 Promise 链进行 HTTP 请求的想法。
在本章的前面,我们看到了在 JavaScript 中使用 XMLHttpRequest 对象进行 HTTP 请求。这个奇怪命名的对象具有一个古老且笨拙的 API,它已经大部分被新的、基于 Promise 的 Fetch API(§15.11.1)所取代。在其最简单的形式中,这个新的 HTTP API 就是函数fetch()
。你传递一个 URL 给它,它会返回一个 Promise。当 HTTP 响应开始到达并且 HTTP 状态和头部可用时,这个 Promise 就会被实现:
fetch("/api/user/profile").then(response => { // When the promise resolves, we have status and headers if (response.ok && response.headers.get("Content-Type") === "application/json") { // What can we do here? We don't actually have the response body yet. } });
当fetch()
返回的 Promise 被实现时,它会将一个 Response 对象传递给您传递给其then()
方法的函数。这个响应对象让您可以访问请求状态和头部,并且还定义了像text()
和json()
这样的方法,分别以文本和 JSON 解析形式访问响应主体。但是尽管初始 Promise 被实现,响应主体可能尚未到达。因此,用于访问响应主体的这些text()
和json()
方法本身返回 Promise。以下是使用fetch()
和response.json()
方法获取 HTTP 响应主体的一种天真的方法:
fetch("/api/user/profile").then(response => { response.json().then(profile => { // Ask for the JSON-parsed body // When the body of the response arrives, it will be automatically // parsed as JSON and passed to this function. displayUserProfile(profile); }); });
这是一种天真地使用 Promise 的方式,因为我们像回调一样嵌套它们,这违背了初衷。更好的习惯是使用 Promise 在一个顺序链中编写代码,就像这样:
fetch("/api/user/profile") .then(response => { return response.json(); }) .then(profile => { displayUserProfile(profile); });
让我们看一下这段代码中的方法调用,忽略传递给方法的参数:
fetch().then().then()
当在一个表达式中调用多个方法时,我们称之为方法链。我们知道fetch()
函数返回一个 Promise 对象,我们可以看到这个链中的第一个.then()
调用在返回的 Promise 对象上调用一个方法。但是链中还有第二个.then()
,这意味着then()
方法的第一次调用本身必须返回一个 Promise。
有时,当设计 API 以使用这种方法链时,只有一个对象,并且该对象的每个方法都返回对象本身以便于链接。然而,这并不是 Promise 的工作方式。当我们编写一系列.then()
调用时,我们并不是在单个 Promise 对象上注册多个回调。相反,then()
方法的每次调用都会返回一个新的 Promise 对象。直到传递给then()
的函数完成,新的 Promise 对象才会被实现。
让我们回到上面原始fetch()
链的简化形式。如果我们在其他地方定义传递给then()
调用的函数,我们可以重构代码如下:
fetch(theURL) // task 1; returns promise 1 .then(callback1) // task 2; returns promise 2 .then(callback2); // task 3; returns promise 3
让我们详细讨论一下这段代码:
- 在第一行,使用一个 URL 调用
fetch()
。它为该 URL 发起一个 HTTP GET 请求并返回一个 Promise。我们将这个 HTTP 请求称为“任务 1”,将 Promise 称为“promise 1”。 - 在第二行,我们调用 promise 1 的
then()
方法,传递我们希望在 promise 1 实现时调用的callback1
函数。then()
方法将我们的回调存储在某个地方,然后返回一个新的 Promise。我们将在这一步返回的新 Promise 称为“promise 2”,并且我们将说“任务 2”在调用callback1
时开始。 - 在第三行,我们调用 promise 2 的
then()
方法,传递我们希望在 promise 2 实现时调用的callback2
函数。这个then()
方法记住我们的回调并返回另一个 Promise。我们将说“任务 3”在调用callback2
时开始。我们可以称这个最新的 Promise 为“promise 3”,但实际上我们不需要为它命名,因为我们根本不会使用它。 - 前三个步骤都是在表达式首次执行时同步发生的。现在,在 HTTP 请求在步骤 1 中发出并通过互联网发送时,我们有一个异步暂停。
- 最终,HTTP 响应开始到达。
fetch()
调用的异步部分将 HTTP 状态和标头包装在一个 Response 对象中,并使用该 Response 对象作为值来实现 promise 1。 - 当 promise 1 被实现时,它的值(Response 对象)被传递给我们的
callback1()
函数,任务 2 开始。这个任务的工作是,给定一个 Response 对象作为输入,获取响应主体作为 JSON 对象。 - 让我们假设任务 2 正常完成,并且能够解析 HTTP 响应的主体以生成一个 JSON 对象。这个 JSON 对象用于实现 promise 2。
- 实现 promise 2 的值成为传递给
callback2()
函数时任务 3 的输入。当任务 3 完成(假设它正常完成)时,promise 3 将被实现。但因为我们从未对 promise 3 做任何操作,当该 Promise 完成时什么也不会发生,异步计算链在这一点结束。
13.2.3 解决 Promise
在上一节中解释了与列表中的 URL 获取 Promise 链相关的内容时,我们谈到了 promise 1、2 和 3。但实际上还涉及第四个 Promise 对象,这将引出我们对 Promise“解决”意味着什么的重要讨论。
请记住,fetch()
返回一个 Promise 对象,当实现时,将传递一个 Response 对象给我们注册的回调函数。这个 Response 对象有.text()
、.json()
和其他方法以各种形式请求 HTTP 响应的主体。但是由于主体可能尚未到达,这些方法必须返回 Promise 对象。在我们一直在研究的示例中,“任务 2”调用.json()
方法并返回其值。这是第四个 Promise 对象,也是callback1()
函数的返回值。
让我们再次以冗长和非成语化的方式重写 URL 获取代码,使回调和 Promises 明确:
function c1(response) { // callback 1 let p4 = response.json(); return p4; // returns promise 4 } function c2(profile) { // callback 2 displayUserProfile(profile); } let p1 = fetch("/api/user/profile"); // promise 1, task 1 let p2 = p1.then(c1); // promise 2, task 2 let p3 = p2.then(c2); // promise 3, task 3
为了使 Promise 链有用地工作,任务 2 的输出必须成为任务 3 的输入。在我们正在考虑的示例中,任务 3 的输入是获取的 URL 主体,解析为 JSON 对象。但是,正如我们刚才讨论的,回调c1
的返回值不是 JSON 对象,而是该 JSON 对象的 Promisep4
。这似乎是一个矛盾,但实际上不是:当p1
被实现时,c1
被调用,任务 2 开始。当p2
被实现时,c2
被调用,任务 3 开始。但是仅仅因为在调用c1
时任务 2 开始,并不意味着任务 2 在c1
返回时必须结束。毕竟,Promises 是关于管理异步任务的,如果任务 2 是异步的(在这种情况下是),那么在回调返回时该任务将尚未完成。
现在我们准备讨论您需要真正掌握 Promises 的最后一个细节。当您将回调c
传递给then()
方法时,then()
返回一个 Promisep
并安排在稍后的某个时间异步调用c
。回调执行一些计算并返回一个值v
。当回调返回时,p
被解析为值v
。当一个 Promise 被解析为一个不是 Promise 的值时,它会立即被实现为该值。因此,如果c
返回一个非 Promise,那么返回值就成为p
的值,p
被实现,我们完成了。但是如果返回值v
本身是一个 Promise,那么p
被解析但尚未实现。在这个阶段,p
不能解决,直到 Promisev
解决。如果v
被实现,那么p
将被实现为相同的值。如果v
被拒绝,那么p
将因同样的原因被拒绝。这就是 Promise“解析”状态的含义:Promise 已经与另一个 Promise 关联或“锁定”。我们还不知道p
是否会被实现或被拒绝,但是我们的回调c
不再控制这一点。p
“解析”意味着它的命运现在完全取决于 Promisev
的发生。
让我们回到我们的 URL 获取示例。当c1
返回p4
时,p2
被解析。但被解析并不意味着被实现,所以任务 3 还没有开始。当完整的 HTTP 响应主体可用时,.json()
方法可以解析它并使用解析后的值来实现p4
。当p4
被实现时,p2
也会自动被实现,具有相同的解析 JSON 值。此时,解析后的 JSON 对象被传递给c2
,任务 3 开始。
这可能是 JavaScript 中最难理解的部分之一,您可能需要阅读本节不止一次。图 13-1 以可视化形式呈现了这个过程,可能有助于为您澄清。
图 13-1. 使用 Promises 获取 URL
13.2.4 更多关于 Promises 和错误
在本章的前面,我们看到您可以将第二个回调函数传递给.then()
方法,并且如果 Promise 被拒绝,则将调用此第二个函数。当发生这种情况时,传递给此第二个回调函数的参数是一个值—通常是代表拒绝原因的 Error 对象。我们还了解到,通过向 Promise 链中添加.catch()
方法调用来处理 Promise 相关的错误是不常见的(甚至是不成文的)。现在我们已经检查了 Promise 链,我们可以回到错误处理并更详细地讨论它。在讨论之前,我想强调的是,在进行异步编程时,仔细处理错误非常重要。对于同步代码,如果您省略了错误处理代码,您至少会得到一个异常和堆栈跟踪,以便您可以找出出了什么问题。对于异步代码,未处理的异常通常不会被报告,错误可能会悄无声息地发生,使得调试变得更加困难。好消息是,.catch()
方法使得在处理 Promise 时处理错误变得容易。
catch 和 finally 方法
Promise 的.catch()
方法只是一种使用null
作为第一个参数并将错误处理回调作为第二个参数调用.then()
的简写方式。给定任何 Promisep
和回调c
,以下两行是等效的:
p.then(null, c); p.catch(c);
.catch()
简写更受欢迎,因为它更简单,并且名称与try/catch
异常处理语句中的catch
子句匹配。正如我们讨论过的,普通异常在异步代码中不起作用。Promise 的.catch()
方法是一种适用于异步代码的替代方法。当同步代码出现问题时,我们可以说异常“沿着调用堆栈上升”直到找到catch
块。对于 Promise 链的异步链,类似的隐喻可能是错误“沿着链路下滑”,直到找到.catch()
调用。
在 ES2018 中,Promise 对象还定义了一个.finally()
方法,其目的类似于try/catch/finally
语句中的finally
子句。如果您在 Promise 链中添加一个.finally()
调用,那么您传递给.finally()
的回调将在您调用它的 Promise 完成时被调用。如果 Promise 完成或拒绝,都会调用您的回调,并且不会传递任何参数,因此您无法找出它是完成还是拒绝。但是,如果您需要在任一情况下运行某种清理代码(例如关闭打开的文件或网络连接),则.finally()
回调是执行此操作的理想方式。与.then()
和.catch()
一样,.finally()
返回一个新的 Promise 对象。.finally()
回调的返回值通常被忽略,而由.finally()
返回的 Promise 通常将使用与调用.finally()
的 Promise 解析或拒绝的相同值解析或拒绝。但是,如果.finally()
回调引发异常,则由.finally()
返回的 Promise 将以该值拒绝。
我们在前几节中学习的 URL 获取代码没有进行任何错误处理。现在让我们通过代码的更实际版本来纠正这一点:
fetch("/api/user/profile") // Start the HTTP request .then(response => { // Call this when status and headers are ready if (!response.ok) { // If we got a 404 Not Found or similar error return null; // Maybe user is logged out; return null profile } // Now check the headers to ensure that the server sent us JSON. // If not, our server is broken, and this is a serious error! let type = response.headers.get("content-type"); if (type !== "application/json") { throw new TypeError(`Expected JSON, got ${type}`); } // If we get here, then we got a 2xx status and a JSON content-type // so we can confidently return a Promise for the response // body as a JSON object. return response.json(); }) .then(profile => { // Called with the parsed response body or null if (profile) { displayUserProfile(profile); } else { // If we got a 404 error above and returned null we end up here displayLoggedOutProfilePage(); } }) .catch(e => { if (e instanceof NetworkError) { // fetch() can fail this way if the internet connection is down displayErrorMessage("Check your internet connection."); } else if (e instanceof TypeError) { // This happens if we throw TypeError above displayErrorMessage("Something is wrong with our server!"); } else { // This must be some kind of unanticipated error console.error(e); } });
让我们通过分析当事情出错时会发生什么来分析这段代码。我们将使用之前使用的命名方案:p1
是fetch()
调用返回的 Promise。p2
是第一个.then()
调用返回的 Promise,c1
是我们传递给该.then()
调用的回调。p3
是第二个.then()
调用返回的 Promise,c2
是我们传递给该调用的回调。最后,c3
是我们传递给.catch()
调用的回调。(该调用返回一个 Promise,但我们不需要通过名称引用它。)
可能失败的第一件事是 fetch()
请求本身。如果网络连接断开(或由于某种原因无法进行 HTTP 请求),那么 Promise p1
将被拒绝,并带有一个 NetworkError 对象。我们没有将错误处理回调函数作为第二个参数传递给 .then()
调用,因此 p2
也将以相同的 NetworkError 对象被拒绝。(如果我们向第一个 .then()
调用传递了错误处理程序,错误处理程序将被调用,如果它正常返回,p2
将被解析和/或完成,并带有该处理程序的返回值。)然而,没有处理程序,p2
被拒绝,然后 p3
由于相同原因被拒绝。此时,c3
错误处理回调被调用,并其中的 NetworkError 特定代码运行。
我们的代码可能失败的另一种方式是,如果我们的 HTTP 请求返回 404 Not Found 或其他 HTTP 错误。这些是有效的 HTTP 响应,因此 fetch()
调用不认为它们是错误。fetch()
将 404 Not Found 封装在一个 Response 对象中,并用该对象完成 p1
,导致调用 c1
。我们在 c1
中的代码检查 Response 对象的 ok
属性,以检测是否收到了正常的 HTTP 响应,并通过简单返回 null
处理这种情况。因为这个返回值不是一个 Promise,它立即完成 p2
,并用这个值调用 c2
。我们在 c2
中明确检查和处理 falsy 值,通过向用户显示不同的结果来处理这种情况。这是一个我们将异常条件视为非错误并在不使用错误处理程序的情况下处理它的案例。
如果我们得到一个正常的 HTTP 响应代码,但 Content-Type 头部未正确设置,c1
中会发生一个更严重的错误。我们的代码期望一个 JSON 格式的响应,所以如果服务器发送给我们 HTML、XML 或纯文本,我们将会遇到问题。c1
包含了检查 Content-Type 头部的代码。如果头部错误,它将把这视为一个不可恢复的问题并抛出一个 TypeError。当传递给 .then()
(或 .catch()
)的回调抛出一个值时,作为 .then()
调用的返回值的 Promise 将被拒绝,并带有该抛出的值。在这种情况下,引发 TypeError 的 c1
中的代码导致 p2
被拒绝,并带有该 TypeError 对象。由于我们没有为 p2
指定错误处理程序,p3
也将被拒绝。c2
将不会被调用,并且 TypeError 将传递给 c3
,它具有明确检查和处理这种类型错误的代码。
关于这段代码有几点值得注意。首先,请注意,使用常规的同步 throw
语句抛出的错误对象最终会在 Promise 链中的 .catch()
方法调用中异步处理。这应该清楚地说明为什么这种简写方法优先于向 .then()
传递第二个参数,并且为什么在 Promise 链末尾使用 .catch()
调用是如此习惯化的。
在我们离开错误处理的话题之前,我想指出,虽然习惯于在每个 Promise 链的末尾使用 .catch()
来清理(或至少记录)链中发生的任何错误,但在 Promise 链的其他地方使用 .catch()
也是完全有效的。如果你的 Promise 链中的某个阶段可能会因错误而失败,并且如果错误是某种可恢复的错误,不应该阻止链的其余部分运行,那么你可以在链中插入一个 .catch()
调用,代码可能看起来像这样:
startAsyncOperation() .then(doStageTwo) .catch(recoverFromStageTwoError) .then(doStageThree) .then(doStageFour) .catch(logStageThreeAndFourErrors);
请记住,您传递给 .catch()
的回调只有在前一个阶段的回调抛出错误时才会被调用。如果回调正常返回,那么 .catch()
回调将被跳过,并且前一个回调的返回值将成为下一个 .then()
回调的输入。还要记住,.catch()
回调不仅用于报告错误,还用于处理和恢复错误。一旦错误传递给 .catch()
回调,它就会停止在 Promise 链中传播。.catch()
回调可以抛出新错误,但如果它正常返回,那么返回值将用于解析和/或实现相关的 Promise,并且错误将停止传播。
让我们具体说明一下:在前面的代码示例中,如果 startAsyncOperation()
或 doStageTwo()
抛出错误,则将调用 recoverFromStageTwoError()
函数。如果 recoverFromStageTwoError()
正常返回,则其返回值将传递给 doStageThree()
,异步操作将继续正常进行。另一方面,如果 recoverFromStageTwoError()
无法恢复,则它将抛出错误(或重新抛出传递给它的错误)。在这种情况下,doStageThree()
和 doStageFour()
都不会被调用,并且由 recoverFromStageTwoError()
抛出的错误将传递给 logStageThreeAndFourErrors()
。
有时,在复杂的网络环境中,错误可能更多或更少地随机发生,通过简单地重试异步请求来处理这些错误可能是合适的。想象一下,您已经编写了一个基于 Promise 的操作来查询数据库:
queryDatabase() .then(displayTable) .catch(displayDatabaseError);
现在假设瞬时网络负载问题导致失败率约为 1%。一个简单的解决方案可能是使用 .catch()
调用重试查询:
queryDatabase() .catch(e => wait(500).then(queryDatabase)) // On failure, wait and retry .then(displayTable) .catch(displayDatabaseError);
如果假设的故障确实是随机的,那么添加这一行代码应该将您的错误率从 1% 降低到 0.01%。
13.2.5 并行的 Promises
我们花了很多时间讨论 Promise 链,用于顺序运行更大异步操作的异步步骤。但有时,我们希望并行执行多个异步操作。函数 Promise.all()
可以做到这一点。Promise.all()
接受一个 Promise 对象数组作为输入,并返回一个 Promise。如果任何输入 Promise 被拒绝,则返回的 Promise 将被拒绝。否则,它将以每个输入 Promise 的实现值数组实现。因此,例如,如果您想获取多个 URL 的文本内容,您可以使用以下代码:
// We start with an array of URLs const urls = [ /* zero or more URLs here */ ]; // And convert it to an array of Promise objects promises = urls.map(url => fetch(url).then(r => r.text())); // Now get a Promise to run all those Promises in parallel Promise.all(promises) .then(bodies => { /* do something with the array of strings */ }) .catch(e => console.error(e));
Promise.all()
稍微比之前描述的更灵活。输入数组可以包含 Promise 对象和非 Promise 值。如果数组的元素不是 Promise,则会被视为已实现 Promise 的值,并且会被简单地复制到输出数组中。
Promise.all()
返回的 Promise 在任何输入 Promise 被拒绝时也会被拒绝。这会立即发生在第一个拒绝时,而其他输入 Promise 仍在等待的情况下也可能发生。在 ES2020 中,Promise.allSettled()
接受一个输入 Promise 数组并返回一个 Promise,就像 Promise.all()
一样。但是 Promise.allSettled()
永远不会拒绝返回的 Promise,并且在所有输入 Promise 都已完成之前不会实现该 Promise。该 Promise 解析为一个对象数组,每个输入 Promise 都有一个对象。每个返回的对象都有一个 status
属性,设置为“fulfilled”或“rejected”。如果状态是“fulfilled”,那么对象还将有一个 value
属性,给出实现值。如果状态是“rejected”,那么对象还将有一个 reason
属性,给出相应 Promise 的错误或拒绝值:
Promise.allSettled([Promise.resolve(1), Promise.reject(2), 3]).then(results => { results[0] // => { status: "fulfilled", value: 1 } results[1] // => { status: "rejected", reason: 2 } results[2] // => { status: "fulfilled", value: 3 } });
有时,您可能希望同时运行多个 Promise,但可能只关心第一个实现的值。在这种情况下,您可以使用Promise.race()
而不是Promise.all()
。它返回一个 Promise,当输入数组中的 Promise 中的第一个实现或拒绝时,该 Promise 将实现或拒绝。(或者,如果输入数组中有任何非 Promise 值,则简单地返回其中的第一个。)
13.2.6 创建 Promises
在许多先前的示例中,我们使用了返回 Promise 的函数fetch()
,因为它是内置到 Web 浏览器中的最简单的返回 Promise 的函数之一。我们对 Promises 的讨论还依赖于假设的返回 Promise 的函数getJSON()
和wait()
。编写返回 Promises 的函数确实非常有用,本节展示了如何创建基于 Promise 的 API。特别是,我们将展示getJSON()
和wait()
的实现。
基于其他 Promises 的 Promises
如果您有其他返回 Promise 的函数作为起点,编写返回 Promise 的函数就很容易。给定一个 Promise,您可以通过调用.then()
来创建(并返回)一个新的 Promise。因此,如果我们使用现有的fetch()
函数作为起点,我们可以这样编写getJSON()
:
function getJSON(url) { return fetch(url).then(response => response.json()); }
代码很简单,因为fetch()
API 的 Response 对象具有预定义的json()
方法。json()
方法返回一个 Promise,我们从回调中返回该 Promise(回调是一个带有单表达式主体的箭头函数,因此返回是隐式的),因此getJSON()
返回的 Promise 解析为response.json()
返回的 Promise。当该 Promise 实现时,由getJSON()
返回的 Promise 也实现为相同的值。请注意,此getJSON()
实现中没有错误处理。我们不检查response.ok
和 Content-Type 头,而是允许json()
方法拒绝返回的 Promise,如果响应主体无法解析为 JSON,则会引发 SyntaxError。
让我们再写一个返回 Promise 的函数,这次使用getJSON()
作为初始 Promise 的来源。
function getHighScore() { return getJSON("/api/user/profile").then(profile => profile.highScore); }
我们假设这个函数是某种基于 Web 的游戏的一部分,并且 URL“/api/user/profile”返回一个包含highScore
属性的 JSON 格式数据结构。
基于同步值的 Promises
有时,您可能需要实现现有的基于 Promise 的 API,并从函数返回一个 Promise,即使要执行的计算实际上不需要任何异步操作。在这种情况下,静态方法Promise.resolve()
和Promise.reject()
将实现您想要的效果。Promise.resolve()
以其单个参数作为值,并返回一个将立即(但异步地)实现为该值的 Promise。类似地,Promise.reject()
接受一个参数,并返回一个将以该值为原因拒绝的 Promise。(要明确:这些静态方法返回的 Promises 在返回时并未已实现或已拒绝,但它们将在当前同步代码块运行完毕后立即实现或拒绝。通常,除非有许多待处理的异步任务等待运行,否则这将在几毫秒内发生。)
请回顾§13.2.3 中的内容,已解决的 Promise 与已实现的 Promise 不是同一回事。当我们调用Promise.resolve()
时,通常会传递实现值以创建一个 Promise 对象,该对象将很快实现为该值。但是该方法的名称不是Promise.fulfill()
。如果将 Promisep1
传递给Promise.resolve()
,它将返回一个新的 Promisep2
,该 Promise 立即解决,但直到p1
实现或拒绝之前,它才会实现或拒绝。
可以编写一个基于 Promise 的函数,其中值是同步计算的,并使用Promise.resolve()
异步返回,尽管这种情况可能不太常见。然而,在异步函数中有同步特殊情况是相当常见的,你可以使用Promise.resolve()
和Promise.reject()
来处理这些特殊情况。特别是,如果在开始异步操作之前检测到错误条件(例如错误的参数值),你可以通过返回使用Promise.reject()
创建的 Promise 来报告该错误。(在这种情况下,你也可以同步抛出错误,但这被认为是不好的做法,因为调用者需要同时编写同步的catch
子句和使用异步的.catch()
方法来处理错误。)最后,Promise.resolve()
有时用于在 Promise 链中创建初始 Promise。我们将看到一些以这种方式使用它的示例。
从头开始的 Promises
对于getJSON()
和getHighScore()
,我们首先调用现有函数以获取初始 Promise,并通过调用该初始 Promise 的.then()
方法创建并返回一个新 Promise。但是,当你无法使用另一个返回 Promise 的函数作为起点时,如何编写返回 Promise 的函数呢?在这种情况下,你可以使用Promise()
构造函数创建一个全新的 Promise 对象,你可以完全控制它。操作如下:你调用Promise()
构造函数并将一个函数作为其唯一参数传递。你传递的函数应该预期两个参数,按照惯例,应该命名为resolve
和reject
。构造函数会同步调用带有resolve
和reject
参数的函数。在调用你的函数后,Promise()
构造函数会返回新创建的 Promise。返回的 Promise 受你传递给构造函数的函数控制。该函数应执行一些异步操作,然后调用resolve
函数以解析或实现返回的 Promise,或调用reject
函数以拒绝返回的 Promise。你的函数不必是异步的:如果这样做,即使你同步调用resolve
或reject
,Promise 仍将异步解析、实现或拒绝。
通过阅读关于将函数传递给构造函数的函数的功能可能很难理解,但希望一些示例能够澄清这一点。以下是如何编写基于 Promise 的wait()
函数的方法,我们在本章的早期示例中使用过:
function wait(duration) { // Create and return a new Promise return new Promise((resolve, reject) => { // These control the Promise // If the argument is invalid, reject the Promise if (duration < 0) { reject(new Error("Time travel not yet implemented")); } // Otherwise, wait asynchronously and then resolve the Promise. // setTimeout will invoke resolve() with no arguments, which means // that the Promise will fulfill with the undefined value. setTimeout(resolve, duration); }); }
请注意,用于控制使用Promise()
构造函数创建的 Promise 的命运的一对函数的名称分别为resolve()
和reject()
,而不是fulfill()
和reject()
。如果将一个 Promise 传递给resolve()
,则返回的 Promise 将解析为该新 Promise。然而,通常情况下,你会传递一个非 Promise 值,这将用该值实现返回的 Promise。
示例 13-1 是另一个使用Promise()
构造函数的示例。这个示例实现了我们的getJSON()
函数,用于在 Node 中使用,因为fetch()
API 没有内置。请记住,我们在本章一开始讨论了异步回调和事件。这个示例同时使用了回调和事件处理程序,因此很好地演示了我们如何在其他类型的异步编程风格之上实现基于 Promise 的 API。
示例 13-1. 一个异步的 getJSON() 函数
const http = require("http"); function getJSON(url) { // Create and return a new Promise return new Promise((resolve, reject) => { // Start an HTTP GET request for the specified URL request = http.get(url, response => { // called when response starts // Reject the Promise if the HTTP status is wrong if (response.statusCode !== 200) { reject(new Error(`HTTP status ${response.statusCode}`)); response.resume(); // so we don't leak memory } // And reject if the response headers are wrong else if (response.headers["content-type"] !== "application/json") { reject(new Error("Invalid content-type")); response.resume(); // don't leak memory } else { // Otherwise, register events to read the body of the response let body = ""; response.setEncoding("utf-8"); response.on("data", chunk => { body += chunk; }); response.on("end", () => { // When the response body is complete, try to parse it try { let parsed = JSON.parse(body); // If it parsed successfully, fulfill the Promise resolve(parsed); } catch(e) { // If parsing failed, reject the Promise reject(e); } }); } }); // We also reject the Promise if the request fails before we // even get a response (such as when the network is down) request.on("error", error => { reject(error); }); }); }
JavaScript 权威指南第七版(GPT 重译)(五)(3)https://developer.aliyun.com/article/1485373