Meistern Sie die asynchrone Iteration in JavaScript mit der 'for await...of'-Schleife und benutzerdefinierten Async-Iterator-Helfern. Verbessern Sie die Stream-Verarbeitung und Datenhandhabung mit praktischen Beispielen.
JavaScript Async-Iterator-Helfer: For Each – Iteration bei der Stream-Verarbeitung
Asynchrone Programmierung ist ein Eckpfeiler der modernen JavaScript-Entwicklung und ermöglicht es Anwendungen, zeitaufwändige Operationen durchzuführen, ohne den Haupt-Thread zu blockieren. Asynchrone Iteratoren, eingeführt in ECMAScript 2018, bieten einen leistungsstarken Mechanismus zur asynchronen Verarbeitung von Datenströmen. Dieser Blogbeitrag befasst sich mit dem Konzept der asynchronen Iteratoren und zeigt, wie man eine asynchrone 'for each'-Hilfsfunktion implementiert, um die Stream-Verarbeitung zu optimieren.
Asynchrone Iteratoren verstehen
Ein asynchroner Iterator ist ein Objekt, das der AsyncIterator-Schnittstelle entspricht. Er definiert eine next()-Methode, die ein Promise zurückgibt, welches zu einem Objekt mit zwei Eigenschaften aufgelöst wird:
value: Der nächste Wert in der Sequenz.done: Ein boolescher Wert, der anzeigt, ob der Iterator abgeschlossen ist.
Asynchrone Iteratoren werden häufig verwendet, um Daten aus asynchronen Quellen wie Netzwerk-Streams, Dateisystemen oder Datenbanken zu konsumieren. Die for await...of-Schleife bietet eine bequeme Syntax, um über asynchrone Iterables zu iterieren.
Beispiel: Asynchrones Lesen aus einer Datei
Stellen Sie sich ein Szenario vor, in dem Sie eine große Datei Zeile für Zeile lesen müssen, ohne den Haupt-Thread zu blockieren. Dies können Sie mit einem asynchronen Iterator erreichen:
const fs = require('fs');
const readline = require('readline');
async function* readFileLines(filePath) {
const fileStream = fs.createReadStream(filePath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
for await (const line of rl) {
yield line;
}
}
async function processFile(filePath) {
for await (const line of readFileLines(filePath)) {
console.log(`Line: ${line}`);
}
}
// Example usage
processFile('path/to/your/file.txt');
In diesem Beispiel ist readFileLines eine asynchrone Generatorfunktion, die jede Zeile der Datei ausgibt, sobald sie gelesen wird. Die processFile-Funktion iteriert dann mit for await...of über die Zeilen und verarbeitet jede Zeile asynchron.
Implementierung eines asynchronen 'For Each'-Helfers
Obwohl die for await...of-Schleife nützlich ist, kann sie umständlich werden, wenn Sie komplexe Operationen auf jedem Element im Stream durchführen müssen. Eine asynchrone 'for each'-Hilfsfunktion kann diesen Prozess vereinfachen, indem sie die Iterationslogik kapselt.
Grundlegende Implementierung
Hier ist eine grundlegende Implementierung einer asynchronen 'for each'-Funktion:
async function asyncForEach(iterable, callback) {
for await (const item of iterable) {
await callback(item);
}
}
Diese Funktion nimmt ein asynchrones Iterable und eine Callback-Funktion als Argumente entgegen. Sie iteriert mit for await...of über das Iterable und ruft die Callback-Funktion für jedes Element auf. Die Callback-Funktion sollte ebenfalls asynchron sein, wenn Sie auf deren Abschluss warten möchten, bevor Sie zum nächsten Element übergehen.
Beispiel: Datenverarbeitung von einer API
Angenommen, Sie rufen Daten von einer API ab, die einen Stream von Elementen zurückgibt. Sie können den asynchronen 'for each'-Helfer verwenden, um jedes Element zu verarbeiten, sobald es ankommt:
async function* fetchDataStream(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
return;
}
// Assuming the API returns JSON chunks
const chunk = decoder.decode(value);
const items = JSON.parse(`[${chunk.replace(/\}\{/g, '},{')}]`); //Split chunks into valid json array
for(const item of items){
yield item;
}
}
} finally {
reader.releaseLock();
}
}
async function processItem(item) {
// Simulate an asynchronous operation
await new Promise(resolve => setTimeout(resolve, 100));
console.log(`Processing item: ${JSON.stringify(item)}`);
}
async function main() {
const apiUrl = 'https://api.example.com/data'; // Replace with your API endpoint
await asyncForEach(fetchDataStream(apiUrl), processItem);
console.log('Finished processing data.');
}
// Example usage
main();
In diesem Beispiel ruft fetchDataStream Daten von der API ab und gibt jedes Element aus, sobald es empfangen wird. Die processItem-Funktion simuliert eine asynchrone Operation auf jedem Element. Der asyncForEach-Helfer vereinfacht dann die Iterations- und Verarbeitungslogik.
Erweiterungen und Überlegungen
Fehlerbehandlung
Es ist entscheidend, Fehler zu behandeln, die während der asynchronen Iteration auftreten können. Sie können die Callback-Funktion in einen try...catch-Block einbetten, um Ausnahmen abzufangen und zu behandeln:
async function asyncForEach(iterable, callback) {
for await (const item of iterable) {
try {
await callback(item);
} catch (error) {
console.error(`Error processing item: ${item}`, error);
// You can choose to re-throw the error or continue processing
}
}
}
Gleichzeitigkeitssteuerung
Standardmäßig verarbeitet der asynchrone 'for each'-Helfer Elemente sequenziell. Wenn Sie Elemente gleichzeitig verarbeiten müssen, können Sie einen Promise-Pool verwenden, um die Anzahl der gleichzeitigen Operationen zu begrenzen:
async function asyncForEachConcurrent(iterable, callback, concurrency) {
const executing = [];
for await (const item of iterable) {
const p = callback(item).then(() => executing.splice(executing.indexOf(p), 1));
executing.push(p);
if (executing.length >= concurrency) {
await Promise.race(executing);
}
}
await Promise.all(executing);
}
async function processItem(item) {
// Simulate an asynchronous operation
await new Promise(resolve => setTimeout(resolve, 100));
console.log(`Processing item: ${JSON.stringify(item)}`);
}
async function main() {
const apiUrl = 'https://api.example.com/data'; // Replace with your API endpoint
await asyncForEachConcurrent(fetchDataStream(apiUrl), processItem, 5); // Concurrency of 5
console.log('Finished processing data.');
}
In diesem Beispiel begrenzt asyncForEachConcurrent die Anzahl der gleichzeitigen Callback-Ausführungen auf das angegebene Gleichzeitigkeitslevel. Dies kann die Leistung bei der Verarbeitung großer Datenströme verbessern.
Abbruch
In einigen Fällen müssen Sie den Iterationsprozess möglicherweise vorzeitig abbrechen. Dies können Sie mit einem AbortController erreichen:
async function asyncForEach(iterable, callback, signal) {
for await (const item of iterable) {
if (signal && signal.aborted) {
console.log('Iteration aborted.');
return;
}
await callback(item);
}
}
async function main() {
const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => {
controller.abort(); // Abort after 2 seconds
}, 2000);
const apiUrl = 'https://api.example.com/data'; // Replace with your API endpoint
await asyncForEach(fetchDataStream(apiUrl), processItem, signal);
console.log('Finished processing data.');
}
In diesem Beispiel prüft die asyncForEach-Funktion vor jeder Iteration die Eigenschaft signal.aborted. Wenn das Signal abgebrochen wird, wird die Iteration gestoppt.
Anwendungen in der Praxis
Asynchrone Iteratoren und der asynchrone 'for each'-Helfer können in einer Vielzahl von realen Szenarien angewendet werden:
- Datenverarbeitungs-Pipelines: Verarbeitung großer Datensätze aus Datenbanken oder Dateisystemen.
- Echtzeit-Datenströme: Verarbeitung von Daten aus Web-Sockets, Nachrichtenwarteschlangen oder Sensornetzwerken.
- API-Nutzung: Abrufen und Verarbeiten von Daten von APIs, die Streams von Elementen zurückgeben.
- Bild- und Videoverarbeitung: Verarbeitung großer Mediendateien in Chunks.
- Protokollanalyse: Analyse großer Protokolldateien Zeile für Zeile.
Beispiel – Internationale Aktiendaten: Stellen Sie sich eine Anwendung vor, die Echtzeit-Aktienkurse von verschiedenen internationalen Börsen abruft. Ein asynchroner Iterator kann verwendet werden, um die Daten zu streamen, und ein asynchrones 'for each' kann jeden Kurs verarbeiten und die Benutzeroberfläche mit den neuesten Preisen aktualisieren. Dies kann verwendet werden, um aktuelle Aktienkurse von Unternehmen wie den folgenden anzuzeigen:
- Tencent (China): Abrufen von Aktiendaten eines großen internationalen Technologieunternehmens
- Tata Consultancy Services (Indien): Anzeigen von Aktien-Updates eines führenden IT-Dienstleistungsunternehmens
- Samsung Electronics (Südkorea): Darstellung von Aktienkursen eines globalen Elektronikherstellers
- Toyota Motor Corporation (Japan): Überwachung der Aktienkurse eines internationalen Automobilherstellers
Fazit
Asynchrone Iteratoren und der asynchrone 'for each'-Helfer bieten eine leistungsstarke und elegante Möglichkeit, Datenströme in JavaScript asynchron zu verarbeiten. Durch die Kapselung der Iterationslogik können Sie Ihren Code vereinfachen, die Lesbarkeit verbessern und die Leistung Ihrer Anwendungen steigern. Durch die Behandlung von Fehlern, die Steuerung der Gleichzeitigkeit und die Möglichkeit des Abbruchs können Sie robuste und skalierbare asynchrone Datenverarbeitungs-Pipelines erstellen.