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

异步函数是指其执行已被启动但未等待结果的函数。当执行完成时,异步函数会触发一个事件,并通过该事件传递其结果。
这种运行模式非常适合在网页浏览器中执行。事实上,在浏览器中运行的应用程序就是事件驱动型应用程序:应用程序会对事件做出反应,这些事件主要由用户触发(点击、鼠标移动、文本输入等)。 在浏览器中运行的 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] 进行安装:

[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] 的参数。这些参数也可以使用其他名称;
- [Promise] 构造函数执行两项操作:
- 它会创建一个事件来触发作为参数传递的 [function(resolve, reject)] 函数的执行,但不会等待其结果,而是立即将一个 [Promise] 对象返回给调用方。该对象可能处于以下四种状态:
- [pending]:返回 [Promise] 的异步操作尚未完成;
- [fulfilled]:返回 [Promise] 的异步操作已成功完成;
- [rejected]:返回 [Promise] 的异步操作已失败;
- [settled]:返回 [Promise] 的异步操作已完成;
- 它会创建一个事件来触发作为参数传递的 [function(resolve, reject)] 函数的执行,但不会等待其结果,而是立即将一个 [Promise] 对象返回给调用方。该对象可能处于以下四种状态:
当构造函数返回结果时,生成的 [Promise] 对象处于 [pending] 状态,等待异步函数的结果;
- (待续)
- 第 2 至 5 行中的异步任务会立即启动。异步任务通常是输入/输出任务,其工作原理如下:
- 执行同步代码以启动与另一个组件(例如远程服务器)的 I/O 操作;
- 等待该组件的响应;
- 处理该响应;
- 第 2 至 5 行中的异步任务会立即启动。异步任务通常是输入/输出任务,其工作原理如下:
第二阶段——等待外部组件——是最耗费资源的。与其等待:
- 由事件来通知已收到从外部组件请求的数据;
- 在紧随第一阶段之后的同步代码中(示例代码第 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] 作为关联数据——通常是一个详细描述发生错误的对象;
- 因此,调用方必须订阅这两个事件,以便在异步函数的结果可用时收到通知;
- 一旦第2行和第5行的异步任务启动并完成工作,它就可以使用[Promise]构造函数作为参数传递给它的两个函数[resolve, reject]向调用代码返回结果。约定如下:
当封装在 [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] 对象构造函数的代码:
第 2 行:启动 [Promise] 的异步任务。它通常需要的参数不止封装它的函数传递的 [resolve, reject] 参数。在此情况下,我们将 [Promise] 的创建封装在一个函数中,该函数会向其传递异步函数所需的参数:
以下脚本:
- 定义了两个返回 [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