JavaScript für ungeduldige Programmierer (ES2022-Ausgabe)
Bitte unterstützen Sie dieses Buch: kaufen Sie es oder spenden Sie
(Werbung, bitte nicht blockieren.)

42 Asynchrone Iteration



  Erforderliches Wissen

Für dieses Kapitel sollten Sie vertraut sein mit

42.1 Grundlegende asynchrone Iteration

42.1.1 Protokoll: asynchrone Iteration

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;
}

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

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).

42.1.2 Verwendung von asynchroner Iteration direkt

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 });
}

42.1.3 Verwendung von asynchroner Iteration über for-await-of

Das 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.

42.2 Asynchrone Generatoren

Ein asynchroner Generator ist zwei Dinge gleichzeitig

  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();
}

42.2.1 Beispiel: Erstellen eines asynchronen Iterables über einen asynchronen Generator

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();

42.2.2 Beispiel: Konvertieren eines synchronen Iterables in ein asynchrones Iterable

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).

42.2.3 Beispiel: Konvertieren eines asynchronen Iterables in ein Array

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.

42.2.4 Beispiel: Transformieren eines asynchronen Iterables

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.

42.2.5 Beispiel: Mappen über asynchrone Iterables

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

42.3 Asynchrone Iteration über Node.js-Streams

42.3.1 Node.js-Streams: asynchron über Callbacks (Push)

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.

42.3.2 Node.js-Streams: asynchron über asynchrone Iteration (Pull)

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.

42.3.3 Beispiel: von Chunks zu Zeilen

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',
  ]);