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

优雅的异步处理: 承诺

承诺是一个相对比较新的特性,你可以使用它来延迟一些操作直到前面的代码已经返回结果。对于时间上顺序完成的一系列操作,这个真的有用。本文展示承诺如何工作,使用网络编程接口何处会见到它, 最重要的:怎样写你自己的承诺.

什么是“承诺?

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

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

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

 回调函数的麻烦

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

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

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

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

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

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

使用承诺改良

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

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

这要好得多 – 更容易看到发生了什么,我们只需要一个.接()代码块来处理所有错误,它不会阻塞主线程(这样我们就可以一边学习一边等着收可以发布的软件了),并保证每个操作在运行之前等待先前的操作完成。我们能够以这种方式一个接一个地链接多个异步操作,因为每个.下()代码块返回一个新的 承诺,当.下()代码块运行完毕时它会解析。聪明,对吗?

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

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

甚至这样:

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

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

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

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

然而,这并不容易阅读,如果您的代码块比我们在这里展示的更复杂,那么这个语法可能就不可用了。

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

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

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

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

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

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

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

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

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

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

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

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

应答 => 应答.数据

这是下面的简写

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

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

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

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

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

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

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

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

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

响应失败

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

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

要查看此操作,请尝试拼错文件的网址并重新运行程序。该错误将显示出来。

这并没有比根本不包含.接() 代码块做更多的事情,但是考虑一下——这允许我们精确地控制错误处理。在真实的应用程序中,.接() 代码块可以重试获取图像,或显示默认图像,或提示用户提供不同的图像网址,等等。

将代码块链在一起

这是一种很普通的写法;我们故意这样做是为了帮助你清楚地了解正在发生的事情。如本文前面所示,你可以将.下()块(以及.接()块)链接在一起:

引 阿修斯 自 '阿修斯';

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

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

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

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

承诺术语回顾

在上面的部分有很多内容要讲,所以让我们快速回顾一下,给你一个简短的指南,你可以把它作为书签,并在将来用来刷新你的记忆。你还应该多复习几次上面的部分,以确保这些概念能够被记住。

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

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

上面的例子向我们展示了使用承诺的一些真正的基础知识。现在让我们看一些更高级的功能。首先,将进程一个接一个地链接起来是没问题的,但是如果您想要在一大堆承诺都完成之后才运行一些代码,该怎么办呢?

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

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

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

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

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

引 阿修斯 自 '阿修斯';

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

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

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

我们在这里提供的用于显示项目的代码相当初级,但目前只能作为解释程序。

在承诺 实现/拒绝 后运行一些结束代码

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

诺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)
    .下(信息 => {
      控制台.日志(信息);
    })
    .接(错 => {
      控制台.日志('错误:' + 错);
    });

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

总结

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

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