Asynchrone Flusskontrolle
INFO
Das Material in diesem Beitrag ist stark von Mixus Node.js Buch inspiriert.
Im Kern ist JavaScript so konzipiert, dass es im "Haupt"-Thread, in dem die Ansichten gerendert werden, nicht blockiert. Sie können sich vorstellen, wie wichtig dies im Browser ist. Wenn der Haupt-Thread blockiert wird, führt dies zu dem berüchtigten "Einfrieren", das Endbenutzer fürchten, und es können keine anderen Ereignisse ausgelöst werden, was z. B. zum Verlust der Datenerfassung führt.
Dies schafft einige einzigartige Einschränkungen, die nur ein funktionaler Programmierstil beheben kann. Hier kommen die Callbacks ins Spiel.
Allerdings können Callbacks in komplizierteren Abläufen schwierig zu handhaben sein. Dies führt oft zu "Callback-Hölle", in der mehrere verschachtelte Funktionen mit Callbacks den Code schwieriger lesbar, debuggbar, organisierbar usw. machen.
async1(function (input, result1) {
async2(function (result2) {
async3(function (result3) {
async4(function (result4) {
async5(function (output) {
// mache etwas mit output
});
});
});
});
});
Im realen Leben gäbe es natürlich wahrscheinlich zusätzliche Codezeilen zur Verarbeitung von result1
, result2
usw. Daher führt die Länge und Komplexität dieses Problems normalerweise zu Code, der viel unordentlicher aussieht als das obige Beispiel.
Hier kommen Funktionen sehr nützlich zum Einsatz. Komplexere Operationen bestehen aus vielen Funktionen:
- Initiator-Stil / Eingabe
- Middleware
- Terminator
Der "Initiator-Stil / Eingabe" ist die erste Funktion in der Sequenz. Diese Funktion akzeptiert die ursprüngliche Eingabe, falls vorhanden, für die Operation. Die Operation ist eine ausführbare Reihe von Funktionen, und die ursprüngliche Eingabe wird hauptsächlich sein:
- Variablen in einer globalen Umgebung
- Direkter Aufruf mit oder ohne Argumente
- Werte, die durch Dateisystem- oder Netzwerkanfragen erhalten werden
Netzwerkanfragen können eingehende Anfragen sein, die von einem fremden Netzwerk, von einer anderen Anwendung im selben Netzwerk oder von der App selbst im selben oder einem fremden Netzwerk initiiert werden.
Eine Middleware-Funktion gibt eine andere Funktion zurück, und eine Terminator-Funktion ruft den Callback auf. Das Folgende veranschaulicht den Fluss zu Netzwerk- oder Dateisystemanfragen. Hier ist die Latenz 0, da alle diese Werte im Speicher verfügbar sind.
function final(someInput, callback) {
callback(`${someInput} und beendet durch Ausführung des Callbacks `);
}
function middleware(someInput, callback) {
return final(`${someInput} berührt von Middleware `, callback);
}
function initiate() {
const someInput = 'hallo, das ist eine Funktion ';
middleware(someInput, function (result) {
console.log(result);
// erfordert Callback, um das Ergebnis `zurückzugeben`
});
}
initiate();
Zustandsverwaltung
Funktionen können zustandsabhängig sein oder nicht. Eine Zustandsabhängigkeit entsteht, wenn die Eingabe oder eine andere Variable einer Funktion von einer externen Funktion abhängt.
In diesem Zusammenhang gibt es zwei Hauptstrategien für die Zustandsverwaltung:
- Variablen direkt an eine Funktion übergeben und
- einen Variablenwert aus einem Cache, einer Sitzung, einer Datei, einer Datenbank, einem Netzwerk oder einer anderen externen Quelle beziehen.
Ich habe hier globale Variablen nicht erwähnt. Die Zustandsverwaltung mit globalen Variablen ist oft ein schlampiges Anti-Pattern, das es schwierig oder unmöglich macht, den Zustand zu garantieren. Globale Variablen in komplexen Programmen sollten nach Möglichkeit vermieden werden.
Kontrollfluss
Wenn ein Objekt im Speicher verfügbar ist, ist eine Iteration möglich und es wird keine Änderung des Kontrollflusses geben:
function getSong() {
let _song = '';
let i = 100;
for (i; i > 0; i -= 1) {
_song += `${i} Flaschen Bier an der Wand, man nimmt eine runter und gibt sie weiter, ${
i - 1
} Flaschen Bier an der Wand\n`;
if (i === 1) {
_song += "Hey, holen wir uns noch mehr Bier";
}
}
return _song;
}
function singSong(_song) {
if (!_song) throw new Error("Lied ist '' leer, GIB MIR EIN LIED!");
console.log(_song);
}
const song = getSong();
// Das wird funktionieren
singSong(song);
Wenn die Daten jedoch außerhalb des Speichers existieren, funktioniert die Iteration nicht mehr:
function getSong() {
let _song = '';
let i = 100;
for (i; i > 0; i -= 1) {
/* eslint-disable no-loop-func */
setTimeout(function () {
_song += `${i} Flaschen Bier an der Wand, man nimmt eine runter und gibt sie weiter, ${
i - 1
} Flaschen Bier an der Wand\n`;
if (i === 1) {
_song += "Hey, holen wir uns noch mehr Bier";
}
}, 0);
/* eslint-enable no-loop-func */
}
return _song;
}
function singSong(_song) {
if (!_song) throw new Error("Lied ist '' leer, GIB MIR EIN LIED!");
console.log(_song);
}
const song = getSong('beer');
// Das wird nicht funktionieren
singSong(song);
// Uncaught Error: Lied ist '' leer, GIB MIR EIN LIED!
Warum ist das passiert? setTimeout
weist die CPU an, die Anweisungen an anderer Stelle im Bus zu speichern und weist an, dass die Daten zu einem späteren Zeitpunkt abgeholt werden sollen. Tausende von CPU-Zyklen vergehen, bevor die Funktion wieder die 0-Millisekunden-Marke erreicht, die CPU die Anweisungen vom Bus holt und sie ausführt. Das einzige Problem ist, dass song ('') bereits Tausende von Zyklen zuvor zurückgegeben wurde.
Die gleiche Situation ergibt sich im Umgang mit Dateisystemen und Netzwerkanfragen. Der Hauptthread kann einfach nicht für unbestimmte Zeit blockiert werden – daher verwenden wir Callbacks, um die Ausführung von Code zeitgesteuert und kontrolliert zu planen.
Sie werden in der Lage sein, fast alle Ihre Operationen mit den folgenden 3 Mustern durchzuführen:
- Seriell: Funktionen werden in einer streng sequentiellen Reihenfolge ausgeführt, dies ähnelt am ehesten
for
-Schleifen.
// Operationen, die an anderer Stelle definiert sind und zur Ausführung bereit sind
const operations = [
{ func: function1, args: args1 },
{ func: function2, args: args2 },
{ func: function3, args: args3 },
];
function executeFunctionWithArgs(operation, callback) {
// führt Funktion aus
const { args, func } = operation;
func(args, callback);
}
function serialProcedure(operation) {
if (!operation) process.exit(0); // fertig
executeFunctionWithArgs(operation, function (result) {
// nach Callback fortfahren
serialProcedure(operations.shift());
});
}
serialProcedure(operations.shift());
Vollständig parallel
: wenn die Reihenfolge kein Problem ist, wie z. B. das Versenden von E-Mails an eine Liste von 1.000.000 E-Mail-Empfängern.
let count = 0;
let success = 0;
const failed = [];
const recipients = [
{ name: 'Bart', email: 'bart@tld' },
{ name: 'Marge', email: 'marge@tld' },
{ name: 'Homer', email: 'homer@tld' },
{ name: 'Lisa', email: 'lisa@tld' },
{ name: 'Maggie', email: 'maggie@tld' },
];
function dispatch(recipient, callback) {
// `sendEmail` ist ein hypothetischer SMTP-Client
sendMail(
{
subject: 'Abendessen heute Abend',
message: 'Wir haben viel Kohl auf dem Teller. Kommst du?',
smtp: recipient.email,
},
callback
);
}
function final(result) {
console.log(`Ergebnis: ${result.count} Versuche \
& ${result.success} erfolgreiche E-Mails`);
if (result.failed.length)
console.log(`Senden an folgende E-Mail-Adressen fehlgeschlagen: \
\n${result.failed.join('\n')}\n`);
}
recipients.forEach(function (recipient) {
dispatch(recipient, function (err) {
if (!err) {
success += 1;
} else {
failed.push(recipient.name);
}
count += 1;
if (count === recipients.length) {
final({
count,
success,
failed,
});
}
});
});
- Begrenzt parallel: parallel mit Limit, wie z. B. das erfolgreiche Versenden von E-Mails an 1.000.000 Empfänger aus einer Liste von 10 Millionen Benutzern.
let successCount = 0;
function final() {
console.log(`habe ${successCount} E-Mails versendet`);
console.log('fertig');
}
function dispatch(recipient, callback) {
// `sendEmail` ist ein hypothetischer SMTP-Client
sendMail(
{
subject: 'Abendessen heute Abend',
message: 'Wir haben viel Kohl auf dem Teller. Kommst du?',
smtp: recipient.email,
},
callback
);
}
function sendOneMillionEmailsOnly() {
getListOfTenMillionGreatEmails(function (err, bigList) {
if (err) throw err;
function serial(recipient) {
if (!recipient || successCount >= 1000000) return final();
dispatch(recipient, function (_err) {
if (!_err) successCount += 1;
serial(bigList.pop());
});
}
serial(bigList.pop());
});
}
sendOneMillionEmailsOnly();
Jedes hat seine eigenen Anwendungsfälle, Vorteile und Probleme, über die Sie experimentieren und genauer lesen können. Am wichtigsten ist, dass Sie Ihre Operationen modularisieren und Callbacks verwenden! Wenn Sie irgendwelche Zweifel haben, behandeln Sie alles so, als wäre es Middleware!