javascript 权威指南第7版_第七版 JavaScript 权威指南之异步

本文详细介绍了JavaScript中的异步编程,包括回调函数、定时器、事件、网络请求等。重点讲解了Promise的使用,包括基本概念、链式调用、错误处理,以及对比回调函数的优势。此外,还提到了async和await关键字的引入,使得异步编程更加简洁,并介绍了异步遍历和for/await循环。最后,探讨了如何创建和使用异步迭代器,提供了使用异步生成器实现定时任务的例子。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一些计算程序,比如科学模拟和机器学习模型,是计算密集型【compute-bound】的。它们不间断地运行直到得到结果。

大多数现实世界的程序,则是异步的。浏览器里的 JavaScript 程序是典型的事件驱动,意味着总是在等待处理用户的点击或者触摸事件。服务端的 JavaScript 则总是在等客户端的请求。

这节介绍使 JavaScript 能轻松处理异步的三个重要语言特征Promises: ECMAScript 6 引入,表示还没有得到异步操作结果的对象

关键字 async 和 await :ECMAScript 2017 引入, 提供了更简洁的语法

异步遍历和for/await 循环: ECMAScript 2018 引入,允许使用循环和异步流协作

13.1 Asynchronous Programming with Callbacks

最基本的异步是回调函数。

13.1.1 Timers

计时器比如setTimeout() 函数:

setTimeout(checkForUpdates, 60000);

如果希望多次调用则使用 setInterval() :

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

// setInterval() 的返回值可以被 clearInterval() 用来清除停止调用// 同理 setTimeout() 的返回值被 clearTimeout() 用来清除停止调用function stopCheckingForUpdates() {

clearInterval(updateIntervalId);

}

13.1.2 Events

JavaScript 程序非常广泛地应用事件驱动。不管是鼠标点击还是键盘事件或者触屏事件,事件驱动的 JavaScript 程序通过 addEventListener() 给特定类型的事件注册回调函数。

let okay = document.querySelector('#confirmUpdateDialog button.okay');

okay.addEventListener('click', applyUpdate);

13.1.3 Network Events

另一个常见是异步场景是网络请求:

function getCurrentVersionNumber(versionCallback) {

let request = new XMLHttpRequest();

request.open("GET", "http://www.example.com/api/version");

request.send();

request.onload = function() {

if (request.status === 200) {

let currentVersion = parseFloat(request.responseText);

versionCallback(null, currentVersion);

} else {

versionCallback(response.statusText, null);

}

};

request.onerror = request.ontimeout = function(e) {

versionCallback(e.type, null);

};

}

getCurrentVersionNumber() 方法这里发送一个 HTTP 请求,然后监听请求返回成功或者是超时等错误处理。

注意上面没有继续使用 addEventListener() 而是使用 onload,onerror, and ontimeout,前者更加灵活允许监听多个事件,如果你确定该对象不需要再监听其他事件,后者更加简单直接。

13.1.4 Callbacks and Events in Node

Node.js 服务端 JavaScript 环境深度使用异步并且许多 APIs 都使用回调和异步。

const fs = require("fs"); // filesystem-related APIslet options = {};

// 读一个配置文件fs.readFile("config.json", "utf-8", (err, text) => {

if (err) {

console.warn("Could not read config file:", err);

} else {

Object.assign(options, JSON.parse(text));

}

startProgram(options);

});

下面的函数展示了如何发送一个 HTTP 请求返回一个URL的内容,和上面使用addEventListener() 不同是这里使用 on() 方法 :

const https = require("https");

function getText(url, callback) {

request = https.get(url);

request.on("response", response => {

let httpStatus = response.statusCode;

response.setEncoding("utf-8");

let body = "";

response.on("data", chunk => { body += chunk; });

response.on("end", () => {

if (httpStatus === 200) {

callback(null, body);

} else {

callback(httpStatus, null);

}

});

});

request.on("error", (err) => {

callback(err, null);

});

}

13.2 Promises

Promise本质是一个代表异步计算结果的对象,这个计算结果可能出了或者还没有,Promise API 特意这么设计:没有同步的方法可返回 Promise 的值,只能通过 Promise 调用回调函数。

所以简单来说 Promise 是另一种和回调函数工作的方式。回调函数的缺点之一是回调地狱,另一个是处理错误困难,Promises 用更线性的调用方式和标准化流程解决了这些问题。

Promises 代表着未来的单个异步计算结果,不能表示多个重复的计算结果。后面我们会介绍结合使用 Promises 来替换 setTimeout() , 但是我们不能用 Promises 来替代setInterval()因为后者重复多次调用回调。

同样我们也可以用 Promise 来替换XMLHttpRequest对象上的 “load” 事件,但是我们一般不会用 Promise 来替换HTML 按钮对象上的 “click” 事件, 因为按钮一般允许按多次。

接下来会的内容:解释 Promise 术语 ,展示 Promise 基本使用

展示如何链式反应调用 promises

展示如何创建自定义的 Promise-based APIs.

⚠️Promises 乍看之下简单明了,但是在一些非简单使用的场景可以非常的复杂。推荐你仔细阅读本篇以深入了解,以便能正确和自信地使用。

13.2.1 Using Promises

随着JavaScript语言原生支持 Promises ,浏览器也开始实现 Promise-based APIs,之前我们实现了一个 getText() 方法,稍微修改一下写一个 getJSON() 方法

getJSON(url).then(jsonData => {

});

getJSON() 发起一个 HTTP 请求然后返回 Promise 对象,这个对象定义了一个then() 实例方法。上面的是直接把回调函数传入 getText() 方法,这里我们则是把它传到 then() 方法里。

你可以把 then() 方法想象成回调函数注册处理中心,就像 addEventListener() 方法。如果你多次调用 then() 方法,那么每个都会在上一个异步计算结束完成后再执行。

这个和事件监听不同的是,Promise 仅代表单一计算, then() 方法是Promises里面最显著的特征。

function displayUserProfile(profile) { /* implementation omitted */ }

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

HANDLING ERRORS WITH PROMISES

如果异步请求出现错误,可以通过给 then() 方法传递第二个参数处理错误

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

不过在实际中更好的方式是通过 catch 方法来处理错误。想象一下,如果getJSON()调用成功,问题是出在displayUserProfile()方法里的这种情况,以下的方式更通顺

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

13.2.2 Chaining Promises

另一个 Promises 的重要特征是它可以很自然地链式调用。

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

在上面我们写过了用 XMLHttpRequest 对象来发送请求,这个蹩脚的对象在新的API里面有了更简洁的替代品 Promise-based Fetch API,使用更为简洁

fetch("/api/user/profile").then(response => {

if (response.ok &&

response.headers.get("Content-Type") === "application/json") {

}

});

返回的 response 对象 有许多属性包括状态和头部等,同时也有很多方法比如text()和json(),因为返回的 response 对象还不是你真正想要的数据,要拿到你想要的数据还要处理一层 response.json()

fetch("/api/user/profile")

.then(response => {

response.json()

.then(profile => {

displayUserProfile(profile);

});

});

上面的嵌套的写法也是不推荐的,更推荐下面

fetch("/api/user/profile")

.then(response => {

return response.json();

})

.then(profile => {

displayUserProfile(profile);

});

简化一下,结构就是

fetch().then().then()

fetch(theURL) // task 1; returns promise 1 .then(callback1) // task 2; returns promise 2 .then(callback2); // task 3; returns promise 3

13.2.3 Resolving Promises

在上面的例子里我们看到了三个Promises,其实还有第四个 Promise,请求返回的response需要再调用 response.json() 方法处理数据,这个处理方法返回第四个 Promise,下面是一个展开过程

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 1let p2 = p1.then(c1); // promise 2, task 2let p3 = p2.then(c2); // promise 3, task 3

这中间的过程是 Javascript 中最棘手的部分之一,可能需要多读几遍:

当 p1完成时,开始调用 c1,task 2开始执行;

当 p2完成时,开始调用 c2,task3开始执行;

但是这并不意味着当 c1 返回值时,task 2必须已经完成了。

现在来讨论最重要的细节 We are now ready to discuss the final detail that you need to understand to really master Promises. When you pass a callback c to the then() method, then() returns a Promise p and arranges to asynchronously invoke c at some later time. The callback performs some computation and returns a value v. When the callback returns, p is resolved with the value v. When a Promise is resolved with a value that is not itself a Promise, it is immediately fulfilled with that value. So if c returns a non-Promise, that return value becomes the value of p, p is fulfilled and we are done. But if the return value v is itself a Promise, then p is resolved but not yet fulfilled. At this stage p cannot settle until the Promise v settles. If v is fulfilled, then p will be fulfilled to the same value. If v is rejected, the p will be rejected for the same reason. This is what the “resolved” state of a Promise means: the promise has become associated with, or “locked onto” another Promise. We don’t know yet whether p will be fulfilled or rejected, but our callback c no longer has any control over that. p is “resolved” in the sense that its fate now depends entirely on what happens to Promise v.

Let’s bring this back to our URL fetching example. When c1 returns p4, p2 is resolved. But being resolved is not the same as being fulfilled, so task 3 does not begin yet. When the full body of the HTTP response becomes available, then the .json() method can parse it and use that parsed value to fulfill p4. When p4 is fulfilled, p2 is automatically fulfilled as well, with the same parsed JSON value. At this point, the parsed JSON object is passed to c2 and task 3 begins.

This can be one of the trickiest parts of JavaScript to understand, and you may need to read this section more than once. Example 13-1 presents the process in visual form and may help to clarify it for you.

13.2.4 More on Promises and Errors

其实 .catch() 方法就是一个简写,展开是调用 .then() 以 null 为第一个参数以回调函数为第二个参数,下面两者相同

p.then(null, c);

p.catch(c);

在 ECMAScript 2018, Promise 对象定义了一个 .finally() 方法 类似于 try/catch/finally 的用法。

看一个实际中可能会用到的错误处理:

fetch("/api/user/profile")

.then(response => {

if (!response.ok) {

return null;

}

let type = response.headers.get("content-type");

if (type !== "application/json") {

throw new TypeError(`Expected JSON, got${type}`);

}

return response.json();

})

.then(profile => {

if (profile) {

displayUserProfile(profile);

}

else {

displayLoggedOutProfilePage();

}

})

.catch(e => {

if (e instanceof NetworkError) {

displayErrorMessage("检查网络连接");

} else if (e instanceof TypeError) {

displayErrorMessage("Something is wrong with our server!");

} else {

console.error(e);

}

});

上面例子第一个可能抛错的是 fetch()请求本身,可能是 NetworkError 网络错误。

其次是请求本身返回404等错误,另一个错误是Content-Type 头部设置错误。

值得一提的是,如果某个错误不影响后续代码的执行,可以在链式调用中间插入catch

startAsyncOperation()

.then(doStageTwo)

.catch(recoverFromStageTwoError)

.then(doStageThree)

.then(doStageFour)

.catch(logStageThreeAndFourErrors);

有时候在一些复杂的网络环境,错误可以出现的比较随机,有时候修复方法就是重新再请求一次,比如你正在向数据库发送查询请求

queryDatabase()

.then(displayTable)

.catch(displayDatabaseError);

假设网络的负载问题会导致大约 1% 的请求会出现问题,最简单的修复就是在.catch()里面再试一次:

queryDatabase()

.catch(e => wait(500).then(queryDatabase)) // On failure, wait and retry .then(displayTable)

.catch(displayDatabaseError);

如果失败的原因果真是猜想的那样,那添加的这一行可以把错误发生的概率从 1% 降到 .01%

13.2.5 Promises in Parallel

上面花了很长篇幅循序渐进地介绍异步调用,有时候我们想平行地执行多个异步操作,可以使用 Promise.all()

const urls = [ /* urls here */ ];

promises = urls.map(url => fetch(url).then(r => r.text()));

Promise.all(promises)

.then(bodies => { /* do something with the array of strings */ })

.catch(e => console.error(e));

Promise.all() 其实比想象中的还要灵活,输入可以是 Promise 对象也可以是 non-Promise 值。The Promise returned byPromise.all()rejects when any of the input Promises is rejected. This happens immediately upon the first rejection, and can happen while other input Promises are still pending.

ES2020Promise.allSettled() 在正常工作的时候表现和 Promise.all()一样,不同的是 never rejects the returned Promise and it does not fulfill that Promise until all of the input Promises have settled.

Promise.allSettled() 里每一个返回的对象都有 status 属性,可能值是 “fulfilled” 或“rejected”,如果是 “fulfilled” ,则有另一个属性 value;如果是“rejected”,则有另一个属性reason

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

又比如,你想一次允许多个 Promises 但是只关心第一个成功返回的值,可以使用 Promise.race() 【Or, if there are any non-Promise values in the input array, it simply returns the first of those】

13.2.6 Making Promises

It can be hard to understand the functions passed to a function passed to a constructor by just reading about it, but hopefully some examples will make this clear. Here’s how to write the Promise-basedwait()function that we used in various examples earlier in the chapter:

function wait(duration) {

return new Promise((resolve, reject) => {

if (duration < 0) {

reject(new Error("Time travel not yet implemented"));

}

setTimeout(resolve, duration);

});

}

13.2.7 Promises in Sequence

13.3 async and await

ECMAScript 2017 引入了两个关键字async 和 await,使得可以像写同步函数一样写异步函数。

13.3.1 await Expressions

await 关键字接收一个 promise 对象。This means that any code that usesawaitis itself asynchronous.

let response = await fetch("/api/user/profile");

let profile = await response.json();

13.3.2 async Functions

因为任何带有 await 的语句都是异步的,所以记住这条规则非常重要: 所有的 await 关键字使用都必须在 async开头的函数里面使用

async function getHighScore() {

let response = await fetch("/api/user/profile");

let profile = await response.json();

return profile.highScore;

}

13.3.3 Awaiting Multiple Promises

假如我们这么写 getJSON() 方法

async function getJSON(url) {

let response = await fetch(url);

let body = await response.json();

return body;

}

现在我们调用该方法

let value1 = await getJSON(url1);

let value2 = await getJSON(url2);

上述写法的问题是,getJSON(url2) 会在 getJSON(url1) 执行完成后才开始执行,而这是没有必要的,只要2 不依赖于1,那么就可以一起执行,我们使用 Promise.all()来改进

let [value1, value2] = await Promise.all([getJSON(url1), getJSON(url2)]);

13.3.4 Implementation Details

为了明白async 具体是如何工作的,可以追根溯源来看,假设你这么写一个 async 函数

async function f(x) { /* body */ }

其实它就相当于

function f(x) {

return new Promise(function(resolve, reject) {

try {

resolve((function(x) { /* body */ })(x));

}

catch(e) {

reject(e);

}

});

}

13.4 Asynchronous Iteration

ECMAScript 2018 给遍历异步函数提供了一个新方案

13.4.1 The for/await Loop

Node 12 makes its readable streams asynchronously iterable. This means you can read successive chunks of data from a stream with afor/awaitloop like this one:

const fs = require("fs");

async function parseFile(filename) {

let stream = fs.createReadStream(filename, { encoding: "utf-8"});

for await (let chunk of stream) {

parseChunk(chunk); // Assume parseChunk() is defined elsewhere }

}

13.4.2 Asynchronous Iterators

Asynchronous iterators are quite similar to regular iterators, but there are two important differences.

First, an asynchronously iterable object implements a method with the symbolic nameSymbol.asyncIteratorinstead ofSymbol.iterator.

Second, thenext()method of an asynchronous iterator returns a Promise that resolves to an iterator result object instead of returning an iterator result object directly.

13.4.3 Asynchronous Generators

Here is an example that shows how you might use an async generator and afor/awaitloop to repetitively run code at fixed intervals, using loop syntax instead of asetInterval()callback function:

function elapsedTime(ms) {

return new Promise(resolve => setTimeout(resolve, ms));

}

async function* clock(interval, max=Infinity) {

for(let count = 1; count <= max; count++) { // regular for loop await elapsedTime(interval); // wait for time to pass yield count; // yield the counter }

}

async function test() { // Async so we can use for/await for await (let tick of clock(300, 100)) { // Loop 100 times every 300ms console.log(tick);

}

}

13.4.4 Implementing Asynchronous Iterators

13.5 Summary

In this chapter, you have learned:Most real-world JavaScript programming is asynchronous.

Traditionally, asynchrony has been handled with events and callback functions. This can get complicated however because you can end up with multiple levels of callbacks nested inside other callbacks, and because it is difficult to do robust error handling.

Promises provide a new way of structuring callback functions. If used correctly (and unfortunately, Promises are easy to use incorrectly), they can convert asynchronous code that would have been nested into linear chains of then() calls where one asynchronous step of a computation follows another. Also, promises allow you to centralize your error handling code into a single catch() call at the end of a chain of then() calls.

The async and await keywords allow us to write asynchronous code that is Promise-based under the hood, but which looks like synchronous code. This makes the code easier to understand and reason about. If a function is declared async it will implicitly return a Promise. Inside an async function, you can await a Promise (or a function that returns a Promise), as if the Promise value was synchronously computed.

Objects that are asynchronously iterable can be used with a for/await loop. You can create asynchronously iterable objects by implementing a [Symbol.asyncIterator]() method or by invoking an async function * generator function. Asynchronous iterators provide an alternative to “data” events on streams in Node and can be used to represent a stream of user input events in client-side JavaScript.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值