Generatoren sind leistungsstark und werden von JavaScript-Entwicklern. Viele Tutorials über Generatoren kratzen jedoch nur an der Oberfläche. In diesem Artikel werden wir tief in die Materie eintauchen und ein fortgeschrittenes Verständnis der Theorie hinter Generatoren entwickeln.
Aber zuerst: Möchten Sie dieses Tutorial in Aktion sehen? Hier ist die Videoversion.
Generatoren werden am häufigsten in Sagas verwendet, aber es gibt noch weitere Anwendungsfälle dafür. Einige davon werden wir in diesem Artikel behandeln.
Die kurze Antwort auf die Frage „Was ist ein Generator?“ lautet:
Generatoren sind Pull-Streams in JavaScript.
Lassen Sie uns diese Definition zerlegen und dann zu einigen Beispielen übergehen.
Zuerst müssen Sie zwei Begriffe verstehen: „Pull“ und „Stream“.
Ein Stream ist Daten über die Zeit. Es gibt zwei Arten von Streams: Push-Streams und Pull-Streams.
Ein Push-Stream ist ein Mechanismus, bei dem Sie NICHT kontrollieren können, WANN die Daten ankommen.
Beispiele für Pull-Streams sind:
Ein JavaScript-Beispiel für einen Push-Stream, der Node.js zum Lesen einer großen Datei von der Festplatte verwendet, finden Sie unten.
const fs = require('fs');
const readStream = fs.createReadStream('./largeFile.txt');
readStream.on('data', chunk => {
console.log('data received', chunk.length)
});
readStream.on('end', () => {
console.log('finished reading file');
});
readStream.on('error', error => {
console.log('an error occured while reading the file', error);
});
Ein Pull-Stream ist, wenn Sie SELBST kontrollieren können, WANN Sie die Daten anfordern möchten.
Codebeispiele für Pull-Streams in JavaScript werden Sie bald sehen, wenn es um Generator-Code geht, aber zuerst müssen Sie ein anderes Konzept verstehen.
In der Programmierung können Daten auf zwei grundlegende Arten verarbeitet werden: aktiv oder träge.
Aktiv bedeutet, dass Daten sofort ausgewertet werden, unabhängig davon, ob das Ergebnis in diesem Moment benötigt wird. Ein Push-Stream ist aktiv. (Weitere Beispiele: Array-Methoden, Promises)
// Eager evaluation with array methods:
const numbers = [1, 2, 3, 4, 5];
// Map immediately processes all elements in the array.
const squares = numbers.map(num => {
console.log(`Squaring ${num}`);
return num * num;
});
console.log('squares:', squares); // [1, 4, 9, 16, 25]console.log('squares:', squares); // [1, 4, 9, 16, 25]
Vielleicht denken Sie jetzt: „Okay, aber warum sind Promises aktiv? Ihr Ergebnis kommt doch erst später.“
Promises in JavaScript zeigen aus mehreren Gründen eine aktive Auswertung.
Das folgende Beispiel zeigt, wie Promises sofort ausgeführt werden.
// Eager evaluation with promises and array methods
console.log("Before promise");
let promise = new Promise((resolve, reject) => {
console.log("Inside promise executor");
resolve("Resolved data");
});
console.log("After promise");
promise.then(result => {
console.log(result);
});
Dies führt zu folgender Ausgabe.
$ node eager-promise-example.js
Before promise
Inside promise executor
After promise
Resolved data
Träge bedeutet, dass sie nur ausgewertet wird, wenn der Wert benötigt wird (nicht vorher). Ein Pull-Stream ist träge.
Ein synchrones Beispiel wären die Operanden-Selektor-Operatoren.
// Lazy evaluation with logical operators
function processData(data) {
console.log(`Processing ${data}`); // This never logs out 🚫
return data * data;
}
console.log('Lazy evaluation starts');
const data = 5;
const isDataProcessed = false;
// Lazy evaluation using the logical AND operator.
const result = isDataProcessed && processData(data);
console.log('Result:', result); // falseWenn Sie diesen Code ausführen, werden Sie die folgende Ausgabe sehen.
$ node lazy-evaluation-example.js
Lazy evaluation starts
Result: falseDa isDataProcessed ist false, die processData Funktion nie ausgeführt wird und Sie niemals "Processing 5" in der Konsole sehen. Dies zeigt, dass der Ausdruck nur das auswertet, was zur Erzielung des Ergebnisses erforderlich ist.
Ein Generator ist ein Pull-Stream in JavaScript. Das bedeutet, es ist eine spezielle Art von Funktion, bei der Sie die Ausführung anhalten und fortsetzen es später.
Das Generator Objekt wird von einer Generatorfunktion zurückgegeben und entspricht sowohl dem Iterable-Protokoll als auch dem Iterator-Protokoll.
function* myGenerator() {
yield "Hire senior";
yield "React engineers";
yield "at ReactSquad.io";
}
const iterator = myGenerator();
// Using the generator as an iterator.
console.log(iterator.next()); // { done: false, value: "Hire senior" }
console.log(iterator.next()); // { done: false, value: "React engineers" }
console.log(iterator.next()); // { done: false, value: "at ReactSquad.io" }
console.log(iterator.next()); // { done: true, value: undefined }
// Using the generator as an iterable.
for (let string of myGenerator()) {
console.log(number); // "Hire senior" "React engineers" "at ReactSquad.io"
}Neben der .next() Methode verfügen Generatoren auch über .return() und .throw().
.return() – Die .return() Methode beendet die Ausführung des Generators und gibt den angegebenen Wert zurück, wobei auch alle finally Blöcke ausgelöst werden..throw() – Die .throw() Methode ermöglicht es Ihnen, einen Fehler innerhalb des Generators an der Stelle des letzten `yield` auszulösen, der abgefangen und behandelt werden kann oder dem Generator erlaubt, über einen finally Block aufzuräumen. Wird er nicht abgefangen, stoppt er den Generator und markiert ihn als abgeschlossen.function* numberGenerator() {
try {
yield 1;
yield 2;
yield 3;
} finally {
console.log("Cleanup complete");
}
}
const generator = numberGenerator();
// Using the generator normally.
console.log(generator.next()); // { done: false, value: 1 }
console.log(generator.next()); // { done: false, value: 2 }
// Using return() to finish the generator early.
console.log(generator.return(10)); // { done: true, value: 10 }
// After return(), no more values are yielded.
console.log(generator.next()); // { done: true, value: undefined }
// Resetting the generator for throw example.
const newGenerator = numberGenerator();
console.log(newGenerator.next()); // { done: false, value: 1 }
// Using throw() to signal an error.
try {
newGenerator.throw(new Error("Something went wrong"));
} catch (e) {
console.log(e.message); // "Something went wrong"
}
// After throw(), the generator is closed.
console.log(newGenerator.next()); // { done: true, value: undefined }Sie können auch Zahlen oder andere Werte an Generatoren übergeben, wenn Sie next() mit einem Argument aufrufen.
Versuchen Sie vorherzusagen, was ausgegeben wird und wann im folgenden Beispiel.
function* moreNumbers(x) {
console.log('x', x);
const y = yield x + 2;
console.log('y', y);
const z = yield x + y;
console.log('z', z);
}
const it2 = moreNumbers(40);
console.log(it2.next());
console.log(it2.next(2012));
console.log(it2.next());Dieses Beispiel zeigt, wie die Generatorfunktion moreNumbers Werte manipuliert und erzeugt, basierend auf den Eingaben, die sie während der Abfolge von .next() Aufrufe.
Schauen Sie sich die Ausgabe an und überprüfen Sie Ihre Vorhersage.
const it2 = moreNumbers(40);
// x: 40
console.log(it2.next()); // { value: 42, done: false }
// y: 2012
console.log(it2.next(2012)); // { value: 2052, done: false }
// z: undefined
console.log(it2.next()); // { value: undefined, done: true }Lassen Sie uns jeden Schritt der Generatorfunktion moreNumbers aufschlüsseln, damit Sie sie vollständig verstehen.
Es gibt drei Hauptanwendungsfälle für Generatoren.
Zuvor haben Sie ein Beispiel für das Lesen einer Datei von der Festplatte als Push-Stream gesehen. Im Folgenden wird gezeigt, wie Sie das Datenlesen mithilfe eines Generators schreiben würden, um es in einen Pull-Stream umzuwandeln.
const fs = require('fs');
function getChunkFromStream(stream) {
return new Promise((resolve, reject) => {
stream.once('data', (chunk) => {
stream.pause();
resolve(chunk);
});
stream.once('end', () => {
resolve(null);
});
stream.once('error', (err) => {
reject(err);
});
stream.resume();
});
}
async function* readFileChunkByChunk(filePath) {
const stream = fs.createReadStream(filePath);
let chunk;
while (chunk = await getChunkFromStream(stream)) {
yield chunk;
}
}
const generator = readFileChunkByChunk('./largeFile.txt');
(async () => {
for await (const chunk of generator) {
console.log("data received", chunk.length);
}
})();Sagas sind ein Paradebeispiel für die Handhabung asynchroner I/O-Operationen. Sie werden jedoch in einem zukünftigen Artikel, in einer Reihe von Artikeln über Redux.
Und generell verwendet man Generatoren, wenn man kontrollieren möchte, WANN man einen Wert erhält.
Werfen Sie einen Blick auf dieses Testbeispiel.
test('given an onboarded owner user: shows the invite link creation UI as well as the members of the organization, and lets the user change their role', async ({ page }) => {
// Generator for roles in the organization.
function* roleGenerator() {
const allRoles = Object.values(ORGANIZATION_MEMBERSHIP_ROLES);
for (const role of allRoles) {
yield role;
}
}
const roleIterator = roleGenerator();
const data = await setup({
page,
role: ORGANIZATION_MEMBERSHIP_ROLES.OWNER,
numberOfOtherTeamMembers: allRoles.length,
});
const { organization, sortedUsers, user } = data;
// Navigate to team members settings page.
await page.goto(`/organizations/${organization.slug}/settings/team-members`);
// Loop through each team member to assign roles using the generator.
for (let index = 0; index < sortedUsers.length; index++) {
const memberListItem = page.getByRole('list', { name: /team members/i }).getByRole('listitem').nth(index);
const otherUser = sortedUsers[index];
// Change role for each team member, except the current user.
if (otherUser.id !== user.id) {
await memberListItem.getByRole('button', { name: /member/i }).click();
const role = roleIterator.next().value!;
await page.getByRole('option', { name: role }).getByRole('button').click();
await page.keyboard.press('Escape');
}
}
await teardown(data);
});In diesem Test definieren Sie eine roleGenerator um sequenziell eine Liste von Rollen für Benutzer innerhalb einer Organisation bereitzustellen. Dieser Ansatz ermöglicht es dem Test, jedem Benutzer dynamisch eine einzigartige Rolle aus einer vordefinierten Liste als Teil einer Rollenverwaltungsfunktion in einer Benutzeroberfläche zuzuweisen.
Der Grund, warum für dieses Beispiel ein Generator – im Gegensatz zu einem Array – verwendet wurde, ist, dass die Position des Hauptbenutzers in der sortedUsers Array unbekannt ist und da ein Generator ein Pull-Stream ist, können Sie die Rollenwerte bei Bedarf abrufen.
Wenn Ihnen das gefallen hat, dann werden Sie meinen YouTube-Kanallieben. Schauen Sie mal rein!