mirror of
https://github.com/apachecn/eloquent-js-3e-zh.git
synced 2025-05-23 11:52:20 +00:00
11
This commit is contained in:
parent
ef450a8d38
commit
fbd5654589
315
11.md
315
11.md
@ -1,5 +1,13 @@
|
||||
# 十一、异步编程
|
||||
|
||||
> 原文:[Asynchronous Programming](http://eloquentjavascript.net/11_async.html)
|
||||
>
|
||||
> 译者:[飞龙](https://github.com/wizardforcel)
|
||||
>
|
||||
> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
|
||||
>
|
||||
> 自豪地采用[谷歌翻译](https://translate.google.cn/)
|
||||
|
||||
> 孰能浊以澄?静之徐清;
|
||||
>
|
||||
> 孰能安以久?动之徐生。
|
||||
@ -10,11 +18,11 @@
|
||||
|
||||
但是许多程序与处理器之外的东西交互。 例如,他们可能通过计算机网络进行通信或从硬盘请求数据 - 这比从内存获取数据要慢很多。
|
||||
|
||||
当发生这种事情时,让处理器处于闲置状态是可耻的 - 在此期间可以做一些其他工作。 某种程度上,它由您的操作系统处理,它将在多个正在运行的程序之间切换处理器。 但是,我们希望单个程序在等待网络请求时能做一些事情,这并没有什么帮助。
|
||||
当发生这种事情时,让处理器处于闲置状态是可耻的 - 在此期间可以做一些其他工作。 某种程度上,它由你的操作系统处理,它将在多个正在运行的程序之间切换处理器。 但是,我们希望单个程序在等待网络请求时能做一些事情,这并没有什么帮助。
|
||||
|
||||
## 异步
|
||||
|
||||
在同步编程模型中,一次只发生一件事。 当您调用执行长时间操作的函数时,它只会在操作完成时返回,并且可以返回结果。 这会在您执行操作的时候停止您的程序。
|
||||
在同步编程模型中,一次只发生一件事。 当你调用执行长时间操作的函数时,它只会在操作完成时返回,并且可以返回结果。 这会在你执行操作的时候停止你的程序。
|
||||
|
||||
异步模型允许同时发生多个事件。 当你开始一个动作时,你的程序会继续运行。 当动作结束时,程序会受到通知并访问结果(例如从磁盘读取的数据)。
|
||||
|
||||
@ -64,11 +72,11 @@ setTimeout(() => console.log("Tick"), 500);
|
||||
|
||||
等待通常不是一种非常重要的工作,但在做一些事情时,例如更新动画或检查某件事是否花费比给定时间更长的时间,可能很有用。
|
||||
|
||||
使用回调在一行中执行多个异步操作,意味着您必须不断传递新函数来处理操作之后的计算延续。
|
||||
使用回调在一行中执行多个异步操作,意味着你必须不断传递新函数来处理操作之后的计算延续。
|
||||
|
||||
大多数乌鸦鸟巢计算机都有一个长期的数据存储设备,其中的信息刻在小树枝上,以便以后可以检索。雕刻或查找一段数据需要一些时间,所以长期存储的接口是异步的,并使用回调函数。
|
||||
大多数乌鸦鸟巢计算机都有一个长期的数据存储器,其中的信息刻在小树枝上,以便以后可以检索。雕刻或查找一段数据需要一些时间,所以长期存储的接口是异步的,并使用回调函数。
|
||||
|
||||
存储设备按照名称存储 JSON 编码的数据片段。乌鸦可以存储它隐藏食物的地方的信息,其名称为`"food caches"`,它可以包含指向其他数据片段的名称数组,描述实际的缓存。为了在 Big Oak 鸟巢的存储设备中查找食物缓存,乌鸦可以运行这样的代码:
|
||||
存储器按照名称存储 JSON 编码的数据片段。乌鸦可以存储它隐藏食物的地方的信息,其名称为`"food caches"`,它可以包含指向其他数据片段的名称数组,描述实际的缓存。为了在 Big Oak 鸟巢的存储器中查找食物缓存,乌鸦可以运行这样的代码:
|
||||
|
||||
```js
|
||||
import {bigOak} from "./crow-tech";
|
||||
@ -83,7 +91,7 @@ bigOak.readStorage("food caches", caches => {
|
||||
|
||||
(所有绑定名称和字符串都已从乌鸦语翻译成英语。)
|
||||
|
||||
这种编程风格是可行的,但缩进级别随着每个异步操作而增加,因为您最终会在另一个函数中。 做更复杂的事情,比如同时运行多个动作,会变得有点笨拙。
|
||||
这种编程风格是可行的,但缩进级别随着每个异步操作而增加,因为你最终会在另一个函数中。 做更复杂的事情,比如同时运行多个动作,会变得有点笨拙。
|
||||
|
||||
乌鸦鸟巢计算机为使用请求-响应对进行通信而构建。 这意味着一个鸟巢向另一个鸟巢发送消息,然后它立即返回一个消息,确认收到,并可能包括对消息中提出的问题的回复。
|
||||
|
||||
@ -127,7 +135,7 @@ fifteen.then(value => console.log(`Got ${value}`));
|
||||
// → Got 15
|
||||
```
|
||||
|
||||
为了获得`Promise`的结果,可以使用它的`then`方法。 它注册了一个回调函数,当`Promise`解析并产生一个值时被调用。 您可以将多个回调添加到单个`Promise`中,即使在`Promise`解析(完成)后添加它们,它们也会被调用。
|
||||
为了获得`Promise`的结果,可以使用它的`then`方法。 它注册了一个回调函数,当`Promise`解析并产生一个值时被调用。 你可以将多个回调添加到单个`Promise`中,即使在`Promise`解析(完成)后添加它们,它们也会被调用。
|
||||
|
||||
但那不是`then`方法所做的一切。 它返回另一个`Promise`,它解析处理器函数返回的值,或者如果返回`Promise`,则等待该`Promise`,然后解析为结果。
|
||||
|
||||
@ -166,7 +174,7 @@ storage(bigOak, "enemies")
|
||||
|
||||
为了明确地处理这种拒绝,`Promise`有一个`catch`方法,用于注册一个处理器,当`Promise`被拒绝时被调用,类似于处理器处理正常解析的方式。 这也非常类似于`then`,因为它返回一个新的`Promise`,如果它正常解析,它将解析原始`Promise`的值,否则返回`catch`处理器的结果。 如果`catch`处理器抛出一个错误,新的`Promise`也被拒绝。
|
||||
|
||||
作为简写,`then`还接受拒绝处理器作为第二个参数,因此您可以在单个方法调用中,装配这两种的处理器。
|
||||
作为简写,`then`还接受拒绝处理器作为第二个参数,因此你可以在单个方法调用中,装配这两种的处理器。
|
||||
|
||||
传递给`Promise`构造器的函数接收第二个参数,并与解析函数一起使用,它可以用来拒绝新的`Promise`。
|
||||
|
||||
@ -243,7 +251,7 @@ function requestType(name, handler) {
|
||||
## `Promise`的集合
|
||||
|
||||
|
||||
每台鸟巢计算机在其`neighbors`属性中,都保存了传输距离内的其他鸟巢的数组。 为了检查当前哪些可以访问,您可以编写一个函数,尝试向每个鸟巢发送一个`"ping"`请求(一个简单地请求响应的请求),并查看哪些返回了。
|
||||
每台鸟巢计算机在其`neighbors`属性中,都保存了传输距离内的其他鸟巢的数组。 为了检查当前哪些可以访问,你可以编写一个函数,尝试向每个鸟巢发送一个`"ping"`请求(一个简单地请求响应的请求),并查看哪些返回了。
|
||||
|
||||
在处理同时运行的`Promise`集合时,`Promise.all`函数可能很有用。 它返回一个`Promise`,等待数组中的所有`Promise`解析,然后解析这些`Promise`产生的值的数组(与原始数组的顺序相同)。 如果任何`Promise`被拒绝,`Promise.all`的结果本身被拒绝。
|
||||
|
||||
@ -400,3 +408,292 @@ routeRequest(bigOak, "Church Tower", "note",
|
||||
|
||||
计算机网络的一个显着特点是它们不可靠 - 建立在它们之上的抽象可以提供帮助,但是不能抽象出网络故障。所以网络编程通常关于预测和处理故障。
|
||||
|
||||
## `async`函数
|
||||
|
||||
为了存储重要信息,据了解乌鸦在鸟巢中复制它。 这样,当一只鹰摧毁一个鸟巢时,信息不会丢失。
|
||||
|
||||
为了检索它自己的存储器中没有的信息,鸟巢计算机可能会询问网络中其他随机鸟巢,直到找到一个鸟巢计算机。
|
||||
|
||||
```js
|
||||
requestType("storage", (nest, name) => storage(nest, name));
|
||||
|
||||
function findInStorage(nest, name) {
|
||||
return storage(nest, name).then(found => {
|
||||
if (found != null) return found;
|
||||
else return findInRemoteStorage(nest, name);
|
||||
});
|
||||
}
|
||||
|
||||
function network(nest) {
|
||||
return Array.from(nest.state.connections.keys());
|
||||
}
|
||||
|
||||
function findInRemoteStorage(nest, name) {
|
||||
let sources = network(nest).filter(n => n != nest.name);
|
||||
function next() {
|
||||
if (sources.length == 0) {
|
||||
return Promise.reject(new Error("Not found"));
|
||||
} else {
|
||||
let source = sources[Math.floor(Math.random() *
|
||||
sources.length)];
|
||||
sources = sources.filter(n => n != source);
|
||||
return routeRequest(nest, source, "storage", name)
|
||||
.then(value => value != null ? value : next(),
|
||||
next);
|
||||
}
|
||||
}
|
||||
return next();
|
||||
}
|
||||
```
|
||||
|
||||
因为`connections `是一个`Map`,`Object.keys`不起作用。 它有一个`key`方法,但是它返回一个迭代器而不是数组。 可以使用`Array.from`函数将迭代器(或可迭代对象)转换为数组。
|
||||
|
||||
即使使用`Promise`,这是一些相当笨拙的代码。 多个异步操作以不清晰的方式链接在一起。 我们再次需要一个递归函数(`next`)来建模鸟巢上的遍历。
|
||||
|
||||
代码实际上做的事情是完全线性的 - 在开始下一个动作之前,它总是等待先前的动作完成。 在同步编程模型中,表达会更简单。
|
||||
|
||||
好消息是 JavaScript 允许你编写伪同步代码。 异步函数是一种隐式返回`Promise`的函数,它可以在其主体中,以看起来同步的方式等待其他`Promise`。
|
||||
|
||||
我们可以像这样重写`findInStorage`:
|
||||
|
||||
```js
|
||||
async function findInStorage(nest, name) {
|
||||
let local = await storage(nest, name);
|
||||
if (local != null) return local;
|
||||
|
||||
let sources = network(nest).filter(n => n != nest.name);
|
||||
while (sources.length > 0) {
|
||||
let source = sources[Math.floor(Math.random() *
|
||||
sources.length)];
|
||||
sources = sources.filter(n => n != source);
|
||||
try {
|
||||
let found = await routeRequest(nest, source, "storage",
|
||||
name);
|
||||
if (found != null) return found;
|
||||
} catch (_) {}
|
||||
}
|
||||
throw new Error("Not found");
|
||||
}
|
||||
```
|
||||
|
||||
异步函数由`function`关键字之前的`async`标记。 方法也可以通过在名称前面编写`async`来做成异步的。 当调用这样的函数或方法时,它返回一个`Promise`。 只要主体返回了某些东西,这个`Promise`就解析了。 如果它抛出异常,则`Promise`被拒绝。
|
||||
|
||||
```js
|
||||
findInStorage(bigOak, "events on 2017-12-21")
|
||||
.then(console.log);
|
||||
```
|
||||
|
||||
在异步函数内部,`await`这个词可以放在表达式的前面,等待解`Promise`被解析,然后才能继续执行函数。
|
||||
|
||||
这样的函数不再像常规的 JavaScript 函数一样,从头到尾运行。 相反,它可以在有任何带有`await`的地方冻结,并在稍后恢复。
|
||||
|
||||
对于有意义的异步代码,这种标记通常比直接使用`Promise`更方便。即使你需要做一些不适合同步模型的东西,比如同时执行多个动作,也很容易将`await`和直接使用`Promise`结合起来。
|
||||
|
||||
## 生成器
|
||||
|
||||
函数暂停然后再次恢复的能力,不是异步函数所独有的。 JavaScript 也有一个称为生成器函数的特性。 这些都是相似的,但没有`Promise`。
|
||||
|
||||
当用`function*`定义一个函数(在函数后面加星号)时,它就成为一个生成器。 当你调用一个生成器时,它将返回一个迭代器,我们在第 6 章已经看到了它。
|
||||
|
||||
```js
|
||||
function* powers(n) {
|
||||
for (let current = n;; current *= n) {
|
||||
yield current;
|
||||
}
|
||||
}
|
||||
|
||||
for (let power of powers(3)) {
|
||||
if (power > 50) break;
|
||||
console.log(power);
|
||||
}
|
||||
// → 3
|
||||
// → 9
|
||||
// → 27
|
||||
```
|
||||
|
||||
最初,当你调用`powers`时,函数在开头被冻结。 每次在迭代器上调用`next`时,函数都会运行,直到它碰到`yield`表达式,该表达式会暂停它,并使得产生的值成为由迭代器产生的下一个值。 当函数返回时(示例中的那个永远不会),迭代器就结束了。
|
||||
|
||||
使用生成器函数时,编写迭代器通常要容易得多。 可以用这个生成器编写`group`类的迭代器(来自第 6 章的练习):
|
||||
|
||||
```js
|
||||
Group.prototype[Symbol.iterator] = function*() {
|
||||
for (let i = 0; i < this.members.length; i++) {
|
||||
yield this.members[i];
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
不再需要创建一个对象来保存迭代状态 - 生成器每次`yield`时都会自动保存其本地状态。
|
||||
|
||||
这样的`yield`表达式可能仅仅直接出现在生成器函数本身中,而不是在你定义的内部函数中。 生成器在返回(`yield`)时保存的状态,只是它的本地环境和它`yield`的位置。
|
||||
|
||||
异步函数是一种特殊的生成器。 它在调用时会产生一个`Promise`,当它返回(完成)时被解析,并在抛出异常时被拒绝。 每当它`yield`(`await`)一个`Promise`时,该`Promise`的结果(值或抛出的异常)就是`await`表达式的结果。
|
||||
|
||||
## 事件循环
|
||||
|
||||
异步程序是逐片段执行的。 每个片段可能会启动一些操作,并调度代码在操作完成或失败时执行。 在这些片段之间,该程序处于空闲状态,等待下一个动作。
|
||||
|
||||
所以回调函数不会直接被调度它们的代码调用。 如果我从一个函数中调用`setTimeout`,那么在调用回调函数时该函数已经返回。 当回调返回时,控制权不会回到调度它的函数。
|
||||
|
||||
异步行为发生在它自己的空函数调用堆栈上。 这是没有`Promise`的情况下,在异步代码之间管理异常很难的原因之一。 由于每个回调函数都是以几乎为空的堆栈开始,因此当它们抛出一个异常时,你的`catch`处理程序不会在堆栈中。
|
||||
|
||||
```js
|
||||
try {
|
||||
setTimeout(() => {
|
||||
throw new Error("Woosh");
|
||||
}, 20);
|
||||
} catch (_) {
|
||||
// This will not run
|
||||
console.log("Caught!");
|
||||
}
|
||||
```
|
||||
|
||||
无论事件发生多么紧密(例如超时或传入请求),JavaScript 环境一次只能运行一个程序。 你可以把它看作在程序周围运行一个大循环,称为事件循环。 当没有什么可以做的时候,那个循环就会停止。 但随着事件来临,它们被添加到队列中,并且它们的代码被逐个执行。 由于没有两件事同时运行,运行缓慢的代码可能会延迟其他事件的处理。
|
||||
|
||||
这个例子设置了一个超时,但是之后占用时间,直到超时的预定时间点,导致超时延迟。
|
||||
|
||||
```js
|
||||
let start = Date.now();
|
||||
setTimeout(() => {
|
||||
console.log("Timeout ran at", Date.now() - start);
|
||||
}, 20);
|
||||
while (Date.now() < start + 50) {}
|
||||
console.log("Wasted time until", Date.now() - start);
|
||||
// → Wasted time until 50
|
||||
// → Timeout ran at 55
|
||||
```
|
||||
|
||||
`Promise`总是作为新事件来解析或拒绝。 即使已经解析了`Promise`,等待它会导致你的回调在当前脚本完成后运行,而不是立即执行。
|
||||
|
||||
```js
|
||||
Promise.resolve("Done").then(console.log);
|
||||
console.log("Me first!");
|
||||
// → Me first!
|
||||
// → Done
|
||||
```
|
||||
|
||||
在后面的章节中,我们将看到在事件循环中运行的,各种其他类型的事件。
|
||||
|
||||
## 异步的 bug
|
||||
|
||||
当你的程序同步运行时,除了那些程序本身所做的外,没有发生任何状态变化。 对于异步程序,这是不同的 - 它们在执行期间可能会有空白,这个时候其他代码可以运行。
|
||||
|
||||
我们来看一个例子。 我们乌鸦的爱好之一是计算整个村庄每年孵化的雏鸡数量。 鸟巢将这一数量存储在他们的存储器中。 下面的代码尝试枚举给定年份的所有鸟巢的计数。
|
||||
|
||||
```js
|
||||
function anyStorage(nest, source, name) {
|
||||
if (source == nest.name) return storage(nest, name);
|
||||
else return routeRequest(nest, source, "storage", name);
|
||||
}
|
||||
|
||||
async function chicks(nest, year) {
|
||||
let list = "";
|
||||
await Promise.all(network(nest).map(async name => {
|
||||
list += `${name}: ${
|
||||
await anyStorage(nest, name, `chicks in ${year}`)
|
||||
}\n`;
|
||||
}));
|
||||
return list;
|
||||
}
|
||||
```
|
||||
|
||||
`async name =>`部分展示了,通过将单词`async`放在它们前面,也可以使箭头函数变成异步的。
|
||||
|
||||
代码不会立即看上去有问题......它将异步箭头函数映射到鸟巢集合上,创建一组`Promise`,然后使用`Promise.all`,在返回它们构建的列表之前等待所有`Promise`。
|
||||
|
||||
但它有严重问题。 它总是只返回一行输出,列出响应最慢的鸟巢。
|
||||
|
||||
```js
|
||||
chicks(bigOak, 2017).then(console.log);
|
||||
```
|
||||
|
||||
你能解释为什么吗?
|
||||
|
||||
问题在于`+=`操作符,它在语句开始执行时接受`list`的当前值,然后当`await`结束时,将`list`绑定设为该值加上新增的字符串。
|
||||
|
||||
但是在语句开始执行的时间和它完成的时间之间存在一个异步间隔。 `map`表达式在任何内容添加到列表之前运行,因此每个`+ =`操作符都以一个空字符串开始,并在存储检索完成时结束,将`list`设置为单行列表 - 向空字符串添加那行的结果。
|
||||
|
||||
通过从映射的`Promise`中返回行,并对`Promise.all`的结果调用`join`,可以轻松避免这种情况,而不是通过更改绑定来构建列表。 像往常一样,计算新值比改变现有值的错误更少。
|
||||
|
||||
```js
|
||||
async function chicks(nest, year) {
|
||||
let lines = network(nest).map(async name => {
|
||||
return name + ": " +
|
||||
await anyStorage(nest, name, `chicks in ${year}`);
|
||||
});
|
||||
return (await Promise.all(lines)).join("\n");
|
||||
}
|
||||
```
|
||||
|
||||
像这样的错误很容易做出来,特别是在使用`await`时,你应该知道代码中的间隔在哪里出现。 JavaScript 的显式异步性(无论是通过回调,`Promise`还是`await`)的一个优点是,发现这些间隔相对容易。
|
||||
|
||||
## 总结
|
||||
|
||||
异步编程可以表示等待长时间运行的动作,而不需要在这些动作期间冻结程序。 JavaScript 环境通常使用回调函数来实现这种编程风格,这些函数在动作完成时被调用。 事件循环调度这样的回调,使其在适当的时候依次被调用,以便它们的执行不会重叠。
|
||||
|
||||
`Promise`和异步函数使异步编程更容易。`Promise`是一个对象,代表将来可能完成的操作。并且,异步函数使你可以像编写同步程序一样编写异步程序。
|
||||
|
||||
## 练习
|
||||
|
||||
### 跟踪手术刀
|
||||
|
||||
村里的乌鸦拥有一把老式的手术刀,他们偶尔会用于特殊的任务 - 比如说,切开纱门或包装。 为了能够快速追踪到手术刀,每次将手术刀移动到另一个鸟巢时,将一个条目添加到拥有它和拿走它的鸟巢的存储器中,名称为`"scalpel"`,值为新的位置。
|
||||
|
||||
这意味着找到手术刀就是跟踪存储器条目的痕迹,直到你发现一个鸟巢指向它本身。
|
||||
|
||||
编写一个异步函数`locateScalpel`,它从它运行的鸟巢开始。 你可以使用之前定义的`anyStorage`函数,来访问任意鸟巢中的存储器。 手术刀已经移动了很长时间,你可能会认为每个鸟巢的数据存储器中都有一个`"scalpel"`条目。
|
||||
|
||||
接下来,再次写入相同的函数,而不使用`async`和`await`。
|
||||
|
||||
在两个版本中,请求故障是否正确显示为拒绝? 如何实现?
|
||||
|
||||
```js
|
||||
async function locateScalpel(nest) {
|
||||
// Your code here.
|
||||
}
|
||||
|
||||
function locateScalpel2(nest) {
|
||||
// Your code here.
|
||||
}
|
||||
|
||||
locateScalpel(bigOak).then(console.log);
|
||||
// → Butcher Shop
|
||||
```
|
||||
|
||||
### 构建`Promise.all`
|
||||
|
||||
给定`Promise`的数组,`Promise.all`返回一个`Promise`,等待数组中的所有`Promise`完成。 然后它成功,产生结果值的数组。 如果数组中的一个`Promise`失败,这个`Promise`也失败,故障原因来自那个失败的`Promise`。
|
||||
|
||||
自己实现一个名为`Promise_all`的常规函数。
|
||||
|
||||
请记住,在`Promise`成功或失败后,它不能再次成功或失败,并且解析它的函数的进一步调用将被忽略。 这可以简化你处理`Promise`的故障的方式。
|
||||
|
||||
```js
|
||||
function Promise_all(promises) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Your code here.
|
||||
});
|
||||
}
|
||||
|
||||
// Test code.
|
||||
Promise_all([]).then(array => {
|
||||
console.log("This should be []:", array);
|
||||
});
|
||||
function soon(val) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => resolve(val), Math.random() * 500);
|
||||
});
|
||||
}
|
||||
Promise_all([soon(1), soon(2), soon(3)]).then(array => {
|
||||
console.log("This should be [1, 2, 3]:", array);
|
||||
});
|
||||
Promise_all([soon(1), Promise.reject("X"), soon(3)])
|
||||
.then(array => {
|
||||
console.log("We should not get here");
|
||||
})
|
||||
.catch(error => {
|
||||
if (error != "X") {
|
||||
console.log("Unexpected failure:", error);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
Loading…
x
Reference in New Issue
Block a user