Skip to content

11. 事件驱动编程与异步函数

Image

异步函数是指其执行已被启动但未等待结果的函数。当执行完成时,异步函数会触发一个事件,并通过该事件传递其结果。

这种运行模式非常适合在网页浏览器中执行。事实上,在浏览器中运行的应用程序就是事件驱动型应用程序:应用程序会对事件做出反应,这些事件主要由用户触发(点击、鼠标移动、文本输入等)。 在浏览器中运行的 JavaScript 应用程序通过 HTTP 协议与外部服务进行交互。JavaScript 的原生 HTTP 函数是异步的:它们被启动后,当收到所请求外部服务的响应时,会通过一个事件发出信号,该事件会被添加到应用程序管理的事件集合中。

以下脚本将由 [node.js] 执行,而非由浏览器执行。[node.js] 同样采用事件驱动的执行模型:

  • [node.js] 与浏览器类似,使用事件循环来执行脚本;
  • 脚本的主代码执行是第一个被处理的事件;
  • 如果该主代码启动了异步任务,则执行将持续进行直至这些异步任务完成。这些任务在完成时会触发事件。这些事件会被排队加入事件循环;
  • 若主脚本希望获取异步操作的结果,则必须订阅这些事件;
  • 在脚本发出的所有事件均被处理完毕之前,脚本不会结束;

11.1. 脚本 [async-01]

以下脚本演示了包含异步操作的脚本的行为。


'use strict';
 
// imports
import moment from 'moment';
import { sprintf } from 'sprintf-js';
 
// start
const débutScript = moment(Date.now());
console.log("[début du script],", heure());
 
// setTimeout arms a 1000 ms timer (2nd parameter) and immediately returns the timer number
// when the timer has run out of 1000 ms, it emits an event which is queued by the runtime
// when the event is processed by the runtime, the function (1st parameter) is executed
setTimeout(function () {
  // this code will be executed when the timer reaches value 0
  console.log("[fin de l'action asynchrone setTimeout],", heure(débutScript));
}, 1000)
 
// will be displayed before the internal timer function msg
console.log("[fin du code principal du script],", heure(débutScript));
 
// time and duration display utility
function heure(début) {
  // current time
  const now = moment(Date.now());
  // time formatting
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  // is it necessary to calculate a duration?
  if (début) {
    const durée = now - début;
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // format time + duration
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // result
  return result;
}
  • 第 4 行:我们导入 [moment] 库来格式化日期(第 27 行);
  • 第 5 行:导入 [sprintf-js] 库以格式化时长(第 34 行);
  • 第 8 行:记录脚本的开始时间;
  • 第 9 行:我们使用第 20–34 行中的 [time] 方法显示该时间;
  • 第 14–17 行:[setTimeout] 函数有两个参数(f, duration):f 是一个函数,当经过 [duration] 毫秒后执行;
  • 第 14 行:脚本运行时,[setTimeout] 函数被执行:
    • 第 17 行:启动一个 1000 毫秒的计时器,并开始倒计时,直至最终到达 0。一旦计时器初始化并开始倒计时,[setTimeout] 函数即完成执行。它不会等待倒计时结束。它返回所用计时器的 ID 号,随后执行流程移至下一条指令(第 20 行)。在此处,[setTimeout] 的结果并未被使用;
  • 第 16 行:该消息将在 [setTimeout] 函数的 1000 毫秒延迟结束后显示;
  • 第 15–16 行:作为 [setTimeout] 函数第一个参数的函数 f,将在 1000 毫秒延迟结束后执行。随后将显示第 16 行中的消息;
  • 第 20 行:该消息将在第 16 行的消息之前显示;

时间函数:

  • 第 23 行:该函数接受一个可选参数 [time],即需显示其持续时间的操作的开始时间;
  • 第25–27行:计算并格式化当前时间;
  • 第 29 行:如果存在 [start] 参数,则必须计算持续时间;
  • 第 30 行:计算操作的持续时间。结果为毫秒数;
  • 第 31–32 行:将该毫秒数拆分为秒和毫秒;
  • 第 34 行:将持续时间加到时间上;

执行


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-01.js"
[début du script], heure=09:26:40:238
[fin du code principal du script], heure=09:26:40:246, durée= 0 seconde(s) et 11 millisecondes
[fin de l'action asynchrone setTimeout], heure=09:26:41:249, durée= 1 seconde(s) et 14 millisecondes
 
[Done] exited with code=0 in 1.672 seconds
  • 第 4 行,我们可以看到异步操作 [setTimeout] 在脚本主代码结束约 1 秒后完成;
  • 第6行:第3行显示的时间是主代码完成的时间。如果主代码已启动异步任务,则在所有异步任务执行完毕之前,脚本不会完成。第6行显示的时长是脚本的总执行时间(主代码 + 异步任务);

[setTimeout] 函数允许我们在 [node.js] 环境中模拟异步任务。事实上,[setTimeout] 函数的行为就像一个异步任务:

  • 它会立即返回一个结果——在此情况下为定时器 ID——使用标准的函数机制(return);
  • 它可能在稍后(上述情况尚未发生)通过事件返回其他结果,这些结果随后由 [node.js] 事件循环处理;
  • 在后续的大多数情况下,将存在两个此类事件:
    • 一个可称为 [success] 的事件,由成功完成任务的异步任务触发。该事件关联着一段数据——即任务的结果;
    • 一个可称为 [failure] 的事件,由未能完成任务的异步任务触发。该事件关联着一段数据——通常是一个描述错误的对象。例如,异步网络任务可能出现的错误包括“网络不可用”、“服务器不存在”、“超时”等;
  • 启动异步任务的主代码可以订阅该任务可能发出的事件。当其中一个事件被发出时,主代码会收到通知,并可触发专门用于处理该事件的特定函数的执行。该函数会接收与所发事件关联的异步任务的数据作为参数;

11.2. 脚本 [async-02]

在此脚本中,异步函数 [setTimeout] 将发布事件,以将数据传递给已订阅这些事件的代码。

访问 [node.js] 事件需要额外的库。我们选择 [events] 库,并通过 [npm] 进行安装:

Image

[async-02] 脚本如下:


'use strict';
 
// asynchronous functions can return a result by emitting an event
// the main code can retrieve these results by subscribing to the events issued
 
// imports
import moment from 'moment';
import { sprintf } from 'sprintf-js';
import EventEmitter from 'events';
 
// start
const débutScript = moment(Date.now());
console.log("[début du script],", heure());
// an event transmitter
const eventEmitter = new EventEmitter();
 
// setTimeout sets a 1000 ms timer (2nd parameter) and immediately returns the timer number
// when the timer has run out of 1000 ms, it emits an event which is queued by the runtime
// when the event is processed by the runtime, the function (1st parameter) is executed
setTimeout(function () {
  // this code will be executed when the timer reaches value 0
  console.log("[setTimeout, fin du timer d'1 s],", heure(débutScript));
  // an event is issued to indicate that a result is available
  eventEmitter.emit("timer1Success", { success: 4 });
  // another event is issued to indicate that another result is available
  eventEmitter.emit("timer1Failure", { failure: 6 });
}, 1000)
 
// subscribe to evt [timer1Success]
eventEmitter.on('timer1Success', (result) => {
  console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Success]", result, heure(débutScript)));
});
 
// subscribe to evt [timer1Failure]
eventEmitter.on('timer1Failure', (result) => {
  console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Failure]", result, heure(débutScript)));
});
 
// will be displayed before the msg of evts issued by the function associated with [timer1]
console.log("[fin du code principal du script],", heure(débutScript));
 
// time and duration display utility
function heure(début) {
  // current time
  const now = moment(Date.now());
  // time formatting
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  // is it necessary to calculate a duration?
  if (début) {
    const durée = now - début;
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // format time + duration
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // result
  return result;
}

注释

  • 第 9 行:我们从 [events] 库中导入了 [EventEmitter] 类。这是新的内容:此前,我们只导入过字面量对象和函数;
  • 第 15 行:我们通过 [new] 关键字实例化 [EventEmitter] 类,创建了一个 [node.js] 事件发射器;
  • 第 20–27 行:异步函数 [setTimeout]。执行时,它将触发两个事件:
    • 第 24 行,[timer1Success] 事件,其关联值为对象 {success: 4};
    • 第 26 行,[timer1Failure] 事件,其关联值为对象 {failure: 6};
    • 一个异步函数可以发出任意数量的事件。我们之前提到,通常它只会发出 [success] 或 [failure] 其中一个事件,而不是像我们这里这样同时发出两个;
  • 第 20 行:[setTimeout] 的执行是即时的:定时器被设置,其 ID 返回给调用代码。事件将在稍后触发,本例中为 1 秒后;
  • 如果事件发生时没有代码来处理它们,触发事件就没有意义。这就是为什么主代码必须订阅这两个事件 [timer1Success, timer1Failure] 才能处理它们,特别是为了获取与这些事件关联的数据;
  • 第 30–32 行:主代码订阅了 [timer1Success] 事件。当 [node.js] 事件监听器处理此事件时,它将调用 [eventEmitter.on] 方法的第二个参数所指定的函数,并向其传递与 [timer1Success] 事件相关的数据(此处称为 [result]);
  • 第 31 行:事件处理函数将显示与该事件相关的数据的 JSON 格式以及当前时间;
  • 第 35–37 行:使用类似的代码,主代码订阅了 [timer1Failure] 事件;
  • 订阅事件(第一个参数)不会立即执行 [callback] 函数(第二个参数)中的代码。该函数仅在事件发生后才会被执行;
  • 第 40 行:主脚本代码已执行完毕,但脚本本身尚未结束,因为主代码启动了一个异步任务。在该异步任务完成之前,整个脚本不会结束;

结果如下所示:


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-02.js"
[début du script], heure=09:34:58:909
[fin du code principal du script], heure=09:34:58:916, durée= 0 seconde(s) et 10 millisecondes
[setTimeout, fin du timer d'1 s], heure=09:34:59:929, durée= 1 seconde(s) et 23 millisecondes
la fonction asynchrone du timer a rendu le résultat [{"success":4}], heure=09:34:59:931, durée= 1 seconde(s) et 25 millisecondes, via l'événement [timer1Success]
la fonction asynchrone du timer a rendu le résultat [{"failure":6}], heure=09:34:59:932, durée= 1 seconde(s) et 26 millisecondes, via l'événement [timer1Failure]
 
[Done] exited with code=0 in 1.627 seconds
 
  • 第 3 行:脚本启动 10 毫秒后,主代码结束;
  • 第 4 行:封装在 1000 毫秒定时器中的函数开始,距脚本启动约 1 秒;
  • 第 5 行:处理 [‘timer1Success’] 事件,2 毫秒后;
  • 第 6 行:处理 [‘timer1Failure’] 事件,发生在 [‘timer1Success’] 事件之后 1 毫秒;
  • 第 8 行:全局脚本结束,总时长为 1.627 秒;

11.3. 脚本 [async-03]

以下脚本演示了 [node.js] 事件循环的另一个方面:

  • 该循环依次处理事件,通常按事件到达的顺序进行。某些操作系统会为事件分配优先级,此时事件将按优先级顺序而非到达顺序进行处理;
  • 该循环每次仅处理一个事件。只有在前一个事件完成后,才会处理下一个事件。因此,在事件驱动系统中,应避免编写长期独占处理器的代码,因为事件不会在发生时立即被处理,而是等到事件循环到达该位置时才处理。这会导致应用程序“响应”不灵敏;

脚本 [async-03] 演示了这一现象的示例:


'use strict';
 
// asynchronous functions can return a result by emitting an event
// the main code can retrieve these results by subscribing to the events issued
 
// imports
import moment from 'moment';
import { sprintf } from 'sprintf-js';
import EventEmitter from 'events';
 
// start
const débutScript = moment(Date.now());
console.log("[début du script],", heure());
// an event transmitter
const eventEmitter = new EventEmitter();
 
// setTimeout sets a 1000 ms timer (2nd parameter) and immediately returns the timer number
// when the timer has run out of 1000 ms, it emits an event which is queued by the runtime
// when the event is processed by the runtime, the function (1st parameter) is executed
setTimeout(function () {
  // this code will be executed when the timer reaches value 0
  console.log("[setTimeout, fin du timer d'1 s],", heure(débutScript));
  // an event is issued to indicate that a result is available
  eventEmitter.emit("timer1Success", { success: 4 });
  // another event is issued to indicate that another result is available
  eventEmitter.emit("timer1Failure", { failure: 6 });
}, 1000)
 
// subscribe to evt [timer1Success]
eventEmitter.on('timer1Success', (result) => {
  console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Success]", result, heure(débutScript)));
});
 
// subscribe to evt [timer1Failure]
eventEmitter.on('timer1Failure', (result) => {
  console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Failure]", result, heure(débutScript)));
});

// slightly intensive synchronous code that prevented the main code from completing before the end of [timer1]
for (let i = 0; i < 1000000; i++) {
  for (let j = 0; j < 10000; j++) {
    i + i ^ 2 + i ^ 3;
  }
}
 
// will be displayed before the msg of evts issued by the function associated with [timer1]
console.log("[fin du script],", heure(débutScript));
 
// time and duration display utility
function heure(début) {
 ...
}

注释

  • 此代码源自前一个示例 [async-02],并在其中添加了第 39–44 行;
  • 第 20–27 行:[setTimeout] 函数已被编程为在延迟一秒后执行一个内部异步函数。一秒钟过去后,定时器的异步函数并不会立即执行:系统会在执行循环中放置一个事件来请求执行。如果执行循环正忙于处理另一个事件,定时器的异步函数就必须等待;
  • 第 20–27 行:一旦 [setTimeout] 函数设置好一秒延迟的定时器,它便释放处理器并控制权交还给调用代码。调用代码继续执行第 30–37 行,这些是事件订阅,其执行时间可以忽略不计;
  • 主代码随后执行第40–44行,该部分构成一个包含1,010次迭代的循环。当定时器触发“1秒延迟结束”事件时,这段代码正处于执行中。该事件随后被放入事件循环,但必须等待脚本的主代码执行完毕,才有机会被处理;
  • 第 47 行:脚本主代码的结尾。只有在完成这最后的显示操作后,才能处理定时器结束事件并执行 [setTimeout] 的内部异步函数;

该脚本产生以下结果:


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-03.js"
[début du script], heure=08:55:02:665
[fin du code principal du script], heure=08:55:11:789, durée= 9 seconde(s) et 131 millisecondes
[setTimeout, fin du timer d'1 s], heure=08:55:11:794, durée= 9 seconde(s) et 136 millisecondes
la fonction asynchrone du timer a rendu le résultat [{"success":4}], heure=08:55:11:794, durée= 9 seconde(s) et 136 millisecondes, via l'événement [timer1Success]
la fonction asynchrone du timer a rendu le résultat [{"failure":6}], heure=08:55:11:794, durée= 9 seconde(s) et 136 millisecondes, via l'événement [timer1Failure]
 
[Done] exited with code=0 in 9.796 seconds

评论

  • 第 3 行:我们可以看到脚本的主体代码执行耗时 9 秒。在此期间发生的任何事件都被排入事件循环队列;
  • 第4行:我们看到[timer end]事件在主代码执行完毕5毫秒后被处理。该事件在脚本启动后约1秒被触发,但最终被处理前还需额外等待8秒;

本例的核心要点在于:在事件驱动系统中,代码绝不应长时间占用处理器资源。若存在执行耗时的同步代码,必须设法将其拆分为多个较短的异步任务,并通过事件信号来标记任务完成。

11.4. 脚本 [async-04]

[async-04] 脚本演示了另一种机制,即 [Promise](结果承诺)。该机制避免了显式处理 [node.js] 事件的必要性。它通过隐式方式进行处理,因此开发者可以忽略这些事件的存在。不过,理解这些事件将有助于开发者更好地掌握 [Promise] 的工作原理,因为乍看之下它相当复杂。

[Promise] 类型是 JavaScript 中的一个类。其构造函数接受一个异步函数作为参数,并向该函数传递两个传统上称为 [resolve][reject] 的参数。这些参数也可以使用其他名称;

1
2
3
4
5
6
7
const promise=new Promise(function(resolve, reject){
     // an asynchronous task is launched
    
     // if successful: call resolve(result) where [result] is the result of the asynchronous task;
     // if unsuccessful: call reject(error) where [error] is an object encapsulating the error encountered;
}
// subscribe to events issued by the [Promise] asynchronous task
  • [Promise] 构造函数执行两项操作:
    • 它会创建一个事件来触发作为参数传递的 [function(resolve, reject)] 函数的执行,但不会等待其结果,而是立即将一个 [Promise] 对象返回给调用方。该对象可能处于以下四种状态:
      • [pending]:返回 [Promise] 的异步操作尚未完成;
      • [fulfilled]:返回 [Promise] 的异步操作已成功完成;
      • [rejected]:返回 [Promise] 的异步操作已失败;
      • [settled]:返回 [Promise] 的异步操作已完成;

当构造函数返回结果时,生成的 [Promise] 对象处于 [pending] 状态,等待异步函数的结果;

  • (待续)
    • 第 2 至 5 行中的异步任务会立即启动。异步任务通常是输入/输出任务,其工作原理如下:
      1. 执行同步代码以启动与另一个组件(例如远程服务器)的 I/O 操作;
      2. 等待该组件的响应;
      3. 处理该响应;

第二阶段——等待外部组件——是最耗费资源的。与其等待:

  • 由事件来通知已收到从外部组件请求的数据;
  • 在紧随第一阶段之后的同步代码中(示例代码第 7 行),我们将订阅该事件,随后在某个时刻返回 [node.js] 事件循环。此时,待处理事件列表中的下一个事件将被处理;
  • 在第二阶段,虽然存在并行执行,但发生在不同的设备上:
    • 事件循环的处理器;
    • 用于检索请求数据的外部组件(磁盘、数据库、远程服务器);
  • 在第 2 阶段结束时,一旦 I/O 操作获取了请求的数据,将触发一个事件以指示 I/O 结果已可用。该事件随后将加入事件队列中的其他事件;
  • 轮到它时,该事件将被处理。随后将执行与该事件关联的函数(示例代码第 7 行);

这种工作模式有助于避免停机:即处理器等待比自身运行速度更慢的设备响应的情况;

  • (续)
    • 一旦第2行和第5行的异步任务启动并完成工作,它就可以使用[Promise]构造函数作为参数传递给它的两个函数[resolve, reject]向调用代码返回结果。约定如下:
      • 异步任务通过 [resolve(result)] 信号成功。这相当于向 [node.js] 事件循环中添加一个名为 [resolved] 的事件,并将 [result] 作为关联数据;
      • 异步任务通过 [reject(error)] 信号失败。这相当于向 [node.js] 事件循环中添加一个名为 [rejected] 的事件,并将 [error] 作为关联数据——通常是一个详细描述发生错误的对象;
      • 因此,调用方必须订阅这两个事件,以便在异步函数的结果可用时收到通知;

当封装在 [Promise] 中的异步任务执行完毕后,由 [Promise(…)] 构造函数返回的 [promise] 对象的状态会发生变化:

  • [resolved] 事件将其状态从 [pending] 更改为 [resolved]
  • [rejected] 事件的状态从 [pending] 变为 [rejected]

订阅异步任务的 [resolved][rejected] 事件,需使用 [Promise] 类的相关方法,语法如下:

promise.then(f1).catch(f2).finally(f3);

其中:

  • f1 是在 [promise] 状态从 [pending] 变为 [resolved] 时执行的函数,即当异步任务成功完成其工作时。它接收值 [result] 作为参数,该参数由异步任务的 [resolve(result)] 语句传递;
  • f2 是在 [promise] 的状态从 [pending] 变为 [rejected] 时执行的函数,即当异步任务未能完成其工作时。它接收由异步任务的 [reject(error)] 语句传递的 [error] 值作为参数;
  • f3 是在 [then] [catch] 方法执行完毕后执行的函数,因此它总是会被执行。它不接收任何参数;

这种语法完全隐藏了我们订阅的事件。然而,这仍是一种订阅,与前一个示例中的情况一样,它不会立即执行函数 [f1, f2, f3]。这些函数将在我们订阅的事件 [resolved, rejected] 中的任一事件发生时被执行——或者不被执行。

[async-04] 脚本演示了这一机制:


'use strict';
 
// it is possible to obtain the results (success, failure) of an asynchronous function
// without explicitly using events thanks to the [Promise] class
// this class implicitly uses events, but these are not visible in the code
 
// imports
import moment from 'moment';
import { sprintf } from 'sprintf-js';
 
// start
const débutScript = moment(Date.now());
console.log("[début du script],", heure(débutScript));
 
// definition of an asynchronous task using a promise [Promise]
// the asynchronous task is the [Promise] constructor parameter
const débutPromise1 = moment(Date.now());
const promise1 = new Promise(function (resolve) {
  // log
  console.log("[début fonction asynchrone de promise1],", heure(débutPromise1));
  // asynchronous code
  setTimeout(function () {
    console.log("[fin fonction asynchrone de promise1],", heure(débutPromise1));
    // the asynchronous task returns a result with the [resolve] function
    // the promise is fulfilled
    resolve('[réussite]');
  }, 1000)
});
 
// we can know the result of the promise [promise1]
// when it has been resolved or rejected
// the following instruction is a subscription to the [resolved] event via the [then] method
// and to the [rejected] event via the [catch] method
// the [finally] method is executed whether after a then or a catch
promise1.then(result => {
  // promise success stories [evt resolved]
  console.log(sprintf("[promise1.then], %s, result=%s", heure(débutPromise2), result));
}).catch(result => {
  // error case [evt rejected]
  console.log(sprintf("[promise1.catch], %s, result=%s", heure(débutPromise2), result));
}).finally(() => {
  // executed in all cases
  console.log("[promise1.finally]", heure(débutPromise1));
});
 
// definition of an asynchronous task using a promise [Promise]
const débutPromise2 = moment(Date.now());
const promise2 = new Promise(function (resolve, reject) {
  // log
  console.log("[début fonction asynchrone de promise2],", heure(débutPromise1));
  // asynchronous task
  setTimeout(function () {
    console.log("[fin fonction asynchrone de promise2],", heure(débutPromise2));
    // the asynchronous task returns a result with the [reject] function
    // the promise is lost
    reject('[échec]');
  }, 2000)
});
 
// we can know the result of the promise [promise2]
// when it has been resolved or rejected
promise2.then(result => {
  // promise success stories [evt resolved]
  console.log(sprintf("[promise2.then], %s, result=%s", heure(débutPromise2), result));
}).catch(result => {
  // error case [evt rejected]
  console.log(sprintf("[promise2.catch], %s, result=%s", heure(débutPromise2), result));
}).finally(() => {
  // executed in all cases
  console.log(sprintf("[promise2.finally], %s", heure(débutPromise2)));
});
 
// will be displayed before the msg of asynchronous functions and those of associated events
console.log("[fin du code principal du script],", heure(débutScript));
 
// utility
function heure(début) {
  // current time
  const now = moment(Date.now());
  // time formatting
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  if (début) {
    const durée = now - début;
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // formatting duration
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // result
  return result;
}

注释

  • 第 18–28 行:创建一个 [Promise promise1]。其异步函数将在一秒后通过一个事件返回结果。一旦该异步操作启动(计时器开始运行),我们不会等待其返回结果,而是立即执行第 35 行的代码;
  • 第 35–44 行:我们订阅 [promise1] 的内部异步函数可能发出的两个事件 [resolved, rejected]
  • 第 46–71 行:针对第二个 Promise [promise2],我们重复了与之前相同的代码序列;
  • 第 74 行:脚本的主体部分已执行完毕,但整个脚本尚未结束,因为有两个异步操作已被启动。我们返回事件循环,在某个时刻,[promise1, promise2] [resolved, rejected] 事件之一将会发生。届时该事件将被处理;
  • 随后我们返回事件循环。在那里,当第二个 [resolved, rejected] 事件发生时,将对其进行处理;

执行


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-04.js"
[début du script], heure=09:39:05:950, durée= 0 seconde(s) et 3 millisecondes
[début fonction asynchrone de promise1], heure=09:39:05:958, durée= 0 seconde(s) et 0 millisecondes
[début fonction asynchrone de promise2], heure=09:39:05:959, durée= 0 seconde(s) et 1 millisecondes
[fin du code principal du script], heure=09:39:05:960, durée= 0 seconde(s) et 13 millisecondes
[fin fonction asynchrone de promise1], heure=09:39:06:977, durée= 1 seconde(s) et 19 millisecondes
[promise1.then], heure=09:39:06:980, durée= 1 seconde(s) et 21 millisecondes, result=[réussite]
[promise1.finally] heure=09:39:06:982, durée= 1 seconde(s) et 24 millisecondes
[fin fonction asynchrone de promise2], heure=09:39:07:976, durée= 2 seconde(s) et 17 millisecondes
[promise2.catch], heure=09:39:07:978, durée= 2 seconde(s) et 19 millisecondes, result=[échec]
[promise2.finally], heure=09:39:07:980, durée= 2 seconde(s) et 21 millisecondes
 
[Done] exited with code=0 in 2.589 seconds

注释

  • 第 3 行:[promise1] 的异步函数被调用,但我们不等待其完成,其完成将通过一个事件来通知;
  • 第 4 行:[promise2] 的异步函数被调用,但我们不等待其完成,其完成将通过一个事件来通知;
  • 第 5 行:主代码结束,返回事件循环;
  • 第 6 行:处理 [promise1 的异步函数结束] 事件。[promise1] 的状态将变为 [resolved]。一个事件会通知这一点;
  • 第 7 行:由于 [promise2] 尚未完成工作,刚刚加入循环的 [promise1 resolved] 事件将由 [promise1.then] 方法处理,随后由 [promise.finally] 方法处理(第 8 行);
  • 第 9–11 行:当 [promise2] [pending] 状态变为 [resolved] 时,也会发生同样的机制;

11.5. 脚本 [async-05]

让我们回到 [Promise] 对象构造函数的代码:

1
2
3
4
5
6
7
8
9
'use strict'; 

const promise=new Promise(function(resolve, reject){
// an asynchronous task is launched
// …
// if successful: call resolve(result) where [result] is the result of the asynchronous task;
// if unsuccessful: call reject(error) where [error] is an object encapsulating the error encountered;
}
// subscribe to events issued by the asynchronous task

第 2 行:启动 [Promise] 的异步任务。它通常需要的参数不止封装它的函数传递的 [resolve, reject] 参数。在此情况下,我们将 [Promise] 的创建封装在一个函数中,该函数会向其传递异步函数所需的参数:

'use strict';

// definition of the asynchronous function
function uneFonctionAsynchrone (p1, p2, , pn){
 return new Promise(function(resolve, reject){
     // an asynchronous task is launched with parameters (P1, p2, ..., pn)
    // …
     // if successful: call resolve(result) where [result] is the result of the asynchronous task;
     // if unsuccessful: call reject(error) where [error] is an object encapsulating the error encountered;
}
// subscribe to [resolved, rejected] events to be sent by the asynchronous function [uneFonctionAsynchrone]

// some time later, the asynchronous function [uneFonctionAsynchrone] is called
uneFonctionAsynchrone(e1, e2, , en) ;

以下脚本:

  • 定义了两个返回 [Promise] 的异步函数;
  • 并行启动它们的执行,并在两者都完成后才执行某项任务;

'use strict';
 
// we can define asynchronous functions that render a [Promise] type
// they can then be tagged with the keyword [async]
 
// imports
import moment from 'moment';
import { sprintf } from 'sprintf-js';
 
// start
const débutScript = moment(Date.now());
console.log("[début du script],", heure());
 
// an asynchronous function that renders a promise [Promise]
function async01(p1) {
  return new Promise((resolve) => {
    console.log("[début de la tâche asynchrone async01]");
    // the asynchronous task
    const débutAsync01 = moment(Date.now());
    setTimeout(function () {
      console.log("[fin de la tâche asynchrone async01],", heure(débutAsync01));
      // the asynchronous task can render a complex result
      resolve({
        prop1: [10, 20, 30],
        prop2: "abcd",
        prop3: p1,
      });
    }, 1000)
  });
}
 
// an asynchronous function that renders a promise [Promise]
function async02(p1, p2) {
  return new Promise(resolve => {
    console.log("[début de la tâche asynchrone async02]");
    // asynchronous task
    const débutAsync02 = moment(Date.now());
    setTimeout(function () {
      console.log("[fin de la tâche asynchrone async02],", heure(débutAsync02));
      // the asynchronous task can render a complex result
      resolve({
        prop1: [11, 21, 31],
        prop2: "xyzt",
        prop3: p1 + p2
      });
    }, 2000)
  })
}
 
// run the two asynchronous functions in parallel
// and wait for them both to finish
// the then executes only if both functions have issued the [resolved] event
// the catch is executed as soon as one of the two functions issues the [rejected] event
Promise.all([async01(10), async02(10, 20)])
  // the result is an array [result1, result2] where [result1] is the result emitted by a [resolve] of [async01]
  // and [result2] the result emitted by a [resolve] of [async02]
  .then(result => {
    console.log(sprintf("[promise-all success], %s, result=%j", heure(débutScript), result));
  })
  // error is the result emitted by the first [reject] of one of the two asynchronous functions
  .catch(error => {
    console.log(sprintf("[promise-all error], %s, erreur=%j", heure(débutScript), error));
  })
  // finally is executed after then or catch
  .finally(() => {
    console.log(sprintf("[promise-all finally], %s", heure(débutScript)));
  });
 
// will be displayed before msgs for asynchronous functions and associated events
console.log("[fin du code principal du script],", heure(débutScript));
 
// utility
function heure(début) {
  // current time
  const now = moment(Date.now());
  // time formatting
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  if (début) {
    const durée = now - début;
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // formatting duration
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // result
  return result;
}

注释

  • 第 15–30 行:我们定义了一个函数 [async01],该函数通过定时器事件在 1 秒后返回结果。函数 [async01] 在第 26 行的 result 中被调用;
  • 第33–47行:我们对函数 [async02] 进行类似操作,该函数通过定时器事件在2秒后返回结果。函数 [async02] 接受两个参数,这些参数在第44行的结果中被使用;
  • 当调用 [async01] 和 [async02] 这两个函数时:
    • 将被启动;
    • 将向调用方返回两个 Promise [promise1, promise2]
    • 随后执行控制将返回给调用代码,并继续运行;
    • 大约 1 秒后,[async01] 将触发一个事件,表示其已完成工作。该事件将与 [async01] 传递的结果一同排入与其关联的事件循环中;
    • 大约 2 秒后,[async02] 也将经历相同的过程;
  • 第 54 行:此时才开始执行异步函数 [async01, async02](即 async01(10) 和 async02(10,20))。它们是在作为 [Promise.all] 方法参数传递的数组内执行的。 我们知道 [async01, async02] 都会向调用代码返回一个 Promise。因此,[Promise.all] 的参数是一个包含两个 Promise 的数组;
  • [Promise.all([promise1, promise2, …, promiseN]).then(f1).catch(f2).finally(f3)] 是一种事件订阅:
    • [Promise.all] 的类型为 [Promise]
    • [all] 方法参数数组中的所有 Promise [promise1, promise2, …, promisen] 均从 [pending] 状态转为 [resolved] 状态时,[then] 方法中的函数 [f1] 将会被执行。换言之,当数组中的所有 Promise 均成功完成时,[f1] 将会被执行;
    • [catch] 方法中的 [f2] 函数将在数组中的任意一个 Promise 从 [pending] 状态变为 [rejected] 状态时立即执行。换言之,当数组中的任意一个 Promise 失败时,[f2] 就会被执行;
    • [finally] 方法的 [f3] 函数将在 [then] 或 [catch] 方法中的任一方法执行完毕后执行,因此它总是会被执行;

执行代码后将得到以下结果:


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-05.js"
[début du script], heure=12:17:17:367
[début de la tâche asynchrone async01]
[début de la tâche asynchrone async02]
[fin du code principal du script], heure=12:17:17:375, durée= 0 seconde(s) et 10 millisecondes
[fin de la tâche asynchrone async01], heure=12:17:18:391, durée= 1 seconde(s) et 17 millisecondes
[fin de la tâche asynchrone async02], heure=12:17:19:389, durée= 2 seconde(s) et 14 millisecondes
[promise-all success], heure=12:17:19:390, durée= 2 seconde(s) et 25 millisecondes, result=[{"prop1":[10,20,30],"prop2":"abcd","prop3":10},{"prop1":[11,21,31],"prop2":"xyzt","prop3":30}]
[promise-all finally], heure=12:17:19:392, durée= 2 seconde(s) et 27 millisecondes
 
[Done] exited with code=0 in 2.572 seconds
  • 第 6-7 行:启动了两个异步任务 [async01, async02]。它们并行运行。并非它们的代码执行是并行的,而是它们各自等待请求数据的过程同时发生;
  • 第 5 行:脚本的主体部分已完成。现在只需等待两个异步任务 [async01, async02] 完成;
  • 第 6 行:异步任务 [async01] 在启动后约 1 秒完成。它使用 [resolve] 函数返回结果,因此代码第 56 行数组中的该 Promise 状态从 [pending] 变为 [resolved]。但这还不足以触发代码第 59–60 行的 [then] 方法;
  • 第 7 行:异步任务 [async02] 在启动后约 2 秒完成。它通过 [resolve] 函数返回结果,因此代码第 56 行数组中的该 Promise 状态从 [pending] 变为 [resolved][then] 方法将在事件循环允许时立即执行;
  • 第 8 行: ,[Promise.all] [then] 方法被执行。它接收一个参数数组 [result1, result2],其中 [result1] [async01] 返回的结果,[result2] [async02] 返回的结果;
  • 第 9 行:执行 [Promise.all] [finally] 方法;

11.6. 脚本 [async-06]

这个新脚本演示了如何通过结合使用 [async / await] 关键字,使异步代码看起来像同步代码。事件处理被完全隐藏,使得代码更易于理解。

我们重新审视前面的示例,并进行以下修改:

  • 我们添加了第三个异步函数 [async03],该函数通过 [Promise.reject] 方法返回结果,从而向事件循环发出信号,表明其任务“失败”了;
  • 我们依次执行这三个异步函数 [async01, async02, async03]。在之前的示例中,我们曾并行执行异步函数 [async01, async02]
  • 在引入 [async/await] 关键字之前,异步操作的顺序执行是通过嵌套的 [Promise] 对象实现的。每当需要以这种方式执行多个异步操作时,Promise 的数量就会相应增加,导致代码可读性下降;
  • 借助 [async/await] 关键字,异步任务的顺序执行采用了与同步任务执行类似的语法:

// asynchronous function - using async / await
async function main() {
  // sequential execution of asynchronous tasks
  try {
    // execution while waiting for [async01]
    const result1 = await async01(...);
    console.log("[async01 result]=", result1);
    // execution while waiting for [async02]
    const result2 = await async02(...);
    console.log("[async02 result]=", result2);
    // execution while waiting for [async03]
    const result3 = await async03(...);
    console.log("[async03 result]=", result3);
  } catch (error) {
    // one of the asynchronous actions has failed
    console.log(sprintf("[sequential error]= %j, %s", error));
  } finally {
    // completed
    console.log("[fin exécution séquentielle des tâches asynchrones],");
  }
  • 第 6 行:启动异步函数 [async01](使用 await 关键字),并等待其通过 [Promise.resolve, Promise.reject] 中的任一方法返回结果。因此这是一项阻塞操作;
  • 第 6 行:[await] 关键字将异步操作 [async01] 转换为阻塞操作。我们知道 [async01] 操作有两种返回结果的方式:
    • 它几乎立即向调用代码返回一个 [Promise] 对象;
    • 随后通过 [Promise.resolve, Promise.reject] 方法将结果发布到事件循环中。第 6 行中 [result1] 获取的正是后者。对 [async01] 操作的事件驱动处理已变得不可见;
    • 如果 [async01] 的结果 [result] 是通过 [Promise.resolve(result)] 发布的,则该结果在第 6 行被赋值给 [result1],执行继续进行到第 7 行;
    • 如果 [async01] 的结果通过 [Promise.reject] 解析,这将触发一个异常,代码执行将跳转至第 14 行,即 catch 代码块catch 子句的参数是 [async01] 通过表达式 [Promise.reject(error)] 解析得到的错误对象 (error)。 异步任务也可以通过 [throw(error)] 抛出错误。[error] 对象即是在 [catch(error)] 中捕获到的那个对象;
    • [await] 关键字必须位于以 [async] 关键字开头的函数内部(第 2 行)。该关键字表明 [main] 函数是一个异步函数;
    • 在表达式 [await f(…)] 中,[f] 必须是一个异步函数,该函数向调用方返回一个 [Promise] 对象;
  • 对于第 9 行的异步操作 [async02] 和第 12 行的 [async03],我们采用相同的方式处理;

继续使用 [async / await] 关键字,可以通过以下语法并行执行异步任务:


try {
    // parallel execution of asynchronous tasks
    const result = await Promise.all([async01(...), async02(...), async03(...)]);
    console.log(sprintf("[parallel success], %s, result=%j", heure(débutParallel), result));
  } catch (error) {
    // one of the asynchronous actions has failed
    console.log(sprintf("[parallel error], %s, erreur=%j", heure(débutParallel), error));
  } finally {
    // completed
    console.log(sprintf("[fin exécution parallèle des tâches asynchrones],%s", heure(débutParallel)));
}
  • 第 3 行:这里有一个阻塞操作:我们等待数组 [async01(..), async02(..), async03(..)] 中的三个异步任务通过 [Promise.resolve, Promise.reject] 中的任一方法在事件循环中发布其结果;
  • 如果这三个异步任务使用 [Promise.resolve] 发布结果,则常量 [result] 即为数组 [result1, result2, result3],其中:
    • [result1] [async01] 通过表达式 [Promise.resolve(result1)] 发布的结果;
    • [result2] [async02] 通过表达式 [Promise.resolve(result2)] 发布的结果;
    • [result3] [async03] 通过表达式 [Promise.resolve(result3)] 发布的结果;
  • 如果这三个任务中的任何一个使用表达式 [Promise.reject(error)] 发布其结果,则会引发异常;
    • 第 3 行中的常量 [result] 未被赋值;
    • 执行直接跳转到第 5 行的 [catch] 代码块
    • catch 块中的 (error) 参数是表达式 [Promise.reject(error)] 发布的 (error) 对象;

通过结合这两种语法,我们可以使用与同步代码类似的语法,以顺序或并行方式执行异步任务。 因此,我们应优先采用这种语法,它比之前的语法更易于阅读。这种 [async/await] 语法直到 ECMAScript 6 版本才出现。仍有大量 JavaScript 代码使用 Promise [Promise]。这就是为什么理解它们的工作原理同样重要。

[async-06] 脚本的完整代码如下:


'use strict';
 
// parallel or sequential execution of several asynchronous tasks
// with the keywords async / await
 
// imports
import moment from 'moment';
import { sprintf } from 'sprintf-js';
 
// start
const débutScript = moment(Date.now());
console.log("[début du code principal du script],", heure());

// an asynchronous function rendering a [Promise]
function async01(débutAsync01) {
  return new Promise(function (resolve) {
    console.log("[début fonction asynchrone async01],", heure());
    // asynchronous function
    setTimeout(function () {
      console.log("[fin fonction asynchrone async01],", heure(débutAsync01));
      // asynchronous action can make a result complex
      // here success
      resolve({
        prop1: [11, 21, 31],
        prop2: "abcd"
      });
    }, 1000)
  });
}
 
// an asynchronous function rendering a [Promise]
function async02(débutAsync02) {
  console.log("[début fonction asynchrone async02],", heure());
  return new Promise(function (resolve) {
    // asynchronous function
    setTimeout(function () {
      console.log("[fin fonction asynchrone async02],", heure(débutAsync02));
      // asynchronous action can make a result complex
      // here success
      resolve({
        prop1: [12, 22, 32],
        prop2: "xyzt"
      });
    }, 2000)
  })
}
 
// an asynchronous function rendering a [Promise]
function async03(débutAsync03) {
  console.log("[début fonction asynchrone async03],", heure());
  return new Promise((resolve, reject) => {
    // asynchronous function
    setTimeout(function () {
      console.log("[fin fonction asynchrone async03],", heure(débutAsync03));
      // asynchronous action can make a result complex
      // here failure
      reject({
        prop1: [13, 23, 33],
        prop2: "échec"
      });
    }, 3000)
  })
}
 
// asynchronous function - using async / await
async function main() {
  const débutSequential = moment(Date.now());
  // sequential execution of asynchronous tasks
  console.log("------------ exécution séquentielle des tâches asynchrones lancée ------------------------")
  try {
    // execution while waiting for [async01]
    const débutAsync01 = moment(Date.now());
    const result1 = await async01(débutAsync01);
    console.log("[async01 result]=", result1);
    // execution while waiting for [async02]
    const débutAsync02 = moment(Date.now());
    console.log("début async02-------------", heure());
    const result2 = await async02(débutAsync02);
    console.log("[async02 result]=", result2);
    // execution while waiting for [async03]
    const débutAsync03 = moment(Date.now());
    console.log("début async03-------------", heure());
    const result3 = await async03(débutAsync03);
    console.log("[async03 result]=", result3);
  } catch (error) {
    // one of the asynchronous actions has failed
    console.log(sprintf("[sequential error]= %j, %s", error, heure(débutSequential)));
  } finally {
    // completed
    console.log("[fin exécution séquentielle des tâches asynchrones],", heure(débutSequential));
  }
 
  const débutParallel = moment(Date.now());
  // parallel execution of asynchronous tasks
  console.log("------------ exécution parallèle des tâches asynchrones lancée ------------------------")
  try {
    const result = await Promise.all([async01(débutParallel), async02(débutParallel), async03(débutParallel)]);
    console.log(sprintf("[parallel success], %s, result=%j", heure(débutParallel), result));
  } catch (error) {
    // one of the asynchronous actions has failed
    console.log(sprintf("[parallel error], %s, erreur=%j", heure(débutParallel), error));
  } finally {
    // completed
    console.log(sprintf("[fin exécution parallèle des tâches asynchrones],%s", heure(débutParallel)));
  }
 
  // completed
  console.log("[fin de la fonction main],", heure(débutSequential));
}
// execution asynchronous function main
main();
 
// will be displayed before the various msgs for asynchronous functions and their events
console.log("[fin du code principal du script],", heure(débutScript));
 
// utility
function heure(début) {
  // current time
  const now = moment(Date.now());
  // time formatting
  let result = "heure=" + now.format("HH:mm:ss:SSS");
  if (début) {
    const durée = now - début;
    const milliseconds = durée % 1000;
    const seconds = Math.floor(durée / 1000);
    // formatting duration
    result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
  }
  // result
  return result;
}

执行结果如下:


[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-06.js"
[début du code principal du script], heure=15:02:00:152
------------ exécution séquentielle des tâches asynchrones lancée ------------------------
[début fonction asynchrone async01], heure=15:02:00:161
[fin du code principal du script], heure=15:02:00:164, durée= 0 seconde(s) et 15 millisecondes
[fin fonction asynchrone async01], heure=15:02:01:165, durée= 1 seconde(s) et 4 millisecondes
[async01 result]= { prop1: [ 11, 21, 31 ], prop2: 'abcd' }
début async02------------- heure=15:02:01:253
[début fonction asynchrone async02], heure=15:02:01:254
[fin fonction asynchrone async02], heure=15:02:03:265, durée= 2 seconde(s) et 12 millisecondes
[async02 result]= { prop1: [ 12, 22, 32 ], prop2: 'xyzt' }
début async03------------- heure=15:02:03:268
[début fonction asynchrone async03], heure=15:02:03:268
[fin fonction asynchrone async03], heure=15:02:06:285, durée= 3 seconde(s) et 18 millisecondes
[sequential error]= {"prop1":[13,23,33],"prop2":"échec"}, heure=15:02:06:289, durée= 6 seconde(s) et 129 millisecondes
[fin exécution séquentielle des tâches asynchrones], heure=15:02:06:291, durée= 6 seconde(s) et 131 millisecondes
------------ exécution parallèle des tâches asynchrones lancée ------------------------
[début fonction asynchrone async01], heure=15:02:06:292
[début fonction asynchrone async02], heure=15:02:06:293
[début fonction asynchrone async03], heure=15:02:06:294
[fin fonction asynchrone async01], heure=15:02:07:294, durée= 1 seconde(s) et 2 millisecondes
[fin fonction asynchrone async02], heure=15:02:08:298, durée= 2 seconde(s) et 6 millisecondes
[fin fonction asynchrone async03], heure=15:02:09:297, durée= 3 seconde(s) et 5 millisecondes
[parallel error], heure=15:02:09:298, durée= 3 seconde(s) et 6 millisecondes, erreur={"prop1":[13,23,33],"prop2":"échec"}
[fin exécution parallèle des tâches asynchrones],heure=15:02:09:299, durée= 3 seconde(s) et 7 millisecondes
[fin de la fonction main], heure=15:02:09:300, durée= 9 seconde(s) et 140 millisecondes
 
[Done] exited with code=0 in 9.668 seconds