1. 主页
  2. 文档
  3. 学习君土脚本
  4. 异步君土脚本
  5. 异步君土脚本简介

异步君土脚本简介

在本文中,我们简要回顾一下与同步君土脚本相关的问题,首次介绍你将遇到的一些不同的异步技术,并展示如何使用这些技术解决问题。

同步君土脚本

要理解什么是异步 君土脚本 ,我们应该从确切理解同步 君土脚本 开始。本节回顾我们在上一篇文章中看到的一些信息。

前面学的很多知识基本上都是同步的 — 运行代码,然后尽快返回结果。先看一个简单的例子 :

  定 小明 = { "名": "小明", "年龄": 15 };
  控制台.日志(小明);
  定 字串0 = 象谱.串(小明);
  控制台.日志(字串0);

这段代码, 一行一行的顺序执行:

  1. 先生成一个对象小明
  2. 把这个对象的内容在控制台显示出来
  3. 把这个对象内容使用对象简谱字符串化
  4. 把生成的字符串在控制台显示出来

每一个操作在执行的时候,其他任何事情都没有发生。因为前篇文章提到过君土脚本是单线程。任何时候只能做一件事情,只有一个主线程,其他的事情都阻塞了,直到前面的操作完成。

异步君土脚本

就前面提到的种种原因(比如,和阻塞相关)很多网页编程接口特性使用异步代码,特别是从外部的设备上获取资源,譬如,从网络获取文件,访问数据库,从网络摄像头获得视频流,或者向虚拟现实(VR)头罩广播图像。

为什么使用异步代码这么难?看一个例子,当你从服务器获取一个数据,通常你不可能立马就得到,虽然现在的网络很快,这还是需要时间。这意味着下面的伪代码可能不能正常工作:

引 * 作 秘传 自 '秘传';
定 接收数据: 文 = '';
秘传.取('https://git.jtu.net.cn/xuexi/jtjb/-/raw/master/书.json', (应答) => {
  应答.对('数据', (块) => {
    接收数据 = 块;
  });
});
// 处理接收数据
控制台.日志(接收数据);

因为你不知道下载数据需要多久,所以接收数据没有内容,因为数据还没有就绪。取代的方法就是,代码必须等到 应答 返回才能继续往下执行。

在君土脚本代码中,你经常会遇到两种异步编程风格:老派的回调和新派的承诺。下面就来分别介绍。

异步回调

异步回调 其实就是函数回调,只不过是回调函数作为参数传递给那些在后台执行的其他函数. 当那些后台运行的代码结束,就调用回调函数,通知你工作已经完成,或者其他有趣的事情发生了。使用回调 有一点老套,在一些老派但经常使用的编程接口里面,你会经常看到这种风格。

举个例子,异步回调 就是应答.对()第二个参数(前面的例子):

引 * 作 秘传 自 '秘传';
秘传.取('https://git.jtu.net.cn/xuexi/jtjb/-/raw/master/书.json', (应答) => {
  控制台.日志('状态码:' + 应答.状码)
  控制台.日志('响应头:' + 象谱.串(应答.头))
  应答.置编码('码8');
  应答.对('数据', (块) => {
    控制台.日志(`响应主体: ${块}`);
  });
  应答.对('结束', () => {
    控制台.日志('响应中已无数据');
  });
});

第一个参数是侦听的事件类型(’数据’),第二个参数就是事件发生时调用的回调函数。

当我们把回调函数作为一个参数传递给另一个函数时,仅仅是把回调函数定义作为参数传递过去 — 回调函数并没有立刻执行,回调函数会在包含它的函数的某个地方异步执行,包含函数负责在合适的时候执行回调函数。

你可以自己写一个容易的、包含回调函数的函数。来看另外一个例子,用 秘传.取() 加载资源:

引 * 作 秘传 自 '秘传';

务 加载象谱(网址: 文, 回调: (错: 化, 对象?: 化) => 无) {
  秘传.取(网址, (应答) => {
    若 (应答.状码 != 200) {
      回调(启 错误('错误状态码:' + 应答.状码));
      回;
    }
    应答.置编码('码8');
    定 接收数组: 文[] = [];
    应答.对('数据', 块 => 接收数组.压(块));
    应答.对('结束', () => {
      定 接收字符串 = 接收数组.合并();
      定 接收对象 = {};
      试 {
        接收对象 = 象谱.析(接收字符串.转字串());
        回调(空, 接收对象);
      } 接 (错) {
        回调(错);
      }
    });
  });
}
务 显示文本(错: 化, 对象: 化) {
  若 (错) {
    控制台.日志(错);
  } 别 {
    控制台.日志(象谱.串(对象));
  }
}
定 网址 = 'https://git.jtu.net.cn/xuexi/jtjb/-/raw/master/书.json';
加载象谱(网址, 显示文本);

创建 显示文本() 函数,把错误和接收对象传递给它。然后,我们创建 加载象谱() 函数,把网址和回调函数同时都作为参数。函数用秘传.取()获取给定网址的资源,在获得资源响应后再把接收对象作为参数传递给回调函数去处理。 (使用’数据’和’结束’ 事件处理) ,有点烧脑,是不是?!

回调函数用途广泛 — 他们不仅仅可以用来控制函数的执行顺序和函数之间的数据传递,还可以根据环境的不同,将数据传递给不同的函数,所以对下载好的资源,你可以采用不同的操作来处理,譬如 处理对象()显示文本(), 等等。

请注意,不是所有的回调函数都是异步的 — 有一些是同步的。一个例子就是使用 组.对每个() 来遍历数组:

常 朝代 = ['夏', '商', '周', '秦', '汉'];

朝代.对每个(务(名称, 序) {
  控制台.日志(序 + '. ' + 名称);
});

在这个例子中,我们遍历一个朝代名称的数组,并在控制台中打印索引和值。每() 需要的参数是一个回调函数,回调函数本身带有两个参数,数组元素和索引值。它无需等待任何事情,立即运行。

承诺

承诺是新派的异步代码,现代的网络编程接口经常用到。 阿修斯.取() 就是一个很好的例子, 它基本上就是一个现代版的网络请求。看个例子:

引 阿修斯 自 '阿修斯';

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

阿修斯.取(网址).下(务(应答) {
  回 应答.数据;
}).下(务(数据) {
  控制台.日志(象谱.串(数据));
}).接(务(错) {
  控制台.日志(错);
});

这里阿修斯.取() 只需要一个参数— 资源的网络 网址 — 返回一个承诺 . 承诺是表示异步操作完成或失败的对象。可以说,它代表了一种中间状态。 本质上,这是脚本运行环境说“我承诺尽快给您答复”的方式,因此得名承诺。

这个概念需要练习来适应, 它感觉有点像运行中的薛定谔之猫。这两种可能的结果都还没有发生,因此阿修斯.取操作目前正在等待脚本运行环境试图在将来某个时候完成该操作的结果。然后我们有三个代码块链接到阿修斯.取()的末尾:

  • 两个 下() 块。两者都包含一个回调函数,如果前一个操作成功,该函数将运行,并且每个回调都接收前一个成功操作的结果作为输入,因此您可以继续对它执行其他操作。每个 .下()块返回另一个承诺,这意味着可以将多个.下()块链接到另一个块上,这样就可以依次执行多个异步操作。
  • 如果其中任何一个下()块失败,则在末尾运行接()块——与同步试...接类似,接()提供了一个错误对象,可用来报告发生的错误类型。但是请注意,同步试...接不能与承诺一起工作,尽管它可以与途/等一起工作,稍后您将了解到这一点。

注释: 在本模块稍后的部分中,你将学习更多关于承诺的内容,所以如果你还没有完全理解这些承诺,请不要担心。

事件队列

像承诺这样的异步操作被放入事件队列中,事件队列在主线程完成处理后运行,这样它们就不会阻止后续君土脚本代码的运行。排队操作将尽快完成,然后将结果返回到君土脚本环境。

承诺 对比 回调

承诺与旧式回调有一些相似之处。它们本质上是一个返回的对象,您可以将回调函数附加到该对象上,而不必将回调作为参数传递给另一个函数。

然而,承诺是专门为异步操作而设计的,与旧式回调相比具有许多优点:

  • 您可以使用多个下()操作将多个异步操作链接在一起,并将其中一个操作的结果作为输入传递给下一个操作。这种链接方式对回调来说要难得多,会使回调以混乱的“末日金字塔”告终。 (也称为回调地狱)。
  • 承诺总是严格按照它们放置在事件队列中的顺序调用。
  • 错误处理要好得多——所有的错误都由块末尾的一个.接()块处理,而不是在“金字塔”的每一层单独处理。

异步代码的本质

让我们研究一个示例,它进一步说明了异步代码的本质,展示当我们不完全了解代码执行顺序以及将异步代码视为同步代码时可能发生的问题。下面的示例与我们之前看到的非常相似。一个不同之处在于,我们包含了许多控制台.日志()语句,以展示代码将在其中执行的顺序。

引 阿修斯 自 '阿修斯';

控制台.日志('开始');

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

阿修斯.取(网址).下(务(应答) {
  控制台.日志('工作了 :)')
  回 应答.数据;
}).下(务(数据) {
  控制台.日志(象谱.串(数据));
}).接(务(错) {
  控制台.日志(错);
});
控制台.日志('都作了!');

系统将会执行这些代码,看见第一个控制台.日志() 输出(开始) ,然后创建网址 变量。

然后,它将移动到下一行并开始执行阿修斯.取()块,但是,因为阿修斯.取()是异步执行的,没有阻塞,所以在承诺相关代码之后程序继续执行,从而到达最后的控制台.日志()语句(都作了!)并将其输出到控制台。

只有当阿修斯.取() 块完成运行返回结果给.下() ,我们才最后看到第二个控制台.日志() 消息 (工作了 :)) . 所以 这些消息 可能以 和你预期不同的顺序出现:

  • 开始
  • 都作了!
  • 工作了 🙂

如果你感到疑惑,考虑下面这个小例子:

引 * 作 秘传 自 '秘传';

控制台.日志('准备读取数据')

秘传.取('https://git.jtu.net.cn/xuexi/jtjb/-/raw/master/书.json', (应答) => {
  控制台.日志('读取数据有返回了')
});

控制台.日志('都做了')

这在行为上非常相似——第一个和第三个控制台.日志()消息将立即显示,但是第二个消息将被阻塞,直到获取资源有返回。

小结

在最基本的形式中,君土脚本是一种同步的、阻塞的、单线程的语言,在这种语言中,一次只能执行一个操作。但脚本运行环境定义了函数和编程接口,允许我们当某些事件发生时不是按照同步方式,而是异步地调用函数(比如,时间的推移,用户通过鼠标的交互,或者获取网络数据)。这意味着您的代码可以同时做几件事情,而不需要停止或阻塞主线程。

异步还是同步执行代码,取决于我们要做什么。

有些时候,我们希望事情能够立即加载并发生。例如,当将一些用户定义的样式应用到一个页面时,您希望这些样式能够尽快被应用。

但是,如果我们正在运行一个需要时间的操作,比如查询数据库并使用结果填充模板,那么最好将该操作从主线程中移开使用异步完成任务。随着时间的推移,您将了解何时选择异步技术比选择同步技术更有意义。