Deutsch

Erkunden Sie JavaScript Async Iterator Helpers, um die Stream-Verarbeitung zu revolutionieren. Lernen Sie, wie Sie asynchrone Datenströme mit map, filter, take, drop und mehr effizient handhaben.

JavaScript Async Iterator Helpers: Leistungsstarke Stream-Verarbeitung für moderne Anwendungen

In der modernen JavaScript-Entwicklung ist der Umgang mit asynchronen Datenströmen eine häufige Anforderung. Ob Sie Daten von einer API abrufen, große Dateien verarbeiten oder Echtzeit-Ereignisse behandeln – die effiziente Verwaltung asynchroner Daten ist entscheidend. Die Async Iterator Helpers von JavaScript bieten eine leistungsstarke und elegante Möglichkeit, diese Ströme zu verarbeiten, und ermöglichen einen funktionalen und zusammensetzbaren Ansatz zur Datenmanipulation.

Was sind Async Iterators und Async Iterables?

Bevor wir uns mit den Async Iterator Helpers befassen, wollen wir die zugrunde liegenden Konzepte verstehen: Async Iterators und Async Iterables.

Ein Async Iterable ist ein Objekt, das eine Methode zur asynchronen Iteration über seine Werte definiert. Dies geschieht durch die Implementierung der @@asyncIterator-Methode, die einen Async Iterator zurückgibt.

Ein Async Iterator ist ein Objekt, das eine next()-Methode bereitstellt. Diese Methode gibt ein Promise zurück, das zu einem Objekt mit zwei Eigenschaften aufgelöst wird:

Hier ist ein einfaches Beispiel:


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 500)); // Simuliert eine asynchrone Operation
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  for await (const value of asyncIterable) {
    console.log(value); // Ausgabe: 1, 2, 3, 4, 5 (mit 500ms Verzögerung zwischen jedem Wert)
  }
})();

In diesem Beispiel ist generateSequence eine asynchrone Generatorfunktion, die eine Sequenz von Zahlen asynchron erzeugt. Die for await...of-Schleife wird verwendet, um die Werte aus dem asynchronen Iterable zu konsumieren.

Einführung in Async Iterator Helpers

Async Iterator Helpers erweitern die Funktionalität von Async Iterators und bieten eine Reihe von Methoden zur Transformation, Filterung und Manipulation von asynchronen Datenströmen. Sie ermöglichen einen funktionalen und zusammensetzbaren Programmierstil, der den Aufbau komplexer Datenverarbeitungspipelines erleichtert.

Zu den wichtigsten Async Iterator Helpers gehören:

Schauen wir uns jeden Helper mit Beispielen an.

map()

Der map()-Helper transformiert jedes Element des asynchronen Iterables mithilfe einer bereitgestellten Funktion. Er gibt ein neues asynchrones Iterable mit den transformierten Werten zurück.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

const doubledIterable = asyncIterable.map(x => x * 2);

(async () => {
  for await (const value of doubledIterable) {
    console.log(value); // Ausgabe: 2, 4, 6, 8, 10 (mit 100ms Verzögerung)
  }
})();

In diesem Beispiel verdoppelt map(x => x * 2) jede Zahl in der Sequenz.

filter()

Der filter()-Helper wählt Elemente aus dem asynchronen Iterable basierend auf einer bereitgestellten Bedingung (Prädikatfunktion) aus. Er gibt ein neues asynchrones Iterable zurück, das nur die Elemente enthält, die die Bedingung erfüllen.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(10);

const evenNumbersIterable = asyncIterable.filter(x => x % 2 === 0);

(async () => {
  for await (const value of evenNumbersIterable) {
    console.log(value); // Ausgabe: 2, 4, 6, 8, 10 (mit 100ms Verzögerung)
  }
})();

In diesem Beispiel wählt filter(x => x % 2 === 0) nur die geraden Zahlen aus der Sequenz aus.

take()

Der take()-Helper gibt die ersten N Elemente aus dem asynchronen Iterable zurück. Er gibt ein neues asynchrones Iterable zurück, das nur die angegebene Anzahl von Elementen enthält.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

const firstThreeIterable = asyncIterable.take(3);

(async () => {
  for await (const value of firstThreeIterable) {
    console.log(value); // Ausgabe: 1, 2, 3 (mit 100ms Verzögerung)
  }
})();

In diesem Beispiel wählt take(3) die ersten drei Zahlen aus der Sequenz aus.

drop()

Der drop()-Helper überspringt die ersten N Elemente aus dem asynchronen Iterable und gibt den Rest zurück. Er gibt ein neues asynchrones Iterable zurück, das die verbleibenden Elemente enthält.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

const afterFirstTwoIterable = asyncIterable.drop(2);

(async () => {
  for await (const value of afterFirstTwoIterable) {
    console.log(value); // Ausgabe: 3, 4, 5 (mit 100ms Verzögerung)
  }
})();

In diesem Beispiel überspringt drop(2) die ersten beiden Zahlen aus der Sequenz.

toArray()

Der toArray()-Helper konsumiert das gesamte asynchrone Iterable und sammelt alle Elemente in einem Array. Er gibt ein Promise zurück, das zu einem Array mit allen Elementen aufgelöst wird.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const numbersArray = await asyncIterable.toArray();
  console.log(numbersArray); // Ausgabe: [1, 2, 3, 4, 5]
})();

In diesem Beispiel sammelt toArray() alle Zahlen aus der Sequenz in einem Array.

forEach()

Der forEach()-Helper führt eine bereitgestellte Funktion einmal für jedes Element im asynchronen Iterable aus. Er gibt *kein* neues asynchrones Iterable zurück, sondern führt die Funktion als Seiteneffekt aus. Dies kann nützlich sein, um Operationen wie das Protokollieren oder das Aktualisieren einer Benutzeroberfläche durchzuführen.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(3);

(async () => {
  await asyncIterable.forEach(value => {
    console.log("Value:", value);
  });
  console.log("forEach completed");
})();
// Ausgabe: Value: 1, Value: 2, Value: 3, forEach completed

some()

Der some()-Helper prüft, ob mindestens ein Element im asynchronen Iterable den von der bereitgestellten Funktion implementierten Test besteht. Er gibt ein Promise zurück, das zu einem booleschen Wert aufgelöst wird (true, wenn mindestens ein Element die Bedingung erfüllt, andernfalls false).


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const hasEvenNumber = await asyncIterable.some(x => x % 2 === 0);
  console.log("Has even number:", hasEvenNumber); // Ausgabe: Has even number: true
})();

every()

Der every()-Helper prüft, ob alle Elemente im asynchronen Iterable den von der bereitgestellten Funktion implementierten Test bestehen. Er gibt ein Promise zurück, das zu einem booleschen Wert aufgelöst wird (true, wenn alle Elemente die Bedingung erfüllen, andernfalls false).


async function* generateSequence(end) {
  for (let i = 2; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(4);

(async () => {
  const areAllEven = await asyncIterable.every(x => x % 2 === 0);
  console.log("Are all even:", areAllEven); // Ausgabe: Are all even: true
})();

find()

Der find()-Helper gibt das erste Element im asynchronen Iterable zurück, das die bereitgestellte Testfunktion erfüllt. Wenn keine Werte die Testfunktion erfüllen, wird undefined zurückgegeben. Er gibt ein Promise zurück, das zum gefundenen Element oder undefined aufgelöst wird.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const firstEven = await asyncIterable.find(x => x % 2 === 0);
  console.log("First even number:", firstEven); // Ausgabe: First even number: 2
})();

reduce()

Der reduce()-Helper führt eine vom Benutzer bereitgestellte „Reducer“-Callback-Funktion für jedes Element des asynchronen Iterables in der richtigen Reihenfolge aus und übergibt dabei den Rückgabewert aus der Berechnung des vorhergehenden Elements. Das Endergebnis der Ausführung des Reducers über alle Elemente ist ein einzelner Wert. Er gibt ein Promise zurück, das zum endgültigen akkumulierten Wert aufgelöst wird.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const sum = await asyncIterable.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
  console.log("Sum:", sum); // Ausgabe: Sum: 15
})();

Praktische Beispiele und Anwendungsfälle

Async Iterator Helpers sind in einer Vielzahl von Szenarien wertvoll. Schauen wir uns einige praktische Beispiele an:

1. Verarbeitung von Daten von einer Streaming-API

Stellen Sie sich vor, Sie erstellen ein Echtzeit-Datenvisualisierungs-Dashboard, das Daten von einer Streaming-API empfängt. Die API sendet kontinuierlich Aktualisierungen, und Sie müssen diese Aktualisierungen verarbeiten, um die neuesten Informationen anzuzeigen.


async function* fetchDataFromAPI(url) {
  let response = await fetch(url);

  if (!response.body) {
    throw new Error("ReadableStream wird in dieser Umgebung nicht unterstützt");
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) {
        break;
      }
      const chunk = decoder.decode(value);
      // Angenommen, die API sendet JSON-Objekte, die durch Zeilenumbrüche getrennt sind
      const lines = chunk.split('\n');
      for (const line of lines) {
        if (line.trim() !== '') {
          yield JSON.parse(line);
        }
      }
    }
  } finally {
    reader.releaseLock();
  }
}

const apiURL = 'https://example.com/streaming-api'; // Ersetzen Sie dies durch Ihre API-URL
const dataStream = fetchDataFromAPI(apiURL);

// Verarbeiten des Datenstroms
(async () => {
  for await (const data of dataStream.filter(item => item.type === 'metric').map(item => ({ timestamp: item.timestamp, value: item.value }))) {
    console.log('Verarbeitete Daten:', data);
    // Das Dashboard mit den verarbeiteten Daten aktualisieren
  }
})();

In diesem Beispiel ruft fetchDataFromAPI Daten von einer Streaming-API ab, parst die JSON-Objekte und gibt sie als asynchrones Iterable aus. Der filter-Helper wählt nur die Metriken aus, und der map-Helper transformiert die Daten in das gewünschte Format, bevor das Dashboard aktualisiert wird.

2. Lesen und Verarbeiten großer Dateien

Angenommen, Sie müssen eine große CSV-Datei mit Kundendaten verarbeiten. Anstatt die gesamte Datei in den Speicher zu laden, können Sie Async Iterator Helpers verwenden, um sie Stück für Stück zu verarbeiten.


async function* readLinesFromFile(filePath) {
  const file = await fsPromises.open(filePath, 'r');

  try {
    let buffer = Buffer.alloc(1024);
    let fileOffset = 0;
    let remainder = '';

    while (true) {
      const { bytesRead } = await file.read(buffer, 0, buffer.length, fileOffset);
      if (bytesRead === 0) {
        if (remainder) {
          yield remainder;
        }
        break;
      }

      fileOffset += bytesRead;
      const chunk = buffer.toString('utf8', 0, bytesRead);
      const lines = chunk.split('\n');

      lines[0] = remainder + lines[0];
      remainder = lines.pop() || '';

      for (const line of lines) {
        yield line;
      }
    }
  } finally {
    await file.close();
  }
}

const filePath = './customer_data.csv'; // Ersetzen Sie dies durch Ihren Dateipfad
const lines = readLinesFromFile(filePath);

// Verarbeiten der Zeilen
(async () => {
  for await (const customerData of lines.drop(1).map(line => line.split(',')).filter(data => data[2] === 'USA')) {
    console.log('Kunde aus den USA:', customerData);
    // Kundendaten aus den USA verarbeiten
  }
})();

In diesem Beispiel liest readLinesFromFile die Datei Zeile für Zeile und gibt jede Zeile als asynchrones Iterable aus. Der drop(1)-Helper überspringt die Kopfzeile, der map-Helper teilt die Zeile in Spalten auf, und der filter-Helper wählt nur Kunden aus den USA aus.

3. Handhabung von Echtzeit-Ereignissen

Async Iterator Helpers können auch zur Handhabung von Echtzeit-Ereignissen aus Quellen wie WebSockets verwendet werden. Sie können ein asynchrones Iterable erstellen, das Ereignisse bei ihrem Eintreffen ausgibt, und dann die Helper zur Verarbeitung dieser Ereignisse verwenden.


async function* createWebSocketStream(url) {
  const ws = new WebSocket(url);

  yield new Promise((resolve, reject) => {
      ws.onopen = () => {
          resolve();
      };
      ws.onerror = (error) => {
          reject(error);
      };
  });

  try {
    while (ws.readyState === WebSocket.OPEN) {
      yield new Promise((resolve, reject) => {
        ws.onmessage = (event) => {
          resolve(JSON.parse(event.data));
        };
        ws.onerror = (error) => {
          reject(error);
        };
        ws.onclose = () => {
           resolve(null); // Mit null auflösen, wenn die Verbindung schließt
        }
      });

    }
  } finally {
    ws.close();
  }
}

const websocketURL = 'wss://example.com/events'; // Ersetzen Sie dies durch Ihre WebSocket-URL
const eventStream = createWebSocketStream(websocketURL);

// Verarbeiten des Ereignisstroms
(async () => {
  for await (const event of eventStream.filter(event => event.type === 'user_login').map(event => ({ userId: event.userId, timestamp: event.timestamp }))) {
    console.log('Benutzer-Login-Ereignis:', event);
    // Benutzer-Login-Ereignis verarbeiten
  }
})();

In diesem Beispiel erstellt createWebSocketStream ein asynchrones Iterable, das von einem WebSocket empfangene Ereignisse ausgibt. Der filter-Helper wählt nur Benutzer-Login-Ereignisse aus, und der map-Helper transformiert die Daten in das gewünschte Format.

Vorteile der Verwendung von Async Iterator Helpers

Browser- und Laufzeitumgebungs-Unterstützung

Async Iterator Helpers sind noch ein relativ neues Feature in JavaScript. Stand Ende 2024 befinden sie sich in Stufe 3 des TC39-Standardisierungsprozesses, was bedeutet, dass sie wahrscheinlich in naher Zukunft standardisiert werden. Sie werden jedoch noch nicht nativ in allen Browsern und Node.js-Versionen unterstützt.

Browser-Unterstützung: Moderne Browser wie Chrome, Firefox, Safari und Edge fügen schrittweise Unterstützung für Async Iterator Helpers hinzu. Sie können die neuesten Informationen zur Browser-Kompatibilität auf Websites wie Can I use... überprüfen, um zu sehen, welche Browser dieses Feature unterstützen.

Node.js-Unterstützung: Neuere Versionen von Node.js (v18 und höher) bieten experimentelle Unterstützung für Async Iterator Helpers. Um sie zu verwenden, müssen Sie Node.js möglicherweise mit dem Flag --experimental-async-iterator ausführen.

Polyfills: Wenn Sie Async Iterator Helpers in Umgebungen verwenden müssen, die sie nicht nativ unterstützen, können Sie einen Polyfill verwenden. Ein Polyfill ist ein Stück Code, das die fehlende Funktionalität bereitstellt. Es sind mehrere Polyfill-Bibliotheken für Async Iterator Helpers verfügbar; eine beliebte Option ist die core-js-Bibliothek.

Implementierung benutzerdefinierter Async Iterators

Obwohl Async Iterator Helpers eine bequeme Möglichkeit zur Verarbeitung bestehender asynchroner Iterables bieten, müssen Sie manchmal Ihre eigenen benutzerdefinierten asynchronen Iteratoren erstellen. Dies ermöglicht es Ihnen, Daten aus verschiedenen Quellen wie Datenbanken, APIs oder Dateisystemen auf eine streaming-basierte Weise zu verarbeiten.

Um einen benutzerdefinierten asynchronen Iterator zu erstellen, müssen Sie die @@asyncIterator-Methode für ein Objekt implementieren. Diese Methode sollte ein Objekt mit einer next()-Methode zurückgeben. Die next()-Methode sollte ein Promise zurückgeben, das zu einem Objekt mit den Eigenschaften value und done aufgelöst wird.

Hier ist ein Beispiel für einen benutzerdefinierten asynchronen Iterator, der Daten von einer paginierten API abruft:


async function* fetchPaginatedData(baseURL) {
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const url = `${baseURL}?page=${page}`;
    const response = await fetch(url);
    const data = await response.json();

    if (data.results.length === 0) {
      hasMore = false;
      break;
    }

    for (const item of data.results) {
      yield item;
    }

    page++;
  }
}

const apiBaseURL = 'https://api.example.com/data'; // Ersetzen Sie dies durch Ihre API-URL
const paginatedData = fetchPaginatedData(apiBaseURL);

// Verarbeiten der paginierten Daten
(async () => {
  for await (const item of paginatedData) {
    console.log('Element:', item);
    // Das Element verarbeiten
  }
})();

In diesem Beispiel ruft fetchPaginatedData Daten von einer paginierten API ab und gibt jedes Element aus, sobald es abgerufen wird. Der asynchrone Iterator übernimmt die Paginierungslogik, was den Konsum der Daten auf eine streaming-basierte Weise erleichtert.

Mögliche Herausforderungen und Überlegungen

Obwohl Async Iterator Helpers zahlreiche Vorteile bieten, ist es wichtig, sich einiger potenzieller Herausforderungen und Überlegungen bewusst zu sein:

Best Practices für die Verwendung von Async Iterator Helpers

Um das Beste aus Async Iterator Helpers herauszuholen, sollten Sie die folgenden Best Practices beachten:

Fortgeschrittene Techniken

Zusammensetzen von benutzerdefinierten Helfern

Sie können Ihre eigenen benutzerdefinierten Async Iterator Helper erstellen, indem Sie vorhandene Helper zusammensetzen oder neue von Grund auf neu erstellen. Dies ermöglicht es Ihnen, die Funktionalität an Ihre spezifischen Bedürfnisse anzupassen und wiederverwendbare Komponenten zu erstellen.


async function* takeWhile(asyncIterable, predicate) {
  for await (const value of asyncIterable) {
    if (!predicate(value)) {
      break;
    }
    yield value;
  }
}

// Beispielverwendung:
async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(10);
const firstFive = takeWhile(asyncIterable, x => x <= 5);

(async () => {
  for await (const value of firstFive) {
    console.log(value);
  }
})();

Kombinieren mehrerer Async Iterables

Sie können mehrere asynchrone Iterables mit Techniken wie zip oder merge zu einem einzigen asynchronen Iterable kombinieren. Dies ermöglicht es Ihnen, Daten aus mehreren Quellen gleichzeitig zu verarbeiten.


async function* zip(asyncIterable1, asyncIterable2) {
    const iterator1 = asyncIterable1[Symbol.asyncIterator]();
    const iterator2 = asyncIterable2[Symbol.asyncIterator]();

    while (true) {
        const result1 = await iterator1.next();
        const result2 = await iterator2.next();

        if (result1.done || result2.done) {
            break;
        }

        yield [result1.value, result2.value];
    }
}

// Beispielverwendung:
async function* generateSequence1(end) {
    for (let i = 1; i <= end; i++) {
        yield i;
    }
}

async function* generateSequence2(end) {
    for (let i = 10; i <= end + 9; i++) {
        yield i;
    }
}

const iterable1 = generateSequence1(5);
const iterable2 = generateSequence2(5);

(async () => {
    for await (const [value1, value2] of zip(iterable1, iterable2)) {
        console.log(value1, value2);
    }
})();

Fazit

Die JavaScript Async Iterator Helpers bieten eine leistungsstarke und elegante Möglichkeit, asynchrone Datenströme zu verarbeiten. Sie ermöglichen einen funktionalen und zusammensetzbaren Ansatz zur Datenmanipulation, der den Aufbau komplexer Datenverarbeitungspipelines erleichtert. Durch das Verständnis der Kernkonzepte von Async Iterators und Async Iterables und die Beherrschung der verschiedenen Helper-Methoden können Sie die Effizienz und Wartbarkeit Ihres asynchronen JavaScript-Codes erheblich verbessern. Da die Unterstützung durch Browser und Laufzeitumgebungen weiter zunimmt, sind Async Iterator Helpers auf dem besten Weg, ein unverzichtbares Werkzeug für moderne JavaScript-Entwickler zu werden.