Wir verwenden hier in Beispielen Browsermethoden
Um die Verwendung von Rückrufen, Versprechen und anderen abstrakten Konzepten zu demonstrieren, verwenden wir einige Browsermethoden: insbesondere das Laden von Skripts und das Durchführen einfacher Dokumentmanipulationen.
Wenn Sie mit diesen Methoden nicht vertraut sind und ihre Verwendung in den Beispielen verwirrend ist, möchten Sie vielleicht ein paar Kapitel aus dem nächsten Teil des Tutorials lesen.
Wir werden jedoch trotzdem versuchen, die Dinge klarzustellen. Browsertechnisch wird es nichts wirklich Komplexes geben.
Viele Funktionen werden von JavaScript-Hostumgebungen bereitgestellt, mit denen Sie asynchrone Aktionen planen können. Mit anderen Worten: Aktionen, die wir jetzt initiieren, die aber später abgeschlossen werden.
Eine solche Funktion ist beispielsweise die setTimeout
-Funktion.
Es gibt weitere Beispiele aus der Praxis für asynchrone Aktionen, z. B. das Laden von Skripten und Modulen (wir werden sie in späteren Kapiteln behandeln).
Schauen Sie sich die Funktion loadScript(src)
an, die ein Skript mit der angegebenen src
lädt:
Funktion LoadScript(src) { // erstellt ein <script>-Tag und hängt es an die Seite an // Dadurch wird das Skript mit der angegebenen Quelle geladen und ausgeführt, wenn es abgeschlossen ist let script = document.createElement('script'); script.src = src; document.head.append(script); }
Es fügt in das Dokument ein neues, dynamisch erstelltes Tag <script src="…">
mit der angegebenen src
ein. Der Browser beginnt automatisch mit dem Laden und führt ihn aus, wenn er fertig ist.
Wir können diese Funktion wie folgt verwenden:
// Das Skript im angegebenen Pfad laden und ausführen LoadScript('/my/script.js');
Das Skript wird „asynchron“ ausgeführt, da es jetzt mit dem Laden beginnt, aber später ausgeführt wird, wenn die Funktion bereits abgeschlossen ist.
Wenn unterhalb von loadScript(…)
Code vorhanden ist, wird nicht gewartet, bis das Laden des Skripts abgeschlossen ist.
LoadScript('/my/script.js'); // der Code unten LoadScript // wartet nicht, bis das Laden des Skripts abgeschlossen ist // ...
Nehmen wir an, wir müssen das neue Skript verwenden, sobald es geladen ist. Es deklariert neue Funktionen und wir möchten sie ausführen.
Aber wenn wir das direkt nach dem Aufruf loadScript(…)
machen würden, würde das nicht funktionieren:
LoadScript('/my/script.js'); // das Skript hat „function newFunction() {…}“ newFunction(); // keine solche Funktion!
Natürlich hatte der Browser wahrscheinlich keine Zeit, das Skript zu laden. Derzeit bietet die Funktion loadScript
keine Möglichkeit, den Abschluss des Ladevorgangs zu verfolgen. Das Skript wird geladen und schließlich ausgeführt, das ist alles. Aber wir würden gerne wissen, wann es passiert, um neue Funktionen und Variablen aus diesem Skript zu verwenden.
Fügen wir eine callback
als zweites Argument zu loadScript
hinzu, die ausgeführt werden soll, wenn das Skript geladen wird:
Funktion LoadScript(src, Callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(script); document.head.append(script); }
Das onload
Ereignis wird im Artikel Laden von Ressourcen: onload und onerror beschrieben. Es führt grundsätzlich eine Funktion aus, nachdem das Skript geladen und ausgeführt wurde.
Wenn wir nun neue Funktionen aus dem Skript aufrufen wollen, sollten wir das in den Callback schreiben:
loadScript('/my/script.js', function() { // Der Rückruf wird ausgeführt, nachdem das Skript geladen wurde newFunction(); // also jetzt funktioniert es ... });
Das ist die Idee: Das zweite Argument ist eine Funktion (normalerweise anonym), die ausgeführt wird, wenn die Aktion abgeschlossen ist.
Hier ist ein ausführbares Beispiel mit einem echten Skript:
Funktion LoadScript(src, Callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(script); document.head.append(script); } LoadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => { Alert(`Cool, das Skript ${script.src} ist geladen`); Alarm( _ ); // _ ist eine im geladenen Skript deklarierte Funktion });
Dies wird als „Callback-basierter“ Stil der asynchronen Programmierung bezeichnet. Eine Funktion, die etwas asynchron ausführt, sollte ein callback
bereitstellen, mit dem wir die Funktion nach Abschluss ausführen lassen.
Hier haben wir es in loadScript
gemacht, aber es ist natürlich ein allgemeiner Ansatz.
Wie können wir zwei Skripte nacheinander laden: das erste und dann das zweite danach?
Die natürliche Lösung wäre, den zweiten loadScript
-Aufruf wie folgt in den Rückruf einzufügen:
loadScript('/my/script.js', function(script) { warning(`Cool, ${script.src} ist geladen, lass uns noch eins laden`); loadScript('/my/script2.js', function(script) { alarm(`Cool, das zweite Skript ist geladen`); }); });
Nachdem das äußere loadScript
abgeschlossen ist, initiiert der Rückruf das innere.
Was wäre, wenn wir noch ein weiteres Drehbuch hätten wollen …?
loadScript('/my/script.js', function(script) { loadScript('/my/script2.js', function(script) { loadScript('/my/script3.js', function(script) { // ...weitermachen, nachdem alle Skripte geladen sind }); }); });
Jede neue Aktion befindet sich also in einem Rückruf. Für wenige Aktionen ist das in Ordnung, für viele jedoch nicht gut, daher werden wir bald weitere Varianten sehen.
In den obigen Beispielen haben wir Fehler nicht berücksichtigt. Was passiert, wenn das Laden des Skripts fehlschlägt? Unser Rückruf sollte darauf reagieren können.
Hier ist eine verbesserte Version von loadScript
, die Ladefehler verfolgt:
Funktion LoadScript(src, Callback) { let script = document.createElement('script'); script.src = src; script.onload = () => callback(null, script); script.onerror = () => callback(new Error(`Skriptladefehler für ${src}`)); document.head.append(script); }
Bei erfolgreichem Laden wird callback(null, script)
aufgerufen, andernfalls callback(error)
.
Die Verwendung:
loadScript('/my/script.js', function(error, script) { if (Fehler) { // Fehler behandeln } anders { // Skript erfolgreich geladen } });
Auch hier ist das Rezept, das wir für loadScript
verwendet haben, tatsächlich recht verbreitet. Dies wird als „Error-First-Callback“-Stil bezeichnet.
Die Konvention lautet:
Das erste Argument des callback
ist für den Fall eines auftretenden Fehlers reserviert. Dann wird callback(err)
aufgerufen.
Das zweite Argument (und ggf. die nächsten) dienen dem erfolgreichen Ergebnis. Dann wird callback(null, result1, result2…)
aufgerufen.
Daher wird die einzelne callback
sowohl zum Melden von Fehlern als auch zum Zurückgeben von Ergebnissen verwendet.
Auf den ersten Blick sieht es nach einem praktikablen Ansatz für die asynchrone Codierung aus. Und das ist es tatsächlich. Für einen oder vielleicht zwei verschachtelte Aufrufe sieht es gut aus.
Aber für mehrere asynchrone Aktionen, die nacheinander folgen, haben wir Code wie diesen:
loadScript('1.js', function(error, script) { if (Fehler) { handleError(error); } anders { // ... loadScript('2.js', function(error, script) { if (Fehler) { handleError(error); } anders { // ... loadScript('3.js', function(error, script) { if (Fehler) { handleError(error); } anders { // ...weitermachen, nachdem alle Skripte geladen sind (*) } }); } }); } });
Im Code oben:
Wir laden 1.js
und wenn kein Fehler vorliegt ...
Wir laden 2.js
und wenn kein Fehler vorliegt ...
Wir laden 3.js
Wenn es keinen Fehler gibt, machen Sie etwas anderes (*)
.
Je stärker die Aufrufe verschachtelt werden, desto tiefer wird der Code und es wird immer schwieriger, ihn zu verwalten, insbesondere wenn wir echten Code anstelle von ...
haben, der möglicherweise mehr Schleifen, bedingte Anweisungen usw. enthält.
Das wird manchmal „Rückrufhölle“ oder „Pyramide des Untergangs“ genannt.
Die „Pyramide“ der verschachtelten Aufrufe wächst mit jeder asynchronen Aktion nach rechts. Bald gerät es außer Kontrolle.
Diese Art der Codierung ist also nicht sehr gut.
Wir können versuchen, das Problem zu lindern, indem wir jede Aktion zu einer eigenständigen Funktion machen, wie folgt:
loadScript('1.js', step1); Funktion Schritt1(Fehler, Skript) { if (Fehler) { handleError(error); } anders { // ... loadScript('2.js', step2); } } Funktion Schritt2(Fehler, Skript) { if (Fehler) { handleError(error); } anders { // ... loadScript('3.js', step3); } } Funktion Schritt3(Fehler, Skript) { if (Fehler) { handleError(error); } anders { // ...weitermachen, nachdem alle Skripte geladen sind (*) } }
Sehen? Es macht das Gleiche, und es gibt jetzt keine tiefe Verschachtelung, da wir jede Aktion zu einer separaten Funktion der obersten Ebene gemacht haben.
Es funktioniert, aber der Code sieht aus wie eine zerrissene Tabelle. Es ist schwer zu lesen, und Sie haben wahrscheinlich bemerkt, dass man beim Lesen zwischen den einzelnen Teilen hin und her springen muss. Das ist unpraktisch, insbesondere wenn der Leser mit dem Code nicht vertraut ist und nicht weiß, wohin er springen soll.
Außerdem sind die Funktionen mit dem Namen step*
alle nur für den einmaligen Gebrauch bestimmt und wurden nur erstellt, um die „Pyramide des Untergangs“ zu vermeiden. Niemand wird sie außerhalb der Aktionskette wiederverwenden. Hier herrscht also ein wenig Namespace-Unordnung.
Wir hätten gerne etwas Besseres.
Glücklicherweise gibt es andere Möglichkeiten, solchen Pyramiden auszuweichen. Eine der besten Möglichkeiten ist die Verwendung von „Versprechen“, die im nächsten Kapitel beschrieben werden.