《JavaScript权威指南第7版》第13章 异步JavaScript

本文详细介绍了JavaScript中的异步编程,从基础的回调函数、定时器、事件和网络事件,过渡到Promise,探讨了Promise的创建、链式调用、错误处理和并行执行。进一步讲解了ES2017引入的async/await关键字,展示了如何简化异步代码,使其更接近同步语法。最后,文章提到了异步迭代器和生成器,提供了一种处理异步事件流的新方法,这对于处理Node.js中的流和客户端事件非常有用。
摘要由CSDN通过智能技术生成

一些计算机程序,如科学模拟和机器学习模型,是受计算限制的:它们不停地连续运行,直到计算出结果为止。然而,大多数现实世界的计算机程序都是异步的。这意味着,在等待数据到达或某些事件发生时,它们常常不得不停止计算。web浏览器中的JavaScript程序通常是事件驱动的,这意味着它们等待用户单击或点击,然后才真正执行任何操作。基于JavaScript的服务器通常会等待客户端请求通过网络到达,然后再做任何事情。

这种异步编程在JavaScript中很常见,本章介绍了三个重要的语言特性,这些特性有助于简化异步代码的使用。在ES6中,Promise是表示异步操作尚未获得结果的对象。关键字async和await是在ES2017中引入的,它提供了简化异步编程的新语法,它允许您将基于Promise的代码构造为与同步的代码一样。最后,在ES2018中引入了异步迭代器和for/await循环,允许您使用看似同步的简单循环处理异步事件流。

具有讽刺意味的是,尽管JavaScript为处理异步代码提供了这些强大的功能,但核心语言中本身不包含异步特性。因此,为了描述Promise、async、await和for/await,我们将首先浏览一下客户端和服务器端JavaScript,解释web浏览器和Node的一些异步特性。(您可以在第15章和第16章中了解有关客户端和服务器端JavaScript的更多信息。)

13.1 使用回调进行异步编程

在最底层,JavaScript中的异步编程是通过回调完成的。回调是一个函数,你编写它,然后传递给其他函数。当某个条件满足或某个(异步)事件发生时,另一个函数就会调用(“回调”)您的函数。对您提供的回调函数的调用会通知您某个条件满足或某个事件发生,有时调用将包含提供附加详细信息的函数参数。通过一些具体的例子,这一点更容易理解,下面的小节将演示使用客户端JavaScript和Node的各种形式的基于回调的异步编程。

13.1.1 定时器

最简单的异步方式之一是,当您希望在经过一定时间后运行某些代码。正如我们在§11.10中看到的,您可以使用setTimeout()函数来完成此操作:

setTimeout(checkForUpdates, 60000);

setTimeout()的第一个参数是一个函数,第二个参数是以毫秒为单位的时间间隔。在前面的代码中,演示用的checkForUpdates()函数将在setTimeout()调用后的60000毫秒(1分钟)之后被调用。checkForUpdates()是程序可能定义的回调函数,而setTimeout()是您调用的函数,用于注册回调函数并指定在哪些异步条件下应该调用它。

setTimeout()调用指定的回调函数一次,不传递任何参数,然后忽略它。如果您正在编写一个真正检查更新的函数,您可能希望它重复运行。可以通过使用setInterval()而不是setTimeout()来完成此操作:

// 一分钟后调用checkForUpdates,之后每分钟再调用一次
let updateIntervalId = setInterval(checkForUpdates, 60000);

// setInterval()返回一个值,我们可以通过调用clearInterval()来停止重复调用。
//(类似地,setTimeout()返回一个可以传递给clearTimeout()的值)
function stopCheckingForUpdates() {
   
	clearInterval(updateIntervalId);
}

13.1.2 事件

客户端JavaScript程序几乎是统一的事件驱动程序:它们通常等待用户做某件事,然后对用户的操作做出响应,而不是运行某种预定的计算。当用户按下键盘上的键、移动鼠标、单击鼠标按钮或触摸触摸屏设备时,web浏览器将生成一个事件。事件驱动的JavaScript程序在指定的上下文中为指定类型的事件注册回调函数,当指定的事件发生时,web浏览器就会调用这些函数。这些回调函数称为事件处理程序或事件侦听器,并使用addEventListener()注册:

// 要求web浏览器返回一个对象,该对象表示与此CSS选择器匹配的HTML<button>元素
let okay = document.querySelector('#confirmUpdateDialog button.okay');

// 现在注册一个回调函数,当用户单击该按钮时将被调用。
okay.addEventListener('click', applyUpdate);

在本例中,applyUpdate()是一个假设的回调函数,我们假设它是在其他地方实现的。号召document.querySelector()返回表示网页中单个指定元素的对象。我们对该元素调用addEventListener()来注册回调。然后addEventListener()的第一个参数是一个字符串,它指定我们感兴趣的事件类型—在本例中是鼠标单击或触摸屏点击。如果用户单击或点击网页的特定元素,浏览器将调用applyUpdate()回调函数,传递一个包含事件详细信息(如时间和鼠标指针坐标)的对象。

13.1.3 网络事件

JavaScript编程中异步的另一个常见来源是网络请求。在浏览器中运行的JavaScript可以使用如下代码从web服务器获取数据:

function getCurrentVersionNumber(versionCallback) {
    // 注释回调参数
    // 向后端版本API发出脚本化的HTTP请求
    let request = new XMLHttpRequest();
    request.open("GET", "http://www.example.com/api/version");
    request.send();
    // 注册将在响应到达时调用的回调
    request.onload = function () {
   
        if (request.status === 200) {
   
            // 如果HTTP状态良好,则获取版本号并调用回调。
            let currentVersion = parseFloat(request.responseText);
            versionCallback(null, currentVersion);
        } else {
   
            // 否则向回调报告错误
            versionCallback(response.statusText, null);
        }
    };
    // 注册另一个将为网络错误调用的回调
    request.onerror = request.ontimeout = function (e) {
   
        versionCallback(e.type, null);
    };
}

客户端JavaScript代码可以使用XMLHttpRequest类和回调函数发出HTTP请求,并在服务器到达时异步处理服务器的响应。1这里定义的getCurrentVersionNumber()函数(我们可以想象它是由我们在§13.1.1中讨论的假设的checkForUpdates()函数使用的)发出一个HTTP请求并定义事件处理程序,当接收到服务器的响应或超时或其他错误导致请求失败时,将调用这些处理程序。

请注意,上面的代码示例没有像前面的示例那样调用addEventListener()。对于大多数web API(包括本API),可以通过对生成事件的对象调用addEventListener()来定义事件处理程序,并将感兴趣事件的名称与回调函数一起传递。但是,通常,也可以通过将单个事件侦听器直接分配给对象的属性来注册它。这就是我们在这个示例代码中所做的,将函数分配给onload、onerror和ontimeout属性。按照惯例,像这样的事件侦听器属性的名称总是以on开头。addEventListener()是更灵活的技术,因为它允许多个事件处理程序。但是,如果您确定没有其他代码需要为同一对象和事件类型注册侦听器,那么只需为回调设置适当的属性会更简单。

关于这个示例代码中的getCurrentVersionNumber()函数,另一件需要注意的事情是,由于它发出异步请求,它不能同步返回调用者感兴趣的值(当前版本号)。相反,调用者传递一个回调函数,当结果准备就绪或发生错误时调用该函数。在本例中,调用者提供一个回调函数,该函数需要两个参数。如果XMLHttpRequest工作正常,那么getCurrentVersionNumber()将调用第一个参数为空、版本号作为第二个参数的回调。或者,如果发生错误,则getCurrentVersionNumber()调用回调,第一个参数中包含错误详细信息,第二个参数中包含null。

13.1.4 Node中的回调和事件

Node.js的服务器端JavaScript环境是高度异步的,它定义了许多使用回调和事件的API。例如,读取文件内容的默认API是异步的,并在读取文件内容后调用回调函数:

const fs = require("fs"); // “fs”模块具有与文件系统相关的API
let options = {
    // 保存程序选项的对象
    // 默认选项在这里
};

// 读取配置文件,然后调用回调函数
fs.readFile("config.json", "utf-8", (err, text) => {
   
    if (err) {
   
        // 如果出现错误,则显示警告,但继续进行下一步
        console.warn("Could not read config file:", err);
    } else {
   
        // 否则,解析文件内容并分配给options对象
        Object.assign(options, JSON.parse(text));
    }
    // 不管是哪种情况,我们现在都可以开始运行程序了
    startProgram(options);
});

Node的fs.readFile()函数的最后一个参数是双参数回调。它异步读取指定的文件,然后调用回调。如果文件读取成功,它将文件内容作为第二个回调参数传递。如果有错误,它会将错误作为第一个回调参数传递。在本例中,我们将回调表示为箭头函数,这是这种简单操作的简洁自然的语法。

Node还定义了许多基于事件的API。下面的函数演示如何对Node中的URL的内容发出HTTP请求。它有两层用事件侦听器处理的异步代码。请注意,Node使用on()方法而不是addEventListener()注册事件侦听器:

const https = require("https");

// 读取URL的文本内容,并异步地将其传递给回调。
function getText(url, callback) {
   
    // 启动URL的http get请求
    const request = https.get(url);
    // 注册一个函数来处理“response”事件。
    request.on("response", response => {
   
        // response事件表示已收到响应头
        let httpStatus = response.statusCode;
        // 尚未收到HTTP响应的正文。
		// 因此,我们注册了更多的事件处理程序,以便在事件到达时调用。
        response.setEncoding("utf-8"); // 我们需要Unicode文本
        let body = ""; // 我们将在这里累加。

        // 此事件处理程序在正文块准备就绪时调用
        response.on("data", chunk => {
    body += chunk; });

        // 此事件处理程序在响应完成时调用
        response.on("end", () => {
   
            if (httpStatus === 200) {
    // 如果HTTP响应良好
                callback(null, body); // 将响应正文传递给回调
            } else {
    // 否则传递错误
                callback(httpStatus, null);
            }
        });
    });

    // 我们还为低级网络错误注册一个事件处理程序
    request.on("error", (err) => {
   
        callback(err, null);
    });
}

13.2 Promise

现在我们已经看到了客户端和服务器端JavaScript环境中回调和基于事件的异步编程的示例,我们可以介绍Promise了,一个旨在简化异步编程的核心语言特性。

Promise是表示异步计算结果的对象。这个结果可能已经准备好了,也可能还没有准备好,Promise API故意对此含糊其辞:无法同步获取Promise的值;只有在值准备好时,才能要求Promise调用回调函数。如果您正在定义一个异步API(如上一节中的getText()函数),但希望使其基于Promise,请忽略回调参数,而是返回Promise对象。然后调用者可以在这个Promise对象上注册一个或多个回调,当异步计算完成时将调用它们。

所以,在最简单的层面上,Promise只是处理回调的另一种方式。然而,使用它们也有实际的好处。基于回调的异步编程的一个真正的问题是,通常在回调内部的回调中结束回调,代码行高度缩进,难以阅读。Promise允许这种嵌套回调被重新表示为一个更线性的Promise链,它更易于阅读和推理。

回调的另一个问题是它们会使处理错误变得困难。如果异步函数(或异步调用的回调)抛出异常,则该异常无法传播回异步操作的发起方。这是关于异步编程的一个基本事实:它打破了异常处理。另一种方法是使用回调参数和返回值仔细地跟踪和传播错误,但这很乏味,而且很难纠正。这里的Promise通过标准化处理错误的方法和提供一种通过Promise链正确传播错误的方法来帮助您。

注意,Promise表示单个异步计算的未来结果。但是,它们不能用来表示重复的异步计算。例如,在本章后面,我们将编写一个基于Promise的setTimeout()函数的替代方法。但是我们不能用Promise来代替setInterval(),因为这个函数会反复调用回调函数,而这正是Promise不打算做的事情。类似地,我们可以使用Promise而不是XMLHttpRequest对象的“load”事件处理程序,因为该回调只被调用一次。但是我们通常不会使用Promise来代替html button对象的“click”事件处理程序,因为我们通常希望允许用户多次单击一个按钮。

以下各小节将:

  • 解释Promise术语并展示基本的Promise用法
  • 展示Promise是如何链接起来的
  • 演示如何创建自己的基于Promise的API

Promise一开始看起来很简单,事实上,Promise的基本用例是简单明了的。但是除了最简单的用例之外,它们可能会变得令人惊讶地混乱。Promise是异步编程的一个强大的习惯用法,但是您需要深入理解它们才能正确和自信地使用它们。不过,花点时间深入了解这一点是值得的,我敦促你们仔细研究这一章。

13.2.1 使用Promise

随着JavaScript核心语言Promises的出现,web浏览器已经开始实现基于Promise的API。在上一节中,我们实现了一个getText()函数,该函数发出一个异步HTTP请求,并将HTTP响应的主体作为字符串传递给指定的回调函数。想象一下这个函数的一个变体getJSON(),它将HTTP响应的主体解析为JSON,并返回一个Promise而不是接受回调参数。我们将在本章后面实现一个getJSON()函数,但现在,让我们看看如何使用这个返回Promise的实用函数:

getJSON(url).then(jsonData => {
   
	// 这是一个回调函数,当解析后的JSON值可用时,它将被异步调用。
});

getJSON()为您指定的URL启动一个异步HTTP请求,然后,当该请求处于挂起状态时,它返回一个Promise对象。Promise对象定义了一个then()实例方法。我们没有将回调函数直接传递给getJSON(),而是将其传递给then()方法。当HTTP响应到达时,该响应的主体被解析为JSON,解析后的结果值被传递给我们传递给then()的函数。

您可以将then()方法看作是回调注册方法,如addEventListener()方法,用于在客户端Java脚本中注册事件处理程序。如果多次调用Promise对象的then()方法,则在Promise的计算完成时将调用您指定的每个函数。

但是,与许多事件侦听器不同,Promise表示一个单独的计算,每个用then()注册的函数将只被调用一次。值得注意的是,传递给then()的函数是异步调用的,即使在调用then()时异步计算已经完成。

在简单的语法层次上,then()方法是Promise的独特特性,它的惯用用法是将.then()直接附加到返回Promise的函数调用中,而不需要将Promise对象分配给变量。

将返回Promise的函数和使用Promise结果的函数命名时使用动词也是惯用做法,这些习惯用法会使代码特别容易阅读:

// 假设您有这样一个函数来显示用户配置文件
function displayUserProfile(profile) {
    /* 省略实现 */ }

// 下面是如何使用带有Promise的函数。
// 注意这一行代码读起来就像一个英语句子:
getJSON("/api/user/profile").then(displayUserProfile);

Promise处理错误

异步操作,特别是那些涉及网络的操作,通常会以多种方式失败,必须编写健壮的代码来处理不可避免地发生的错误。

对于Promise,我们可以通过向then()方法传递第二个函数来实现:

getJSON("/api/user/profile").then(displayUserProfile, handleProfileError);

Promise表示在创建Promise对象之后发生的异步计算的未来结果。因为计算是在Promise对象返回给我们之后执行的,所以传统的计算无法返回值或抛出我们可以捕获的异常。传递给then()的函数提供了替代方法。当同步计算正常完成时,它只将结果返回给调用者。当基于Promise的异步计算正常完成时,它将其结果传递给作为then()的第一个参数的函数。

当同步计算中出现错误时,它会抛出一个异常,该异常会向上传播给调用堆栈,直到有catch子句来处理它为止。当一个异步计算运行时,它的调用者不再在堆栈上,所以如果出现问题,就不可能将异常返回给调用方。

相反,基于Promise的异步计算将异常(通常作为某种错误对象,尽管这不是必需的)传递给我们之前传递给then()的第二个函数。因此,在上面的代码中,如果getJSON()正常运行,它会将结果传递给displayUserProfile()。如果出现错误(用户未登录、服务器关闭、用户的internet连接断开、请求超时等),则getJSON()会将错误对象传递给handleProfileError()。

实际上,很少有两个函数传递给then()。在处理承诺时,有一种更好、更惯用的处理错误的方法。要理解它,首先考虑如果getJSON()正常完成,但displayUserProfile()中出现错误,会发生什么情况。当getJSON()返回时,该回调函数是异步调用的,因此它也是异步的,不能有意义地抛出异常(因为调用堆栈上没有代码来处理它)。

处理此代码中错误的惯用方法如下所示:

getJSON("/api/user/profile").then(displayUserProfile).catch(handleProfileError);

使用此代码,getJSON()的正常结果仍将传递给displayUserProfile(),但getJSON()或displayUserProfile()中的任何错误(包括displayUserProfile引发的任何异常)都将传递给handleProfileError()。catch()方法等于调用then()的一种简写形式,相当于第一个参数为null,指定的错误处理程序函数作为第二个参数。

在下一节讨论Promise链时,我们将对catch()和这个错误处理习惯用法有更多的了解。

Promise术语
在我们进一步讨论Promise之前,有必要停下来定义一些术语。当我们不编程时,当我们谈论人类的Promise时,我们会说Promise是“遵守”或“违背”。当讨论JavaScript Promise时,等价术语是“兑现(fulfilled)”和“拒绝(rejected)”。假设您调用了Promise的then()方法并向其传递了两个回调函数。我们说,如果第一个回调被调用,那么这个Promise已经兑现了。我们说,如果第二个回调被调用,那么这个Promise就被拒绝了。如果一个Promise既没有兑现也没有被拒绝,那么它就是待定的(pending)。一旦一个Promise兑现或被拒绝,我们就说它已经结束了。请注意,一个Promise永远不会既兑现又被拒绝。一个Promise一旦结束了就永远不会从兑现状态变成拒绝状态,反之亦然。

还记得我们在本节开始时是如何定义Promise的:“Promise是一个表示异步操作结果的对象。”重要的是要记住,Promise不仅仅是注册回调以在某些异步代码完成时运行的抽象方式,它们表示异步代码的结果。如果异步代码正常运行(并且兑现了Promise),那么结果本质上就是代码的返回值。如果异步代码不能正常完成(并且Promise被拒绝),那么结果是一个错误对象或代码在非异步时可能抛出的其他值。任何已经结束的Promise都有一个与之相关的值,而且这个值不会改变。如果兑现了Promise,则该值是一个返回值,该值将传递给注册为then()的第一个参数的任何回调函数。如果Promise被拒绝,那么该值就是某种类型的错误,它会传递给任何使用catch()注册的回调函数或作为then()的第二个参数。

我之所以要对Promise的术语进行精确的描述,是因为Promise也可以被解决(resolved)。很容易将这个已解决的状态与已兑现的状态或已结束的状态混淆,但两者都不完全相同。理解已解决的状态是深入理解Promise的关键之一,在我们讨论完下面的Promise链之后,我将再次讨论它。

13.2.2 Promise链

Promise最重要的好处之一是,它们提供了一种自然的方法,可以将异步操作序列表示为then()方法调用的线性链,而不必将每个操作嵌套在前一个调用的回调中。例如,这里有一个假设的Promise链:


                
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值