Async Iteratoren und Generatoren in JavaScript
for...of-Schleifn gehn über Arrays und ondere Iterables Wert für Wert. for await...of mocht des Gleiche, wenn jeder Wert Zeit zum Erzeugen braucht - paginierte API-Ergebnisse lesn, a große Datei streamen oda Events verarbeiten, wia sie einkemman - ohne dass du händisch mit Promises und Schleifenzustand jonglieren muasst. Da Mechanismus hinter beiden sand Generatoren, und sobois du oan schreiben kannst, wird a ganze Kategorie von "verarbeit diesen Strom an Sochn"-Code um einiges einfacher.
Normale Generatoren: pausierbare Funktionen
A Generatorfunktion (function*) kann sich mit yield söba pausieren und später weitermachen, wobei ihr lokaler Zustand zwischen den Fortsetzungen erhaltn bleibt.
function* countTo(n) {
for (let i = 1; i <= n; i++) {
yield i;
}
}
for (const value of countTo(3)) {
console.log(value); // 1, 2, 3
}
countTo(3) aufzurufen führt den Funktionskörper ned aus - es gibt an Iterator zruck. Jeder .next()-Aufruf (den for...of unter da Haubn macht) führt die Funktion bis zum nächsten yield aus, gibt den Wert zruck und pausiert wieda. Des is der ganze Mechanismus: a Funktion, de wiederaufgnommen wern kann, statt nur imma von Anfang bis Ende zum laufn.
Async Generatoren: Die söbe Idee, mit Warten
A async Generator (async function*) kann vorm Yielden awaitn, sodass jeder Wert von ana asynchronen Operation abhängn kann - Netzwerk-Request, Datenbankabfrage, Timer.
async function* fetchAllUsers() {
let page = 1;
while (true) {
const response = await fetch(`/api/users?page=${page}`);
const { users, hasMore } = await response.json();
yield* users; // yield each user individually, not the whole page
if (!hasMore) return;
page++;
}
}
for await (const user of fetchAllUsers()) {
console.log(user.name);
}
Da Consumer muass nie wissn, dass Paginierung passiert. Er sieht einen einzigen Strom an user-Objekten und verarbeitet sie eins nach'm ondern; da Generator versteckt die Paging-Schleifn, die fetch-Aufrufe und die "bin i scho fertig"-Logik komplett.
Warum ned einfach a Array von Promises zruckgebn?
Die Alternative - alle Seitn vorab holn, mit Promise.all zammfassn, Ergebnis flachklopfn - funktioniert, bedeutet aber, den ganzn Datensatz im Speicher zum haltn und auf jede Seitn zum wartn, bevor da erste Datensatz verarbeitet wird. A async Generator liefert, sobois jeder Wert fertig is:
// Eager: waits for everything, holds it all in memory
async function fetchAllUsersEager() {
const pages = await Promise.all(
pageNumbers.map((p) => fetch(`/api/users?page=${p}`).then((r) => r.json())),
);
return pages.flatMap((p) => p.users);
}
// Lazy: starts producing values immediately, one page at a time
async function* fetchAllUsersLazy() {
// ...as above
}
Bei am Datensatz mit 10.000 Nutzern auf 100 Seitn kann die Eager-Version User #1 erst verarbeiten, wenn olle 100 Requests fertig sand. Die Generator-Version fangt mit User #1 an, sobois Seitn 1 do is, und - genauso wichtig - a Consumer, der nur die ersten 20 User braucht, kann früh aufhörn, ohne dass da Generator je die Seitn 2 bis 100 holt.
Früh aufhörn stoppt die Orbeit tatsächlich
Aus ana for await...of-Schleifn auszubrechen ruaft das .return() vom Generator auf, womit's Runtime olle ausständigen finally-Blöcke ausführt - so gibst du Ressourcen sauber frei, ned nur so hörst mit'm Iterieren auf.
async function* readLines(filePath) {
const fileHandle = await openFile(filePath);
try {
let line;
while ((line = await fileHandle.readLine()) !== null) {
yield line;
}
} finally {
console.log('closing file handle');
await fileHandle.close();
}
}
for await (const line of readLines('access.log')) {
if (line.includes('ERROR')) {
console.log('found it:', line);
break; // triggers the generator's finally block — file still gets closed
}
}
Ohne das finally im Generator würd frühes Abbrechen den File Handle offen lossn. Des is die söbe Garantie, die a normales try/finally um a Schleifn gibt, nur erweitert auf a Ressource, deren Lebensdauer mehrere await-Stellen umspannt.
Dein eigenes Async Iterable bauen
Olles, wos Symbol.asyncIterator implementiert, funktioniert mit for await...of - du brauchst ned speziell a Generatorfunktion, auch wenn's meistens der einfachste Weg is, oane zu schreiben. Des is nützlich, um Dinge zu umhüllen, de Werte über die Zeit erzeugen, aber söba koane Generatoren sand, wia an WebSocket-Nachrichtenstrom:
function messagesFrom(socket) {
return {
[Symbol.asyncIterator]() {
const queue = [];
let resolveNext;
socket.addEventListener('message', (event) => {
if (resolveNext) {
resolveNext({ value: event.data, done: false });
resolveNext = null;
} else {
queue.push(event.data);
}
});
return {
next() {
if (queue.length > 0) {
return Promise.resolve({ value: queue.shift(), done: false });
}
return new Promise((resolve) => { resolveNext = resolve; });
},
};
},
};
}
for await (const message of messagesFrom(socket)) {
console.log('received:', message);
}
Des macht aus ana API im Event-Emitter-Stil wos, über des du direkt loopen kannst, statt an Callback zu registriern und State händisch außerhalb vo da Schleifn zu valwoitn.
Wann ma dazu greift
Async Generatoren sand des richtige Werkzeug, wann immer du a Folge an Werten verarbeitest, de über die Zeit einkemman, und der Consumer ned wissn soi, wia sie einkemman - Paginierung, Streaming-Antworten, stückweises Dateilesn, WebSocket-/SSE-Nachrichtenströme. Wennst den kompletten Datensatz scho in da Hand host (sagn ma das Ergebnis von oam fetch-Aufruf), is a normales Array mit for...of einfacher, und es gibt ka Grund, an Generator einzuführn. Greif speziell zu async function*, wenn "den nächsten Wert erzeugen" söba aufs Warten angewiesen is.