回调

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

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

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

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

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

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

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

  1. import {bigOak} from "./crow-tech";
  2. bigOak.readStorage("food caches", caches => {
  3. let firstCache = caches[0];
  4. bigOak.readStorage(firstCache, info => {
  5. console.log(info);
  6. });
  7. });

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

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

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

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

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

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

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

  1. import {defineRequestType} from "./crow-tech";
  2. defineRequestType("note", (nest, content, source, done) => {
  3. console.log(`${nest.name} received note: ${content}`);
  4. done();
  5. });

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

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

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