# 十一、异步编程

> 原文：[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/)

> 孰能浊以澄？静之徐清；
> 
> 孰能安以久？动之徐生。
> 
> 老子，《道德经》

计算机的核心部分称为处理器，它执行构成我们程序的各个步骤。 到目前为止，我们看到的程序都是让处理器忙碌，直到他们完成工作。 处理数字的循环之类的东西，几乎完全取决于处理器的速度。

但是许多程序与处理器之外的东西交互。 例如，他们可能通过计算机网络进行通信或从硬盘请求数据 - 这比从内存获取数据要慢很多。

当发生这种事情时，让处理器处于闲置状态是可耻的 - 在此期间可以做一些其他工作。 某种程度上，它由你的操作系统处理，它将在多个正在运行的程序之间切换处理器。 但是，我们希望单个程序在等待网络请求时能做一些事情，这并没有什么帮助。

## 异步

在同步编程模型中，一次只发生一件事。 当你调用执行长时间操作的函数时，它只会在操作完成时返回，并且可以返回结果。 这会在你执行操作的时候停止你的程序。

异步模型允许同时发生多个事件。 当你开始一个动作时，你的程序会继续运行。 当动作结束时，程序会收到通知并访问结果（例如从磁盘读取的数据）。

我们可以使用一个小例子来比较同步和异步编程：一个从网络获取两个资源然后合并结果的程序。

在同步环境中，只有在请求函数完成工作后，它才返回，执行此任务的最简单方法是逐个创建请求。 这有一个缺点，仅当第一个请求完成时，第二个请求才会启动。 所花费的总时间至少是两个响应时间的总和。

在同步系统中解决这个问题的方法是启动额外的控制线程。 线程是另一个正在运行的程序，它的执行可能会交叉在操作系统与其他程序当中 - 因为大多数现代计算机都包含多个处理器，所以多个线程甚至可能同时运行在不同的处理器上。 第二个线程可以启动第二个请求，然后两个线程等待它们的结果返回，之后它们重新同步来组合它们的结果。

在下图中，粗线表示程序正常花费运行的时间，细线表示等待网络所花费的时间。 在同步模型中，网络所花费的时间是给定控制线程的时间线的一部分。 在异步模型中，从概念上讲，启动网络操作会导致时间轴中出现分裂。 启动该动作的程序将继续运行，并且该动作将与其同时发生，并在程序结束时通知该程序。

![](img/11-1.svg)

另一种描述差异的方式是，等待动作完成在同步模型中是隐式的，而在异步模型中，在我们的控制之下，它是显式的。

异步性是个双刃剑。 它可以生成不适合直线控制模型的程序，但它也可以使直线控制的程序更加笨拙。 本章后面我们会看到一些方法来解决这种笨拙。

两种重要的 JavaScript 编程平台（浏览器和 Node.js）都可能需要一段时间的异步操作，而不是依赖线程。 由于使用线程进行编程非常困难（理解程序在同时执行多个事情时所做的事情要困难得多），这通常被认为是一件好事。

## 乌鸦科技

大多数人都知道乌鸦非常聪明。 他们可以使用工具，提前计划，记住事情，甚至可以互相沟通这些事情。

大多数人不知道的是，他们能够做一些事情，并且对我们隐藏得很好。我听说一个有声望的（但也有点古怪的）专家 corvids 认为，乌鸦技术并不落后于人类的技术，并且正在迎头赶上。

例如，许多乌鸦文明能够构建计算设备。 这些并不是电子的，就像人类的计算设备一样，但是它们操作微小昆虫的行动，这种昆虫是与白蚁密切相关的物种，它与乌鸦形成了共生关系。 鸟类为它们提供食物，对之对应，昆虫建立并操作复杂的殖民地，在其内部的生物的帮助下进行计算。

这些殖民地通常位于大而久远的鸟巢中。 鸟类和昆虫一起工作，建立一个球形粘土结构的网络，隐藏在巢的树枝之间，昆虫在其中生活和工作。

为了与其他设备通信，这些机器使用光信号。 鸟类在特殊的通讯茎中嵌入反光材料片段，昆虫校准这些反光材料将光线反射到另一个鸟巢，将数据编码为一系列快速闪光。 这意味着只有具有完整视觉连接的巢才能沟通。

我们的朋友 corvid 专家已经绘制了 Rhône 河畔的 Hières-sur-Amby 村的乌鸦鸟巢网络。 这张地图显示了鸟巢及其连接。

在一个令人震惊的趋同进化的例子中，乌鸦计算机运行 JavaScript。 在本章中，我们将为他们编写一些基本的网络函数。

![](img/11-2.png)

## 回调

异步编程的一种方法是使执行慢动作的函数接受额外的参数，即回调函数。动作开始，当它结束时，使用结果调用回调函数。

例如，在 Node.js 和浏览器中都可用的`setTimeout`函数，等待给定的毫秒数（一秒为一千毫秒），然后调用一个函数。

```js
setTimeout(() => console.log("Tick"), 500);
```

等待通常不是一种非常重要的工作，但在做一些事情时，例如更新动画或检查某件事是否花费比给定时间更长的时间，可能很有用。

使用回调在一行中执行多个异步操作，意味着你必须不断传递新函数来处理操作之后的计算延续。

大多数乌鸦鸟巢计算机都有一个长期的数据存储器，其中的信息刻在小树枝上，以便以后可以检索。雕刻或查找一段数据需要一些时间，所以长期存储的接口是异步的，并使用回调函数。

存储器按照名称存储 JSON 编码的数据片段。乌鸦可以存储它隐藏食物的地方的信息，其名称为`"food caches"`，它可以包含指向其他数据片段的名称数组，描述实际的缓存。为了在 Big Oak 鸟巢的存储器中查找食物缓存，乌鸦可以运行这样的代码：

```js
import {bigOak} from "./crow-tech";

bigOak.readStorage("food caches", caches => {
  let firstCache = caches[0];
  bigOak.readStorage(firstCache, info => {
    console.log(info);
  });
});
```

（所有绑定名称和字符串都已从乌鸦语翻译成英语。）

这种编程风格是可行的，但缩进级别随着每个异步操作而增加，因为你最终会在另一个函数中。 做更复杂的事情，比如同时运行多个动作，会变得有点笨拙。

乌鸦鸟巢计算机为使用请求-响应对进行通信而构建。 这意味着一个鸟巢向另一个鸟巢发送消息，然后它立即返回一个消息，确认收到，并可能包括对消息中提出的问题的回复。

每条消息都标有一个类型，它决定了它的处理方式。 我们的代码可以为特定的请求类型定义处理器，并且当这样的请求到达时，调用处理器来产生响应。

`"./crow-tech"`模块所导出的接口为通信提供基于回调的函数。 鸟巢拥有`send`方法来发送请求。 它接受目标鸟巢的名称，请求的类型和请求的内容作为它的前三个参数，以及一个用于调用的函数，作为其第四个和最后一个参数，当响应到达时调用。

```js
bigOak.send("Cow Pasture", "note", "Let's caw loudly at 7PM",
            () => console.log("Note delivered."));
```

但为了使鸟巢能够接收该请求，我们首先必须定义名为`"note"`的请求类型。 处理请求的代码不仅要在这台鸟巢计算机上运行，而且还要运行在所有可以接收此类消息的鸟巢上。 我们只假定一只乌鸦飞过去，并将我们的处理器代码安装在所有的鸟巢中。

```js
import {defineRequestType} from "./crow-tech";

defineRequestType("note", (nest, content, source, done) => {
  console.log(`${nest.name} received note: ${content}`);
  done();
});
```

`defineRequestType`函数定义了一种新的请求类型。该示例添加了对`"note"`请求的支持，它只是向给定的鸟巢发送备注。我们的实现调用`console.log`，以便我们可以验证请求到达。鸟巢有`name`属性，保存他们的名字。

给`handler`的第四个参数done，是一个回调函数，它在完成请求时必须调用。如果我们使用了处理器的返回值作为响应值，那么这意味着请求处理器本身不能执行异步操作。执行异步工作的函数通常会在完成工作之前返回，安排回调函数在完成时调用。所以我们需要一些异步机制 - 在这种情况下是另一个回调函数 - 在响应可用时发出信号。

某种程度上，异步性是传染的。任何调用异步的函数的函数，本身都必须是异步的，使用回调或类似的机制来传递其结果。调用回调函数比简单地返回一个值更容易出错，所以以这种方式构建程序的较大部分并不是很好。

## `Promise`

当这些概念可以用值表示时，处理抽象概念通常更容易。 在异步操作的情况下，你不需要安排将来某个时候调用的函数，而是返回一个代表这个未来事件的对象。

这是标准类`Promise`的用途。 `Promise`是一种异步行为，可以在某个时刻完成并产生一个值。 当值可用时，它能够通知任何感兴趣的人。

创建`Promise`的最简单方法是调用`Promise.resolve`。 这个函数确保你给它的值包含在一个`Promise`中。 如果它已经是`Promise`，那么仅仅返回它 - 否则，你会得到一个新的`Promise`，并使用你的值立即结束。

```js
let fifteen = Promise.resolve(15);
fifteen.then(value => console.log(`Got ${value}`));
// → Got 15
```

为了获得`Promise`的结果，可以使用它的`then`方法。 它注册了一个回调函数，当`Promise`解析并产生一个值时被调用。 你可以将多个回调添加到单个`Promise`中，即使在`Promise`解析（完成）后添加它们，它们也会被调用。

但那不是`then`方法所做的一切。 它返回另一个`Promise`，它解析处理器函数返回的值，或者如果返回`Promise`，则等待该`Promise`，然后解析为结果。

将`Promise`视为一种手段，将值转化为异步现实，是有用处的。 一个正常的值就在那里。promised 的值是未来可能存在或可能出现的值。 根据`Promise`定义的计算对这些包装值起作用，并在值可用时异步执行。

为了创建`Promise`，你可以将`Promise`用作构造器。 它有一个有点奇怪的接口 - 构造器接受一个函数作为参数，它会立即调用，并传递一个函数来解析这个`Promise`。 它以这种方式工作，而不是使用`resolve`方法，这样只有创建`Promise`的代码才能解析它。

这就是为`readStorage`函数创建基于`Promise`的接口的方式。

```js
function storage(nest, name) {
  return new Promise(resolve => {
    nest.readStorage(name, result => resolve(result));
  });
}

storage(bigOak, "enemies")
  .then(value => console.log("Got", value));
```

这个异步函数返回一个有意义的值。 这是`Promise`的主要优点 - 它们简化了异步函数的使用。 基于`Promise`的函数不需要传递回调，而是类似于常规函数：它们将输入作为参数并返回它们的输出。 唯一的区别是输出可能还不可用。

## 故障

> 译者注：这段如果有配套代码会更容易理解，但是没有，所以凑合看吧。

常规的 JavaScript 计算可能会因抛出异常而失败。 异步计算经常需要类似的东西。 网络请求可能会失败，或者作为异步计算的一部分的某些代码，可能会引发异常。

异步编程的回调风格中最紧迫的问题之一是，确保将故障正确地报告给回调函数，是非常困难的。

一个广泛使用的约定是，回调函数的第一个参数用于指示操作失败，第二个参数包含操作成功时生成的值。 这种回调函数必须始终检查它们是否收到异常，并确保它们引起的任何问题，包括它们调用的函数所抛出的异常，都会被捕获并提供给正确的函数。

`Promise`使这更容易。可以解决它们（操作成功完成）或拒绝（故障）。只有在操作成功时，才会调用解析处理器（使用`then`注册），并且拒绝会自动传播给由`then`返回的新`Promise`。当一个处理器抛出一个异常时，这会自动使`then`调用产生的`Promise`被拒绝。因此，如果异步操作链中的任何元素失败，则整个链的结果被标记为拒绝，并且不会调用失败位置之后的任何常规处理器。

就像`Promise`的解析提供了一个值，拒绝它也提供了一个值，通常称为拒绝的原因。当处理器中的异常导致拒绝时，异常值将用作原因。同样，当处理器返回被拒绝的`Promise`时，拒绝流入下一个`Promise`。`Promise.reject`函数会创建一个新的，立即被拒绝的`Promise`。

为了明确地处理这种拒绝，`Promise`有一个`catch`方法，用于注册一个处理器，当`Promise`被拒绝时被调用，类似于处理器处理正常解析的方式。 这也非常类似于`then`，因为它返回一个新的`Promise`，如果它正常解析，它将解析原始`Promise`的值，否则返回`catch`处理器的结果。 如果`catch`处理器抛出一个错误，新的`Promise`也被拒绝。

作为简写，`then`还接受拒绝处理器作为第二个参数，因此你可以在单个方法调用中，装配这两种的处理器。

传递给`Promise`构造器的函数接收第二个参数，并与解析函数一起使用，它可以用来拒绝新的`Promise`。

通过调用`then`和`catch`创建的`Promise`值的链条，可以看作异步值或失败沿着它移动的流水线。 由于这种链条通过注册处理器来创建，因此每个链条都有一个成功处理器或与其关联的拒绝处理器（或两者都有）。 不匹配结果类型（成功或失败）的处理器将被忽略。 但是那些匹配的对象被调用，并且它们的结果决定了下一次会出现什么样的值 -- 返回非`Promise`值时成功，当它抛出异常时拒绝，并且当它返回其中一个时是`Promise`的结果。

就像环境处理未捕获的异常一样，JavaScript 环境可以检测未处理`Promise`拒绝的时候，并将其报告为错误。

## 网络是困难的

偶尔，乌鸦的镜像系统没有足够的光线来传输信号，或者有些东西阻挡了信号的路径。 信号可能发送了，但从未收到。

事实上，这只会导致提供给`send`的回调永远不会被调用，这可能会导致程序停止，而不会注意到问题。 如果在没有得到回应的特定时间段内，请求会超时并报告故障，那就很好。

通常情况下，传输故障是随机事故，例如汽车的前灯会干扰光信号，只需重试请求就可以使其成功。 所以，当我们处理它时，让我们的请求函数在放弃之前自动重试发送请求几次。

而且，既然我们已经确定`Promise`是一件好事，我们也会让我们的请求函数返回一个`Promise`。 对于他们可以表达的内容，回调和`Promise`是等同的。 基于回调的函数可以打包，来公开基于`Promise`的接口，反之亦然。

即使请求及其响应已成功传递，响应也可能表明失败 - 例如，如果请求尝试使用未定义的请求类型或处理器，会引发错误。 为了支持这个，`send`和`defineRequestType`遵循前面提到的惯例，其中传递给回调的第一个参数是故障原因，如果有的话，第二个参数是实际结果。

这些可以由我们的包装翻译成`Promise`的解析和拒绝。

```js
class Timeout extends Error {}

function request(nest, target, type, content) {
  return new Promise((resolve, reject) => {
    let done = false;
    function attempt(n) {
      nest.send(target, type, content, (failed, value) => {
        done = true;
        if (failed) reject(failed);
        else resolve(value);
      });
      setTimeout(() => {
        if (done) return;
        else if (n < 3) attempt(n + 1);
        else reject(new Timeout("Timed out"));
      }, 250);
    }
    attempt(1);
  });
}
```

因为`Promise`只能解析（或拒绝）一次，所以这个是有效的。 第一次调用`resolve`或`reject`会决定`Promise`的结果，并且任何进一步的调用（例如请求结束后到达的超时，或在另一个请求结束后返回的请求）都将被忽略。

为了构建异步循环，对于重试，我们需要使用递归函数 - 常规循环不允许我们停止并等待异步操作。 `attempt`函数尝试发送请求一次。 它还设置了超时，如果 250 毫秒后没有响应返回，则开始下一次尝试，或者如果这是第四次尝试，则以`Timeout`实例为理由拒绝该`Promise`。

每四分之一秒重试一次，一秒钟后没有响应就放弃，这绝对是任意的。 甚至有可能，如果请求确实过来了，但处理器花费了更长时间，请求将被多次传递。 我们会编写我们的处理器，并记住这个问题 - 重复的消息应该是无害的。

总的来说，我们现在不会建立一个世界级的，强大的网络。 但没关系 - 在计算方面，乌鸦没有很高的预期。

为了完全隔离我们自己的回调，我们将继续，并为`defineRequestType`定义一个包装器，它允许处理器返回一个`Promise`或明确的值，并且连接到我们的回调。

```js
function requestType(name, handler) {
  defineRequestType(name, (nest, content, source,
                           callback) => {
    try {
      Promise.resolve(handler(nest, content, source))
        .then(response => callback(null, response),
              failure => callback(failure));
    } catch (exception) {
      callback(exception);
    }
  });
}
```

如果处理器返回的值还不是`Promise`，`Promise.resolve`用于将转换为`Promise`。

请注意，处理器的调用必须包装在`try`块中，以确保直接引发的任何异常都会被提供给回调函数。 这很好地说明了使用原始回调正确处理错误的难度 - 很容易忘记正确处理类似的异常，如果不这样做，故障将无法报告给正确的回调。`Promise`使其大部分是自动的，因此不易出错。

## `Promise`的集合


每台鸟巢计算机在其`neighbors`属性中，都保存了传输距离内的其他鸟巢的数组。 为了检查当前哪些可以访问，你可以编写一个函数，尝试向每个鸟巢发送一个`"ping"`请求（一个简单地请求响应的请求），并查看哪些返回了。

在处理同时运行的`Promise`集合时，`Promise.all`函数可能很有用。 它返回一个`Promise`，等待数组中的所有`Promise`解析，然后解析这些`Promise`产生的值的数组（与原始数组的顺序相同）。 如果任何`Promise`被拒绝，`Promise.all`的结果本身被拒绝。

```js
requestType("ping", () => "pong");

function availableNeighbors(nest) {
  let requests = nest.neighbors.map(neighbor => {
    return request(nest, neighbor, "ping")
      .then(() => true, () => false);
  });
  return Promise.all(requests).then(result => {
    return nest.neighbors.filter((_, i) => result[i]);
  });
}
```

当一个邻居不可用时，我们不希望整个组合`Promise`失败，因为那时我们仍然不知道任何事情。 因此，在邻居集合上映射一个函数，将它们变成请求`Promise`，并附加处理器，这些处理器使成功的请求产生`true`，拒绝的产生`false`。

在组合`Promise`的处理器中，`filter`用于从`neighbors`数组中删除对应值为`false`的元素。 这利用了一个事实，`filter`将当前元素的数组索引作为其过滤函数的第二个参数（`map`，`some`和类似的高阶数组方法也一样）。

## 网络泛洪

鸟巢仅仅可以邻居通信的事实，极大地减少了这个网络的实用性。

为了将信息广播到整个网络，一种解决方案是设置一种自动转发给邻居的请求。 然后这些邻居转发给它们的邻居，直到整个网络收到这个消息。

```js
import {everywhere} from "./crow-tech";

everywhere(nest => {
  nest.state.gossip = [];
});

function sendGossip(nest, message, exceptFor = null) {
  nest.state.gossip.push(message);
  for (let neighbor of nest.neighbors) {
    if (neighbor == exceptFor) continue;
    request(nest, neighbor, "gossip", message);
  }
}

requestType("gossip", (nest, message, source) => {
  if (nest.state.gossip.includes(message)) return;
  console.log(`${nest.name} received gossip '${
               message}' from ${source}`);
  sendGossip(nest, message, source);
});
```

为了避免永远在网络上发送相同的消息，每个鸟巢都保留一组已经看到的闲话字符串。 为了定义这个数组，我们使用`everywhere`函数（它在每个鸟巢上运行代码）向鸟巢的状态对象添加一个属性，这是我们将保存鸟巢局部状态的地方。

当一个鸟巢收到一个重复的闲话消息，它会忽略它。每个人都盲目重新发送这些消息时，这很可能发生。 但是当它收到一条新消息时，它会兴奋地告诉它的所有邻居，除了发送消息的那个邻居。

这将导致一条新的闲话通过网络传播，如在水中的墨水一样。 即使一些连接目前不工作，如果有一条通往指定鸟巢的替代路线，闲话将通过那里到达它。

这种网络通信方式称为泛洪 - 它用一条信息充满网络，直到所有节点都拥有它。

我们可以调用`sendGossip`看看村子里的消息流。

```js
sendGossip(bigOak, "Kids with airgun in the park");
```

## 消息路由

如果给定节点想要与其他单个节点通信，泛洪不是一种非常有效的方法。 特别是当网络很大时，这会导致大量无用的数据传输。

另一种方法是为消息设置节点到节点的传输方式，直到它们到达目的地。 这样做的困难在于，它需要网络布局的知识。 为了向远方的鸟巢发送请求，有必要知道哪个邻近的鸟巢更靠近其目的地。 以错误的方向发送它不会有太大好处。

由于每个鸟巢只知道它的直接邻居，因此它没有计算路线所需的信息。 我们必须以某种方式，将这些连接的信息传播给所有鸟巢。 当放弃或建造新的鸟巢时，最好是允许它随时间改变的方式。

我们可以再次使用泛洪，但不检查给定的消息是否已经收到，而是检查对于给定鸟巢来说，邻居的新集合，是否匹配我们拥有的当前集合。

```js
requestType("connections", (nest, {name, neighbors},
                            source) => {
  let connections = nest.state.connections;
  if (JSON.stringify(connections.get(name)) ==
      JSON.stringify(neighbors)) return;
  connections.set(name, neighbors);
  broadcastConnections(nest, name, source);
});

function broadcastConnections(nest, name, exceptFor = null) {
  for (let neighbor of nest.neighbors) {
    if (neighbor == exceptFor) continue;
    request(nest, neighbor, "connections", {
      name,
      neighbors: nest.state.connections.get(name)
    });
  }
}

everywhere(nest => {
  nest.state.connections = new Map;
  nest.state.connections.set(nest.name, nest.neighbors);
  broadcastConnections(nest, nest.name);
});
```

该比较使用`JSON.stringify`，因为对象或数组上的`==`只有在两者完全相同时才返回`true`，这不是我们这里所需的。 比较 JSON 字符串是比较其内容的一种简单而有效的方式。

节点立即开始广播它们的连接，它们应该立即为每个鸟巢提供当前网络图的映射，除非有一些鸟巢完全无法到达。

你可以用图做的事情，就是找到里面的路径，就像我们在第 7 章中看到的那样。如果我们有一条通往消息目的地的路线，我们知道将它发送到哪个方向。

这个`findRoute`函数非常类似于第 7 章中的`findRoute`，它搜索到达网络中给定节点的路线。 但不是返回整个路线，而是返回下一步。 下一个鸟巢将使用它的有关网络的当前信息，来决定将消息发送到哪里。

```js
function findRoute(from, to, connections) {
  let work = [{at: from, via: null}];
  for (let i = 0; i < work.length; i++) {
    let {at, via} = work[i];
    for (let next of connections.get(at) || []) {
      if (next == to) return via;
      if (!work.some(w => w.at == next)) {
        work.push({at: next, via: via || next});
      }
    }
  }
  return null;
}
```

现在我们可以建立一个可以发送长途信息的函数。 如果该消息被发送给直接邻居，它将照常发送。 如果不是，则将其封装在一个对象中，并使用`"route"`请求类型，将其发送到更接近目标的邻居，这将导致该邻居重复相同的行为。

```js
function routeRequest(nest, target, type, content) {
  if (nest.neighbors.includes(target)) {
    return request(nest, target, type, content);
  } else {
    let via = findRoute(nest.name, target,
                        nest.state.connections);
    if (!via) throw new Error(`No route to ${target}`);
    return request(nest, via, "route",
                   {target, type, content});
  }
}

requestType("route", (nest, {target, type, content}) => {
  return routeRequest(nest, target, type, content);
});
```

我们现在可以将消息发送到教堂塔楼的鸟巢中，它的距离有四跳。

```js
routeRequest(bigOak, "Church Tower", "note",
             "Incoming jackdaws!");
```

我们已经在原始通信系统的基础上构建了几层功能，来使其便于使用。 这是一个（尽管是简化的）真实计算机网络工作原理的很好的模型。

计算机网络的一个显着特点是它们不可靠 - 建立在它们之上的抽象可以提供帮助，但是不能抽象出网络故障。所以网络编程通常关于预测和处理故障。

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