Erforderliches Wissen
Für dieses Kapitel sollten Sie vertraut sein mit
Um zu verstehen, wie asynchrone Iteration funktioniert, lassen Sie uns zuerst die synchrone Iteration rekapitulieren. Sie umfasst die folgenden Schnittstellen
interface Iterable<T> {
[Symbol.iterator]() : Iterator<T>;
}
interface Iterator<T> {
next() : IteratorResult<T>;
}
interface IteratorResult<T> {
value: T;
done: boolean;
}Iterable ist eine Datenstruktur, deren Inhalte über Iteration zugänglich sind. Es ist eine Fabrik für Iteratoren.Iterator ist eine Fabrik für Iterationsergebnisse, die wir durch Aufruf der Methode .next() abrufen.IterationResult enthält den iterierten .value und einen booleschen Wert .done, der nach dem letzten Element true und davor false ist.Für das Protokoll der asynchronen Iteration möchten wir nur eine Sache ändern: Die Werte, die von .next() erzeugt werden, sollen asynchron geliefert werden. Es gibt zwei denkbare Optionen
.value könnte ein Promise<T> enthalten..next() könnte Promise<IteratorResult<T>> zurückgeben.Anders ausgedrückt, die Frage ist, ob nur Werte oder ganze Iterationsergebnisse in Promises verpackt werden sollen.
Es muss Letzteres sein, denn wenn .next() ein Ergebnis zurückgibt, startet es eine asynchrone Berechnung. Ob diese Berechnung einen Wert liefert oder das Ende der Iteration signalisiert, kann erst nach Abschluss bestimmt werden. Daher müssen sowohl .done als auch .value in ein Promise verpackt werden.
Die Schnittstellen für asynchrone Iteration sehen wie folgt aus.
interface AsyncIterable<T> {
[Symbol.asyncIterator]() : AsyncIterator<T>;
}
interface AsyncIterator<T> {
next() : Promise<IteratorResult<T>>; // (A)
}
interface IteratorResult<T> {
value: T;
done: boolean;
}Der einzige Unterschied zu den synchronen Schnittstellen ist der Rückgabetyp von .next() (Zeile A).
Der folgende Code verwendet das asynchrone Iterationsprotokoll direkt
const asyncIterable = syncToAsyncIterable(['a', 'b']); // (A)
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
// Call .next() until .done is true:
asyncIterator.next() // (B)
.then(iteratorResult => {
assert.deepEqual(
iteratorResult,
{ value: 'a', done: false });
return asyncIterator.next(); // (C)
})
.then(iteratorResult => {
assert.deepEqual(
iteratorResult,
{ value: 'b', done: false });
return asyncIterator.next(); // (D)
})
.then(iteratorResult => {
assert.deepEqual(
iteratorResult,
{ value: undefined, done: true });
})
;In Zeile A erstellen wir ein asynchrones Iterable über die Werte 'a' und 'b'. Eine Implementierung von syncToAsyncIterable() werden wir später sehen.
Wir rufen .next() in Zeile B, Zeile C und Zeile D auf. Jedes Mal verwenden wir .then(), um das Promise zu entpacken, und assert.deepEqual(), um den entpackten Wert zu überprüfen.
Wir können diesen Code vereinfachen, wenn wir eine async-Funktion verwenden. Nun entpacken wir Promises mit await, und der Code sieht fast so aus, als würden wir synchrone Iteration durchführen
async function f() {
const asyncIterable = syncToAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
// Call .next() until .done is true:
assert.deepEqual(
await asyncIterator.next(),
{ value: 'a', done: false });
assert.deepEqual(
await asyncIterator.next(),
{ value: 'b', done: false });
assert.deepEqual(
await asyncIterator.next(),
{ value: undefined, done: true });
}for-await-ofDas asynchrone Iterationsprotokoll ist nicht für die direkte Verwendung gedacht. Eines der Sprachkonstrukte, das es unterstützt, ist die for-await-of-Schleife, die eine asynchrone Version der for-of-Schleife ist. Sie kann in async-Funktionen und asynchronen Generatoren (die später in diesem Kapitel eingeführt werden) verwendet werden. Dies ist ein Beispiel für die Verwendung von for-await-of
for await (const x of syncToAsyncIterable(['a', 'b'])) {
console.log(x);
}
// Output:
// 'a'
// 'b'for-await-of ist relativ flexibel. Neben asynchronen Iterables unterstützt es auch synchrone Iterables
for await (const x of ['a', 'b']) {
console.log(x);
}
// Output:
// 'a'
// 'b'Und es unterstützt synchrone Iterables über Werte, die in Promises verpackt sind
const arr = [Promise.resolve('a'), Promise.resolve('b')];
for await (const x of arr) {
console.log(x);
}
// Output:
// 'a'
// 'b' Übung: Konvertieren eines asynchronen Iterables in ein Array
Warnung: Die Lösung für diese Übung werden wir bald in diesem Kapitel sehen.
exercises/async-iteration/async_iterable_to_array_test.mjs
Ein asynchroner Generator ist zwei Dinge gleichzeitig
await und for-await-of verwenden, um Daten abzurufen.yield und yield* verwenden, um Daten zu produzieren. Asynchrone Generatoren ähneln synchronen Generatoren sehr
Da asynchrone und synchrone Generatoren sehr ähnlich sind, erkläre ich nicht, wie genau yield und yield* funktionieren. Konsultieren Sie bitte §38 „Synchrone Generatoren“, wenn Sie Zweifel haben.
Daher hat ein asynchroner Generator
Das sieht wie folgt aus
async function* asyncGen() {
// Input: Promises, async iterables
const x = await somePromise;
for await (const y of someAsyncIterable) {
// ···
}
// Output
yield someValue;
yield* otherAsyncGen();
}Schauen wir uns ein Beispiel an. Der folgende Code erstellt ein asynchrones Iterable mit drei Zahlen
async function* yield123() {
for (let i=1; i<=3; i++) {
yield i;
}
}Entspricht das Ergebnis von yield123() dem asynchronen Iterationsprotokoll?
async function check() {
const asyncIterable = yield123();
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
assert.deepEqual(
await asyncIterator.next(),
{ value: 1, done: false });
assert.deepEqual(
await asyncIterator.next(),
{ value: 2, done: false });
assert.deepEqual(
await asyncIterator.next(),
{ value: 3, done: false });
assert.deepEqual(
await asyncIterator.next(),
{ value: undefined, done: true });
}
check();Der folgende asynchrone Generator konvertiert ein synchrones Iterable in ein asynchrones Iterable. Er implementiert die Funktion syncToAsyncIterable(), die wir zuvor verwendet haben.
async function* syncToAsyncIterable(syncIterable) {
for (const elem of syncIterable) {
yield elem;
}
}Hinweis: Der Input ist in diesem Fall synchron (kein await erforderlich).
Die folgende Funktion ist eine Lösung für eine frühere Übung. Sie konvertiert ein asynchrones Iterable in ein Array (denken Sie an Spread-Syntax, aber für asynchrone Iterables anstelle von synchronen).
async function asyncIterableToArray(asyncIterable) {
const result = [];
for await (const value of asyncIterable) {
result.push(value);
}
return result;
}Beachten Sie, dass wir in diesem Fall keinen asynchronen Generator verwenden können: Wir erhalten unseren Input über for-await-of und geben ein Array zurück, das in ein Promise verpackt ist. Die letztere Anforderung schließt asynchrone Generatoren aus.
Dies ist ein Test für asyncIterableToArray()
async function* createAsyncIterable() {
yield 'a';
yield 'b';
}
const asyncIterable = createAsyncIterable();
assert.deepEqual(
await asyncIterableToArray(asyncIterable), // (A)
['a', 'b']
);Beachten Sie das await in Zeile A, das erforderlich ist, um das von asyncIterableToArray() zurückgegebene Promise zu entpacken. Damit await funktioniert, muss dieses Codefragment innerhalb einer async-Funktion ausgeführt werden.
Implementieren wir einen asynchronen Generator, der durch Transformation eines vorhandenen asynchronen Iterables ein neues asynchrones Iterable erzeugt.
async function* timesTwo(asyncNumbers) {
for await (const x of asyncNumbers) {
yield x * 2;
}
}Um diese Funktion zu testen, verwenden wir asyncIterableToArray() aus dem vorherigen Abschnitt.
async function* createAsyncIterable() {
for (let i=1; i<=3; i++) {
yield i;
}
}
assert.deepEqual(
await asyncIterableToArray(timesTwo(createAsyncIterable())),
[2, 4, 6]
); Übung: Asynchrone Generatoren
Warnung: Die Lösung für diese Übung werden wir bald in diesem Kapitel sehen.
exercises/async-iteration/number_lines_test.mjs
Zur Erinnerung, so mappt man über synchrone Iterables
function* mapSync(iterable, func) {
let index = 0;
for (const x of iterable) {
yield func(x, index);
index++;
}
}
const syncIterable = mapSync(['a', 'b', 'c'], s => s.repeat(3));
assert.deepEqual(
Array.from(syncIterable),
['aaa', 'bbb', 'ccc']);Die asynchrone Version sieht wie folgt aus
async function* mapAsync(asyncIterable, func) { // (A)
let index = 0;
for await (const x of asyncIterable) { // (B)
yield func(x, index);
index++;
}
}Beachten Sie, wie ähnlich die synchrone und die asynchrone Implementierung sind. Die einzigen beiden Unterschiede sind async in Zeile A und await in Zeile B. Das ist vergleichbar mit dem Wechsel von einer synchronen zu einer asynchronen Funktion – wir müssen nur das Schlüsselwort async und gelegentlich await hinzufügen.
Um mapAsync() zu testen, verwenden wir die Hilfsfunktion asyncIterableToArray() (zuvor in diesem Kapitel gezeigt)
async function* createAsyncIterable() {
yield 'a';
yield 'b';
}
const mapped = mapAsync(
createAsyncIterable(), s => s.repeat(3));
assert.deepEqual(
await asyncIterableToArray(mapped), // (A)
['aaa', 'bbb']);Auch hier verwenden wir await, um ein Promise zu entpacken (Zeile A), und dieses Codefragment muss innerhalb einer async-Funktion ausgeführt werden.
Übung:
filterAsyncIter()
exercises/async-iteration/filter_async_iter_test.mjs
Traditionell geschieht das asynchrone Lesen aus Node.js-Streams über Callbacks
function main(inputFilePath) {
const readStream = fs.createReadStream(inputFilePath,
{ encoding: 'utf8', highWaterMark: 1024 });
readStream.on('data', (chunk) => {
console.log('>>> '+chunk);
});
readStream.on('end', () => {
console.log('### DONE ###');
});
}Das heißt, der Stream hat die Kontrolle und pusht Daten an den Leser.
Ab Node.js 10 können wir auch asynchrone Iteration verwenden, um aus Streams zu lesen
async function main(inputFilePath) {
const readStream = fs.createReadStream(inputFilePath,
{ encoding: 'utf8', highWaterMark: 1024 });
for await (const chunk of readStream) {
console.log('>>> '+chunk);
}
console.log('### DONE ###');
}Diesmal hat der Leser die Kontrolle und zieht Daten aus dem Stream.
Node.js-Streams iterieren über Chunks (beliebig lange Datenstücke). Der folgende asynchrone Generator konvertiert ein asynchrones Iterable über Chunks in ein asynchrones Iterable über Zeilen
/**
* Parameter: async iterable of chunks (strings)
* Result: async iterable of lines (incl. newlines)
*/
async function* chunksToLines(chunksAsync) {
let previous = '';
for await (const chunk of chunksAsync) { // input
previous += chunk;
let eolIndex;
while ((eolIndex = previous.indexOf('\n')) >= 0) {
// line includes the EOL (Windows '\r\n' or Unix '\n')
const line = previous.slice(0, eolIndex+1);
yield line; // output
previous = previous.slice(eolIndex+1);
}
}
if (previous.length > 0) {
yield previous;
}
}Wenden wir chunksToLines() auf ein asynchrones Iterable über Chunks an (wie es von chunkIterable() erzeugt wird)
async function* chunkIterable() {
yield 'First\nSec';
yield 'ond\nThird\nF';
yield 'ourth';
}
const linesIterable = chunksToLines(chunkIterable());
assert.deepEqual(
await asyncIterableToArray(linesIterable),
[
'First\n',
'Second\n',
'Third\n',
'Fourth',
]);Nachdem wir nun ein asynchrones Iterable über Zeilen haben, können wir die Lösung einer früheren Übung, numberLines(), verwenden, um diese Zeilen zu nummerieren
async function* numberLines(linesAsync) {
let lineNumber = 1;
for await (const line of linesAsync) {
yield lineNumber + ': ' + line;
lineNumber++;
}
}
const numberedLines = numberLines(chunksToLines(chunkIterable()));
assert.deepEqual(
await asyncIterableToArray(numberedLines),
[
'1: First\n',
'2: Second\n',
'3: Third\n',
'4: Fourth',
]);