1. 主页
  2. 文档
  3. 学习君土脚本
  4. 异步君土脚本
  5. 途和等:让异步编程更简单

途和等:让异步编程更简单

途 务 和 等 关键字是基于承诺的语法糖,它们使得异步代码更易于编写和阅读。通过使用它们,异步代码看起来更像是老式同步代码,因此它们非常值得学习。本文为您提供了您需要了解的内容。

途和等基础

在代码中使用 途 /*async*/和等/*await*/ 有两个部分。

途 关键字

首先,我们使用  关键字,你把它放在函数声明的前面,把它变成一个异步函数。异步函数是一个知道怎样使用  关键字调用异步代码的函数。

尝试运行以下行:

务 你好() { 回 "你好" };
你好();

该函数返回“你好” —— 没什么特别的,对吧?

如果我们将其变成异步函数呢?请尝试以下方法:

途 务 你好() { 回 "你好" };
控制台.日志(你好());

哈。现在调用该函数会返回一个 承诺。这是异步函数的特征之一 —— 它保证函数的返回值为 承诺。

你也可以创建一个异步函数表达式,如下所示:

定 你好 = 途 务() { 回 "你好" };

你可以使用箭头函数:

定 你好 = 途 () => { 回 "你好" };

这些都基本上是一样的。

要实际使用承诺完成时返回的值,我们可以使用.下()代码块,因为它返回的是 承诺:

你好().下((值) => 控制台.日志(值));

甚至只是简写如

你好().下(控制台.日志);

这就像我们在上一篇文章中看到的那样。

将  关键字加到函数申明中,可以告诉它们返回的是 承诺,而不是直接返回值。此外,它避免了同步函数为支持使用 等 带来的任何潜在开销。在函数声明为  时,君土脚本引擎会添加必要的处理,以优化你的程序。

等关键字

当 等 关键字与异步函数一起使用时,它的真正优势就变得明显了 —— 事实上, 等 只在异步函数里面才起作用。它可以放在任何异步的、基于 承诺 的函数之前。它会暂停代码在该行上,直到 承诺 完成,然后返回结果值。在暂停的同时,其他正在等待执行的代码就有机会执行了。

您可以在调用任何返回承诺的函数时使用 ,包括网络编程接口函数。

这是一个简单的示例:

途 务 你好() {
  回 等 诺.解决("你好");
};

你好().下(控制台.日志);

当然,上面的示例不是很有用,但它确实展示了语法。让我们继续,看一个真实示例。

 使用 途/等 重写 承诺 代码

让我们回顾一下我们在上一篇文章中简单的 阿修斯.取 示例:

引 阿修斯 自 '阿修斯';

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

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

到现在为止,你应该对 承诺 及其工作方式有一个较好的理解。让我们将其转换为使用途 / 等看看它使事情变得简单了多少:

引 阿修斯 自 '阿修斯';

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

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

读取数据()
  .接(错误 => {
    控制台.日志('出错了:' + 错误);
  });

它使代码简单多了,更容易理解 —— 去除了到处都是 .下() 代码块!

由于  关键字将函数转换为 承诺,您可以重构以上代码 —— 使用 承诺 和 等 的混合方式,将函数的后半部分抽取到新代码块中。这样做可以更灵活:

引 阿修斯 自 '阿修斯';

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

途 务 读取数据() {
  定 应答 = 等 阿修斯.取(网址);
  若(应答.状态 != 200) {
    抛 启 错误('读取数据出错:' + 应答.状态);
  }
  回 应答.数据;
}

读取数据()
  .下(数据 => {
    控制台.日志(象谱.串(数据));
  });

它到底是如何工作的?

您会注意到我们已经将代码封装在函数中,并且我们在  关键字之前包含了  关键字。这是必要的 –– 您必须创建一个异步函数来定义一个代码块,在其中运行异步代码;  只能在异步函数内部工作。

读取数据()函数定义中,您可以看到代码与先前的 承诺 版本非常相似,但存在一些差异。不需要附加 .下() 代码块到每个基于承诺方法的结尾,你只需要在方法调用前添加 等 关键字,然后把结果赋给变量。等 关键字使君土脚本运行时暂停于此行,允许其他代码在此期间执行,直到异步函数调用返回其结果。一旦完成,您的代码将继续从下一行开始执行。例如:

  定 应答 = 等 阿修斯.取(网址);

解析器会在此行上暂停,直到当服务器返回的响应变得可用时。此时 阿修斯.取() 返回的 承诺 将会完成,返回的 应答 会被赋值给 应答 变量。一旦服务器返回的响应可用,解析器就会移动到下一行。我们将数据读取数据()函数中返回。

这意味着当我们调用读取数据()函数时,它会返回一个承诺,因此我们可以将.下()链接到它的末尾,在其中我们把数据显示在控制台上。

你可能已经觉得“这真的很酷!”,你是对的 —— 用更少的.下()代码块来封装代码,同时它看起来很像同步代码,所以它非常直观。

添加错误处理

如果你想添加错误处理,你有几个选择。

您可以将同步的 试...接 结构和 途/等 一起使用 。此示例扩展了我们上面展示的第一个版本代码:

引 阿修斯 自 '阿修斯';

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

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

读取数据();

接() {} 代码块会接收一个错误对象 错误 ; 我们现在可以将其记录到控制台,它将向我们提供详细的错误消息,显示错误被抛出的代码中的位置。

如果你想使用我们上面展示的第二个(重构)代码版本,你最好继续混合方式并将 .接() 块链接到 .下() 调用的末尾,就像这样:

引 阿修斯 自 '阿修斯';

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

途 务 读取数据() {
  定 应答 = 等 阿修斯.取(网址);
  若(应答.状态 != 200) {
    抛 启 错误('读取数据出错:' + 应答.状态);
  }
  回 应答.数据;
}

读取数据().下(数据 => {
  控制台.日志(数据);
}).接(错 => {
  控制台.日志(错);
});

这是因为 .接() 块将捕获来自异步函数调用和承诺链中的错误。如果您在此处使用了试/接 代码块,则在调用 读取数据() 函数时,您仍可能会收到未处理的错误。

等待诺.全()

途 / 等 建立在 承诺 之上,因此它与承诺提供的所有功能兼容。这包括诺.全() –– 你完全可以通过调用  诺.全() 将所有结果返回到变量中,就像同步代码一样。让我们再次回到上一篇中看到的例子

将其转换为 途 / 等,现在看起来像这样:

引 阿修斯 自 '阿修斯';

途 务 显示内容() {
  定 网址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));
}

显示内容().接((错: 错误) => {
  控制台.日志('错误:' + 错.信息);
});

请看诺.全() 行:

定 值数组 = 等 诺.全([书1, 书2, 书3]);

在这里,通过使用,我们能够在三个承诺的结果都可用的时候,放入数据组数组中。这看起来非常像同步代码。我们需要将所有代码封装在一个新的异步函数显示内容() 中,尽管没有减少很多代码,但能够将大部分代码从 .下() 代码块移出,使代码得到了简化,更易读。

为了错误处理,我们在 显示内容() 调用中包含了一个 .接() 代码块;这将处理两个函数中出现的错误。

注意: 也可以在异步函数中使用同步  代码块代替 .终() 异步代码块,以显示操作如何进行的最终报告。

途/等的缺陷

了解途/等是非常有用的,但还有一些缺点需要考虑。

途/等 让你的代码看起来是同步的,在某种程度上,也使得它的行为更加地同步。  关键字会阻塞其后的代码,直到承诺完成,就像执行同步操作一样。它确实可以允许其他任务在此期间继续运行,但您自己的代码被阻塞。

这意味着您的代码可能会因为大量的承诺相继发生而变慢。每个都会等待前一个完成,而你实际想要的是所有的这些承诺同时开始处理(就像我们没有使用途/等时那样)。

有一种模式可以缓解这个问题——通过将 承诺 对象存储在变量中来同时开始它们,然后等待它们全部执行完毕。让我们看一些证明这个概念的例子。

我们有两个可用的例子 —— 慢调用异步快调用异步。它们都以自定义承诺函数开始,该函数使用设置超时() 调用伪造异步进程:

  务 超时承诺(间隔: 数) {
    回 启 诺((解决, 驳回) => {
      设置超时(务 () {
        解决("好了");
      }, 间隔);
    });
  };

然后每个包含一个 时间测试() 异步函数,等待三个 超时承诺() 调用:

  途 务 时间测试() {
    ...
  }

每一个都以记录开始时间结束,查看 时间测试() 承诺 需要多长时间才能完成,然后记录结束时间并报告操作总共需要多长时间:

  定 开始时间 = 日期.现在();
  时间测试().下(() => {
    定 结束时间 = 日期.现在();
    定 耗时 = 结束时间 - 开始时间;
    控制台.日志("使用了" + 耗时 + "毫秒");
  })

时间测试() 函数在每种情况下都不同。

慢调用异步示例中,时间测试() 如下所示:

  途 务 时间测试() {
    等 超时承诺(3000);
    等 超时承诺(3000);
    等 超时承诺(3000);
  }

在这里,我们直接等待所有三个超时承诺()调用,使每个调用3秒钟。后续的每一个都被迫等到最后一个完成 – 如果你运行第一个例子,你会看到弹出框报告的总运行时间大约为9秒。

快调用异步示例中,时间测试() 如下所示:

  途 务 时间测试() {
    定 诺1 = 超时承诺(3000);
    定 诺2 = 超时承诺(3000);
    定 诺3 = 超时承诺(3000);
    等 诺1;
    等 诺2;
    等 诺3;
  }

在这里,我们将三个承诺对象存储在变量中,这样可以同时启动它们关联的进程。

接下来,我们等待他们的结果 – 因为承诺都在基本上同时开始处理,承诺将同时完成;当您运行第二个示例时,您将看到弹出框报告总运行时间仅超过3秒!

您必须仔细测试您的代码,并在性能开始受损时牢记这一点。

另一个小小的不便是你必须将等待执行的承诺封装在异步函数中。

途/等 的类方法

最后值得一提的是,我们可以在类/对象方法前面添加,以使它们返回承诺,并它们内部的承诺。查看使用异步方法的修改版本:

  类 人 {
    姓名: { 姓: 文, 名: 文 };
    年龄: 数;
    性别: 文;
    兴趣: 文[];
    构(姓: 文, 名: 文, 年龄: 数, 性别: 文, 兴趣: 文[]) {
      此.姓名 = { 姓, 名 };
      此.年龄 = 年龄;
      此.性别 = 性别;
      此.兴趣 = 兴趣;
    }
    描述() {
      控制台.日志(此.姓名.姓 + 此.姓名.名 + '有' + 此.年龄 + '岁。他喜欢' + 此.兴趣[0] + ' 和 ' + 此.兴趣[1] + '。');
    }
    途 问候() {
      回 等 诺.解决("你好, 我是" + 此.姓名.姓 + 此.姓名.名);
    }
  }

  定 人1 = 启 人("张", "三", 32, '男', ['编程', '乒乓球']);

第一个实例方法可以使用如下:

  人1.问候().下(控制台.日志);

总结

途/等 提供了一种很好的,简化的方法来编写更易于阅读和维护的异步代码。