1. 主页
  2. 文档
  3. 学习君土脚本
  4. 异步君土脚本
  5. 优雅的异步处理

优雅的异步处理

诺/*Promise*/ 是一个相对较新的功能,允许你推迟进一步的操作,直到上一个操作完成或响应其失败。这对于设置一系列异步操作以正常工作非常有用。本文向你展示了 如何工作,如何在编程接口中使用它们以及如何编写自己的编程接口。

什么是“诺“?

我们在教程的第一篇文章中简要地了解了 ,接下来我们将在更深层次理解诺。

本质上, 是一个对象,代表操作的中间状态 —— 正如它的单词含义 ‘承诺’ ,它保证在未来可能返回某种结果。虽然 并不保证操作在何时完成并返回结果,但是它保证当结果可用时,你的代码能正确处理结果,当结果不可用时,你的代码同样会被执行,来优雅的处理错误。

通常你不会对一个异步操作从开始执行到返回结果所用的时间感兴趣(除非它耗时过长),你会更想在任何时候都能响应操作结果,当然它不会阻塞其余代码的执行就更好了。

 回调函数的麻烦

要完全理解为什么 诺 是一件好事,应该回想之前的回调函数,理解它们造成的困难。

我们来谈谈以软件开发作为类比。为了使我们的软件开发成功,我们有一些基本的阶段工作需要做,不按顺序执行或上一步没完成就执行下一步是不会成功的:

  1. 软件需求分析。系统分析员根据产品的需求做软件需求分析,需要深入了解和分析需求,如果有不清楚的地方,需要通过问题处理向用户确认需求,编写出软件的需求。
  2. 软件设计。开发者根据软件的需求,作概要设计和详细设计,在设计过程中,如果遇到问题,通过问题处理与系统分析员解决问题。设计出软件的设计。
  3. 然后开发和测试,发布软件。开发者根据软件的设计编写代码,发布测试版本;测试人员编写测试用例,测试发布版本。如果开发和测试过程中遇到问题,通过问题处理来解决!开发和测试开发出可以发布的软件。

对于旧式回调,上述功能的伪代码表示可能如下所示:

软件需求分析(产品的需求, 务(软件的需求) {
  软件设计(软件的需求, 务(软件的设计) {
    开发和测试(软件的设计, 务(可以发布的软件) {
      发布软件(可以发布的软件);
    }, 问题处理);
  }, 问题处理);
}, 问题处理);

这很麻烦且难以阅读(通常称为“回调地狱”),需要多次调用错误处理()(每个嵌套函数一次),还有其他问题。

使用 诺 改良

诺 使得上面的情况更容易编写,解析和运行。如果我们使用异步 诺 代表上面的伪代码,我们最终会得到这样的结果:

软件需求分析(产品需求)
.下(务(软件需求) {
  回 软件设计(软件需求);
})
.下(务(软件设计) {
  回 开发和测试(软件设计);
})
.下(务(可以发布软件) {
  发布软件(可以发布软件);
})
.接(问题处理);

这要好得多 – 更容易看到发生了什么,我们只需要一个.接()块来处理所有错误,它不会阻塞主线程(所以我们可以在等待时继续玩视频游戏为了准备好收集披萨),并保证每个操作在运行之前等待先前的操作完成。我们能够以这种方式一个接一个地链接多个异步操作,因为每个.下()块返回一个新的 诺,当.下()块运行完毕时它会解析。聪明,对吗?

使用箭头函数,你可以进一步简化代码:

软件需求分析(产品的需求)
.下(软件的需求 =>
  软件设计(软件的需求)
)
.下(软件的设计 =>
  开发和测试(软件的设计)
)
.下(可以发布的软件 =>
  发布软件(可以发布的软件)
)
.接(问题处理);

甚至这样:

软件需求分析(产品的需求)
.下(软件的需求 => 软件设计(软件的需求))
.下(软件的设计 => 开发和测试(软件的设计))
.下(可以发布的软件 => 发布软件(可以发布的软件))
.接(问题处理);

这是有效的,因为使用箭头函数 () => x 是 ()=> {回 x;}  的有效简写; 。

你甚至可以这样做,因为函数只是直接传递它们的参数,所以不需要额外的函数层:

软件需求分析(产品的需求).下(软件设计).下(开发和测试).下(发布软件).接(问题处理);

但是,这并不容易阅读,如果你的块比我们在此处显示的更复杂,则此语法可能无法使用。

注意: 你可以使用 途/等 语法进行进一步的改进,我们将在下一篇文章中深入讨论。

最基本的,诺 与事件监听器类似,但有一些差异:

  • 一个诺只能成功或失败一次。它不能成功或失败两次,并且一旦操作完成,它就无法从成功切换到失败,反之亦然。
  • 如果诺成功或失败并且你稍后添加成功/失败回调,则将调用正确的回调,即使事件发生在较早的时间。

 解释 诺 的基本语法:一个真实的例子

诺 很重要,因为大多数现代网络编程接口都将它们用于执行潜在冗长任务的函数。要使用现代网络技术,你需要使用诺。在本章的后面我们将看看如何编写自己的诺,但是现在我们将看一些你将在网络编程接口中遇到的简单示例。

在第一个示例中,我们将使用阿修斯.取()方法从网络获取文件,数据 属性得到 响应的原始内容对象,然后在控制台显示该对象。这与我们在 异步君土脚本简介 中看到的示例非常相似,但是会在构建你自己的基于 诺 的代码时有所不同。 

  1. 首先,添加以下行:
引 阿修斯 自 '阿修斯';

定 网址 = `https://git.jtu.net.cn/xuexi/shuru/-/raw/master/${编码地址('书')}.json`;
定 诺0 =  阿修斯.取(网址);

这会调用 阿修斯.取() 方法,将文件的网址作为参数从网络中提取。我们将 阿修斯.取() 返回的对象存储在一个名为诺0的变量中。正如我们之前所说的,这个对象代表了一个最初既不成功也不失败的中间状态 – 这个状态下的诺的官方术语叫作 待定/*pending*/

  1. 为了响应成功完成操作(在这种情况下,当返回应答时),我们调用诺对象的.下()方法。 .下()块中的回调(称为执行程序)仅在诺调用成功完成时运行并返回应答对象。它将返回的应答对象作为参数传递。

注意.下()块的工作方式类似于使用加事听()向对象添加事件侦听器时的方式。它不会在事件发生之前运行(当诺履行时)。最显着的区别是.下()每次使用时只运行一次,而事件监听器可以多次调用。

我们读取此响应的数据如下:

应答 => 应答.数据

这是下面的简写

务(应答) {
    回 应答.数据;
}

好的,我们还需要做点额外的工作。如果应答的状态不为200,抛出错误,否则返回 数据。就像下面的代码这样做。

定 诺1 = 诺0.下(应答=>{
  若(应答.状态 != 200) {
    抛 启 错('读取数据出错:' + 应答.状态);
  } 别 {
    回 应答.数据
  }
});

5. 每次调用.下()都会创建一个新的诺。我们可以通过调用第二个 诺 的.下()方法来处理它在履行时返回的对象。

将以下内容添加到代码的末尾:

定 诺2 = 诺1.下(数据 => {})

6. 现在让我们填写执行程序函数的主体。在花括号内添加以下行:

  控制台.日志(象谱.串(数据));

这里我们将接收到的数据在控制台显示出来。

响应失败

缺少一些东西 – 如果其中一个诺失败(否决/*rejects*/),目前没有什么可以明确地处理错误。我们可以通过运行前一个诺的 .接() 方法来添加错误处理。立即添加:

定 错误情况 = 诺2.接(错误 => {
  控制台.日志('出错了:' + 错误);
});

要查看此操作,请尝试拼错图像的URL并重新加载页面。该错误将在浏览器的开发人员工具的控制台中报告。

如果你根本不操心包括的 .接() 块,这并没有做太多的事情,但考虑一下(指.接()块) ––这会使我们可以完全控制错误处理方式。在真实的应用程序中,你的.接()块可以重试获取数据,或显示默认数据,或提示用户提供不同的数据网址等等。

将代码块链在一起

这是写出来的一种非常简便的方式;我们故意这样做是为了帮助你清楚地了解发生了什么。如本文前面所示,你可以将.下()块(以及.接()块)链接在一起:

引 阿修斯 自 '阿修斯';

定 网址 = `https://git.jtu.net.cn/xuexi/shuru/-/raw/master/${编码地址('书')}.json`;

阿修斯.取(网址).下(务(应答) {
  若(应答.状态 != 200) {
    抛 启 错('读取数据出错:' + 应答.状态);
  } 别 {
    回 应答.数据
  }
}).下(务(数据) {
  控制台.日志(象谱.串(数据));
}).接(务(错误) {
  控制台.日志(错误);
});

请记住,履行的 诺 所返回的值将成为传递给下一个 .下() 块的执行函数的参数。

注意: 诺中的.下()/接()块基本上是同步代码中试...接块的异步等价物。请记住,同步试 ... 接在异步代码中不起作用。

诺 术语回顾

在上面的部分中有很多要介绍的内容,所以让我们快速回过头来给你一个简短的指南,你可以将它添加到书签中,以便将来更新你的记忆。你还应该再次阅读上述部分,以确保这些概念坚持下去。

  1. 创建诺时,它既不是成功也不是失败状态。这个状态叫作待定(pending)。
  2. 当诺返回时,称为 已解决(resolved).
    1. 一个成功已解决的诺称为实现fullfilled)。它返回一个值,可以通过将.下()块链接到诺链的末尾来访问该值。 .下()块中的执行程序函数将包含诺的返回值。
    2. 一个不成功已解决的诺被称为拒绝rejected)了。它返回一个原因(reason),一条错误消息,说明为什么拒绝 诺。可以通过将.接()块链接到诺链的末尾来访问此原因。

 运行代码以响应多个诺的实现

上面的例子向我们展示了使用诺的一些真正的基础知识。现在让我们看一些更高级的功能。首先,链接进程一个接一个地发生都很好,但是如果你想在一大堆诺全部完成之后运行一些代码呢?

 你可以使用巧妙命名的诺/*Promise*/.全/*all*/()静态方法完成此操作。这将一个诺数组作为输入参数,并返回一个新的诺对象,只有当数组中的所有诺都满足时才会满足。它看起来像这样:

诺.全([甲, 乙, 丙]).下(值数组 => {
  ...
});

如果它们都实现,那么数组中的结果将作为参数传递给.下()块中的执行器函数。如果传递给诺.全()的任何一个 诺 拒绝,整个块将拒绝

这非常有用。想象一下,我们正在获取信息以在内容上动态填充页面上的UI功能。在许多情况下,接收所有数据然后才显示完整内容,而不是显示部分信息。

让我们构建另一个示例来展示这一点。

引 阿修斯 自 '阿修斯';

定 网址1 = `https://git.jtu.net.cn/xuexi/shuru/-/raw/master/${编码地址('书')}.json`;
定 网址2 = `https://git.jtu.net.cn/xuexi/shuru/-/raw/master/${编码地址('书2')}.json`;
定 网址3 = `https://git.jtu.net.cn/xuexi/shuru/-/raw/master/${编码地址('书3')}.json`;

定 书1 = 阿修斯.取(网址1);
定 书2 = 阿修斯.取(网址2);
定 书3 = 阿修斯.取(网址3);

诺.全([书1, 书2, 书3]).下(值数组 => {
  定 回应0 = 值数组[0].数据;
  定 回应1 = 值数组[1].数据;
  定 回应2 = 值数组[2].数据;
  控制台.日志(象谱.串(回应0));
  控制台.日志(象谱.串(回应1));
  控制台.日志(象谱.串(回应2));
}).接((错误: 错) => {
  控制台.日志('错误:' + 错误.信息);
});

我们在这里提供的用于显示项目的代码相当简陋,但现在作为解释器。

注意: 如果你正在改进这段代码,你可能想要遍历一个项目列表来显示,获取和解码每个项目,然后循环遍历诺.全()内部的结果,运行一个不同的函数来显示每个项目取决于什么代码的类型是。这将使它适用于任何数量的项目,而不仅仅是三个。

 在诺 实现/拒绝 后运行一些最终代码

在诺完成后,你可能希望运行最后一段代码,无论它是否已实现(fullfilled)或被拒绝(rejected)。此前,你必须在.下().接()回调中包含相同的代码,例如:

诺0
  .下(应答 => {
    做一些事(应答);
    运行最后代码();
  })
  .接(错误: 错 => {
    错误处理(错误);
    运行最后代码();
  });

在现代浏览器中,.终() 方法可用,它可以链接到常规诺链的末尾,允许你减少代码重复并更优雅地执行操作。上面的代码现在可以写成如下:

诺0
  .下(应答 => {
    做一些事(应答);
  })
  .接(错误: 错 => {
    错误处理(错误);
  }).终(()=>{
    运行最后代码();
  });

有一个示例,这与我们在上面部分中看到的诺.全()演示完全相同,除了我们将终()调用链接到链的末尾:

诺0
  .下(应答 => {
    做一些事(应答);
  })
  .接(错误: 错 => {
    错误处理(错误);
  }).终(()=>{
    运行最后代码();
  });引 阿修斯 自 '阿修斯';

  定 网址1 = `https://git.jtu.net.cn/xuexi/shuru/-/raw/master/${编码地址('书')}.json`;
  定 网址2 = `https://git.jtu.net.cn/xuexi/shuru/-/raw/master/${编码地址('书2')}.json`;
  定 网址3 = `https://git.jtu.net.cn/xuexi/shuru/-/raw/master/${编码地址('书3')}.json`;
  
  定 书1 = 阿修斯.取(网址1);
  定 书2 = 阿修斯.取(网址2);
  定 书3 = 阿修斯.取(网址3);
  
  诺.全([书1, 书2, 书3]).下(值数组 => {
    定 回应0 = 值数组[0].数据;
    定 回应1 = 值数组[1].数据;
    定 回应2 = 值数组[2].数据;
    控制台.日志(象谱.串(回应0));
    控制台.日志(象谱.串(回应1));
    控制台.日志(象谱.串(回应2));
  }).接((错误: 错) => {
    控制台.日志('错误:' + 错误.信息);
  }).终(()=>{
    控制台.日志('结束');
  });

这会将一条简单的消息记录到控制台,告诉我们运行结束。

注意:终()允许你在异步代码中编写异步等价物 试/ 接 / 终。

 构建自定义诺

好消息是,在某种程度上,你已经建立了自己的诺。当你使用.下()块链接多个诺时,或者将它们组合起来创建自定义函数时,你已经在创建自己的基于异步声明的自定义函数。

将不同的基于诺的编程接口组合在一起以创建自定义函数是迄今为止你使用诺进行自定义事务的最常见方式,并展示了基于相同原则的大多数现代编程接口的灵活性和强大功能。然而,还有另一种方式。

使用诺()构造函数

可以使用诺() 构造函数构建自己的诺。当你需要使用现有的旧项目代码、库或框架以及基于现代诺的代码时,这会派上用场。比如,当你遇到没有使用诺的旧式异步编程接口的代码时,你可以用诺来重构这段异步代码。

让我们看一个简单的示例来帮助你入门 —— 这里我们用 诺 包装一了个置超时(),它会在两秒后运行一个函数,该函数将用字符串“成功!”,解析当前诺(调用链接的解决())。

定 超时诺 = 启 诺<文>((解决, 驳回) => {
  设置超时(务() {
    解决('成功!');
  }, 2000);
});

解决()驳回()是用来实现拒绝新创建的诺的函数。此处,诺 成功运行通过显示字符串“成功!”。

因此,当你调用此诺时,可以将.下()块链接到它的末尾,它将传递给.下()块一串“成功!”。在下面的代码中,我们显示出该消息:

超时诺
  .下((信息) => {
    控制台.日志(信息);
  })

更简化的版本:

超时诺.下(控制台.日志)

上面的例子不是很灵活 – 诺只能实现一个字符串,并且它没有指定任何类型的驳回()条件(诚然,设置超时()实际上没有失败条件,所以对这个简单的例子并不重要)。

拒绝一个自定义诺

我们可以创建一个驳回() 方法拒绝诺  – 就像解决()一样,这需要一个值,但在这种情况下,它是拒绝的原因,即将传递给.接()的错误块。

让我们扩展前面的例子,使其具有一些驳回()条件,并允许在成功时传递不同的消息。

  务 超时诺函数(信息: 文, 间歇: 数) {
    回 启 诺<文>((解决, 驳回) => {
      若 (信息 === '' || 样 信息 !== '文') {
        驳回('信息是空或者不是字符串');
      } 别 若 (间歇 < 0 || 样 间歇 !== '数') {
        驳回('间隙不是数或者为负数');
      } 别 {
        设置超时(务() {
          解决(信息);
        }, 间歇);
      }
    });
  };

在这里,我们将两个方法传递给一个自定义函数 – 一个用来做某事的消息,以及在做这件事之前要经过的时间间隔。在函数内部,我们返回一个新的对象 – 调用该函数将返回我们想要使用的诺。

构造函数中,我们在若 ... 别结构中进行了一些检查:

  1. 首先,我们检查消息是否适合被警告。如果它是一个空字符串或根本不是字符串,我们会使用合适的错误消息拒绝该诺。
  2. 接下来,我们检查间隔是否是适当的间隔值。如果是负数或不是数字,我们会使用合适的错误消息拒绝诺。
  3. 最后,如果参数看起来都正常,我们使用设置超时()在指定的时间间隔过后,使用指定的消息解析诺。

由于超时诺函数()函数返回一个,我们可以将.下().接()等链接到它上面以利用它的功能。现在让我们使用它 – 将以前的超时诺 用法替换为以下值:

  超时诺函数('你好!', 1000)
    .下(信息 => {
      控制台.日志(信息);
    })
    .接(错误 => {
      控制台.日志('错误:' + 错误);
    });

当你按原样保存并运行代码时,一秒钟后你将收到消息提醒。现在尝试将消息设置为空字符串或将间隔设置为负数,例如,你将能够通过相应的错误消息查看被拒绝的诺!你还可以尝试使用已解决的消息执行其他操作,而不仅仅是提醒它。

总结

当我们不知道函数的返回值或返回需要多长时间时,是构建异步应用程序的好方法。它们使得在没有深度嵌套回调的情况下更容易表达和推理异步操作序列,并且它们支持类似于同步试 ... 接语句的错误处理方式。

本文中,我们没有涉及的所有诺的功能,只是最有趣和最有用的功能。当你开始了解有关诺的更多信息时,你会遇到更多功能和技巧。