1) Was ist eine "Rückruf-Hölle" für jemanden, der Javascript und node.js nicht kennt?
Diese andere Frage enthält einige Beispiele für die Hölle des Javascript-Rückrufs: So vermeiden Sie eine lange Verschachtelung asynchroner Funktionen in Node.js.
Das Problem in Javascript ist, dass die einzige Möglichkeit, eine Berechnung "einzufrieren" und den "Rest" (asynchron) ausführen zu lassen, darin besteht, "den Rest" in einen Rückruf einzufügen.
Angenommen, ich möchte Code ausführen, der folgendermaßen aussieht:
x = getData();
y = getMoreData(x);
z = getMoreData(y);
...
Was passiert, wenn ich jetzt die getData-Funktionen asynchron machen möchte, was bedeutet, dass ich die Möglichkeit habe, einen anderen Code auszuführen, während ich darauf warte, dass sie ihre Werte zurückgeben? In Javascript besteht die einzige Möglichkeit darin, alles, was eine asynchrone Berechnung berührt, mithilfe des Continuation-Passing-Stils neu zu schreiben :
getData(function(x){
getMoreData(x, function(y){
getMoreData(y, function(z){
...
});
});
});
Ich glaube nicht, dass ich jemanden davon überzeugen muss, dass diese Version hässlicher ist als die vorherige. :-)
2) Wann (in welchen Einstellungen) tritt das "Callback Hell Problem" auf?
Wenn Sie viele Rückruffunktionen in Ihrem Code haben! Es wird schwieriger, mit ihnen zu arbeiten, je mehr Sie in Ihrem Code haben, und es wird besonders schlimm, wenn Sie Schleifen, Try-Catch-Blöcke und ähnliches ausführen müssen.
Soweit ich weiß, ist die einzige Möglichkeit, in JavaScript eine Reihe von asynchronen Funktionen auszuführen, bei denen eine nach den vorherigen Rückgaben ausgeführt wird, die Verwendung einer rekursiven Funktion. Sie können keine for-Schleife verwenden.
// we would like to write the following
for(var i=0; i<10; i++){
doSomething(i);
}
blah();
Stattdessen müssen wir möglicherweise schreiben:
function loop(i, onDone){
if(i >= 10){
onDone()
}else{
doSomething(i, function(){
loop(i+1, onDone);
});
}
}
loop(0, function(){
blah();
});
//ugh!
Die Anzahl der Fragen, die wir hier bei StackOverflow erhalten, wie man so etwas macht, ist ein Beweis dafür, wie verwirrend es ist :)
3) Warum tritt es auf?
Dies tritt auf, weil in JavaScript die einzige Möglichkeit, eine Berechnung so zu verzögern, dass sie nach der Rückkehr des asynchronen Aufrufs ausgeführt wird, darin besteht, den verzögerten Code in eine Rückruffunktion einzufügen. Sie können Code, der im traditionellen synchronen Stil geschrieben wurde, nicht verzögern, sodass Sie überall verschachtelte Rückrufe erhalten.
4) Oder kann "Callback Hell" auch in einer Single-Threaded-Anwendung auftreten?
Asynchrone Programmierung hat mit Parallelität zu tun, während ein einzelner Thread mit Parallelität zu tun hat. Die beiden Konzepte sind eigentlich nicht dasselbe.
Sie können weiterhin gleichzeitig Code in einem einzelnen Thread-Kontext haben. In der Tat ist JavaScript, die Königin der Callback-Hölle, Single-Threaded.
Was ist der Unterschied zwischen Parallelität und Parallelität?
5) Könnten Sie bitte auch zeigen, wie RX das "Callback Hell Problem" in diesem einfachen Beispiel löst?
Ich weiß insbesondere nichts über RX, aber normalerweise wird dieses Problem gelöst, indem native Unterstützung für asynchrone Berechnungen in der Programmiersprache hinzugefügt wird. Die Implementierungen können variieren und umfassen: Async, Generatoren, Coroutinen und Callcc.
In Python können wir dieses vorherige Schleifenbeispiel folgendermaßen implementieren:
def myLoop():
for i in range(10):
doSomething(i)
yield
myGen = myLoop()
Dies ist nicht der vollständige Code, aber die Idee ist, dass die "Ausbeute" unsere for-Schleife pausiert, bis jemand myGen.next () aufruft. Das Wichtigste ist, dass wir den Code immer noch mit einer for-Schleife schreiben können, ohne dass die Logik "von innen nach außen" ausfallen muss, wie wir es in dieser rekursiven loop
Funktion tun mussten .
Beantworten Sie einfach die Frage: Können Sie bitte auch zeigen, wie RX das "Callback Hell Problem" in diesem einfachen Beispiel löst?
Die Magie ist
flatMap
. Wir können den folgenden Code in Rx für das Beispiel von @ hugomg schreiben:Es ist, als würden Sie einige synchrone FP-Codes schreiben, aber tatsächlich können Sie sie durch asynchron machen
Scheduler
.quelle
Um die Frage, wie Rx löst Rückruf Hölle :
Lassen Sie uns zunächst noch einmal die Rückrufhölle beschreiben.
Stellen Sie sich einen Fall vor, in dem wir http ausführen müssen, um drei Ressourcen zu erhalten - Person, Planet und Galaxie. Unser Ziel ist es, die Galaxie zu finden, in der die Person lebt. Zuerst müssen wir die Person bekommen, dann den Planeten, dann die Galaxie. Das sind drei Rückrufe für drei asynchrone Operationen.
Jeder Rückruf ist verschachtelt. Jeder innere Rückruf ist von seinem übergeordneten Element abhängig. Dies führt zum "Pyramid of Doom" -Stil der Rückrufhölle . Der Code sieht aus wie ein> Zeichen.
Um dies in RxJs zu lösen, könnten Sie so etwas tun:
Mit dem
mergeMap
AKA-flatMap
Operator können Sie es prägnanter gestalten:Wie Sie sehen können, ist der Code abgeflacht und enthält eine einzelne Kette von Methodenaufrufen. Wir haben keine "Pyramide des Untergangs".
Daher wird die Rückrufhölle vermieden.
Falls Sie sich gefragt haben, sind Versprechen ein weiterer Weg, um die Hölle des Rückrufs zu vermeiden, aber Versprechen sind eifrig , nicht faul wie Observablen und (im Allgemeinen) können Sie sie nicht so einfach stornieren.
quelle
Callback Hell ist jeder Code, bei dem die Verwendung von Funktionsrückrufen in asynchronem Code unklar oder schwer zu befolgen ist. Wenn es mehr als eine Indirektionsebene gibt, kann es im Allgemeinen schwieriger werden, Code mithilfe von Rückrufen zu folgen, schwieriger umzugestalten und schwieriger zu testen. Ein Codegeruch besteht aus mehreren Einrückungsstufen, da mehrere Ebenen von Funktionsliteralen übergeben werden.
Dies geschieht häufig, wenn das Verhalten Abhängigkeiten aufweist, dh wenn A vor B vor C auftreten muss. Dann erhalten Sie Code wie folgt:
Wenn Ihr Code viele Verhaltensabhängigkeiten aufweist, kann dies schnell problematisch werden. Besonders wenn es verzweigt ...
Das geht nicht. Wie können wir asynchronen Code in einer bestimmten Reihenfolge ausführen lassen, ohne all diese Rückrufe weitergeben zu müssen?
RX ist die Abkürzung für "Reactive Extensions". Ich habe es nicht benutzt, aber Googeln schlägt vor, dass es ein ereignisbasiertes Framework ist, was Sinn macht. Ereignisse sind ein gängiges Muster, mit dem Code in der richtigen Reihenfolge ausgeführt wird, ohne dass eine spröde Kopplung entsteht . Sie können C dazu bringen, das Ereignis 'bFinished' anzuhören, was erst geschieht, nachdem B als 'aFinished' bezeichnet wird. Sie können dann problemlos zusätzliche Schritte hinzufügen oder diese Art von Verhalten erweitern und auf einfache Weise testen, ob Ihr Code in der richtigen Reihenfolge ausgeführt wird, indem Sie lediglich Ereignisse in Ihrem Testfall senden.
quelle
Rückruf Hölle bedeutet, dass Sie sich innerhalb eines Rückrufs innerhalb eines anderen Rückrufs befinden und es zum n-ten Anruf geht, bis Ihre Bedürfnisse nicht erfüllt sind.
Lassen Sie uns anhand eines Beispiels für einen gefälschten Ajax-Aufruf mithilfe der festgelegten Zeitüberschreitungs-API verstehen, dass wir eine Rezept-API haben und alle Rezepte herunterladen müssen.
Im obigen Beispiel wird nach 1,5 Sekunden, wenn der Timer innerhalb des Rückrufcodes abläuft, ausgeführt. Mit anderen Worten, durch unseren gefälschten Ajax-Aufruf werden alle Rezepte vom Server heruntergeladen. Jetzt müssen wir bestimmte Rezeptdaten herunterladen.
Um bestimmte Rezeptdaten herunterzuladen, haben wir Code in unseren ersten Rückruf geschrieben und die Rezept-ID übergeben.
Angenommen, wir müssen alle Rezepte desselben Herausgebers des Rezepts herunterladen, dessen ID 7638 lautet.
Um unsere Anforderungen zu erfüllen, dh alle Rezepte des Herausgebernamens suru herunterzuladen, haben wir in unserem zweiten Rückruf Code geschrieben. Es ist klar, dass wir eine Rückrufkette geschrieben haben, die Rückrufhölle heißt.
Wenn Sie die Hölle des Rückrufs vermeiden möchten, können Sie Promise verwenden, die js es6-Funktion. Jedes Versprechen erhält einen Rückruf, der aufgerufen wird, wenn ein Versprechen vollständig erfüllt ist. Versprechen Rückruf hat zwei Optionen, entweder es wird gelöst oder abgelehnt. Angenommen, Ihr API-Aufruf ist erfolgreich. Sie können die Auflösung aufrufen und Daten durch die Auflösung übergeben . Sie können diese Daten mit then () abrufen . Wenn Ihre API jedoch fehlgeschlagen ist, können Sie Reject verwenden. Verwenden Sie catch , um den Fehler abzufangen. Denken Sie daran, ein Versprechen immer dann für die Lösung zu verwenden und für die Ablehnung zu fangen
Lassen Sie uns das vorherige Problem der Rückrufhölle mit einem Versprechen lösen.
Laden Sie jetzt ein bestimmtes Rezept herunter:
Jetzt können wir eine andere Methode schreiben, die allRecipeOfAPublisher aufruft, wie getRecipe, die ebenfalls ein Versprechen zurückgibt, und wir können eine weitere then () schreiben, um ein Auflösungsversprechen für allRecipeOfAPublisher zu erhalten. Ich hoffe, dass Sie dies an dieser Stelle selbst tun können.
Wir haben also gelernt, wie man Versprechen konstruiert und konsumiert. Lassen Sie uns nun das Konsumieren von Versprechen vereinfachen, indem wir async / await verwenden, das in es8 eingeführt wird.
Im obigen Beispiel haben wir eine asynchrone Funktion verwendet, da sie im Hintergrund ausgeführt wird. Innerhalb der asynchronen Funktion haben wir das Schlüsselwort await vor jeder Methode verwendet, die zurückgibt oder ein Versprechen ist, weil wir an dieser Position warten müssen, bis dieses Versprechen erfüllt ist, mit anderen Worten in der Balgcodes, bis getIds abgeschlossen ist, aufgelöst oder Programm abgelehnt, führen die Ausführung von Codes unter dieser Zeile nicht mehr aus, wenn IDs zurückgegeben werden. Dann haben wir die Funktion getRecipe () erneut mit einer ID aufgerufen und mit dem Schlüsselwort await gewartet, bis Daten zurückgegeben wurden. So haben wir uns endlich von der Rückrufhölle erholt.
Um wait verwenden zu können, benötigen wir eine asynchrone Funktion. Wir können ein Versprechen zurückgeben. Verwenden Sie es dann, um das Versprechen zu lösen, und kath, um das Versprechen abzulehnen
aus dem obigen Beispiel:
quelle
Eine Möglichkeit, Callback Hell zu vermeiden, ist die Verwendung von FRP, einer "erweiterten Version" von RX.
Ich habe kürzlich angefangen, FRP zu verwenden, weil ich eine gute Implementierung namens FRP gefunden habe
Sodium
( http://sodium.nz/). ).Ein typischer Code sieht folgendermaßen aus (Scala.js):
selectedNote.updates()
ist einStream
was feuert, wennselectedNode
(was a istCell
) sich ändert, dasNodeEditorWidget
dann entsprechend aktualisiert.Also, je nach Inhalt der
selectedNode
Cell
, die aktuell bearbeitetNote
ändert sich also .Dieser Code vermeidet Callback-s vollständig, fast, Cacllback-s werden auf die "äußere Schicht" / "Oberfläche" der App verschoben, wo die Statusbehandlungslogik mit der Außenwelt verbunden ist. Es sind keine Rückrufe erforderlich, um Daten innerhalb der internen Zustandsbehandlungslogik (die eine Zustandsmaschine implementiert) weiterzugeben.
Der vollständige Quellcode ist hier
Das obige Codefragment entspricht dem folgenden einfachen Beispiel zum Erstellen / Anzeigen / Aktualisieren:
Dieser Code sendet auch Aktualisierungen an den Server, sodass Änderungen an den aktualisierten Entitäten automatisch auf dem Server gespeichert werden.
Die gesamte Ereignisbehandlung wird mit
Stream
s undCell
s erledigt. Dies sind FRP-Konzepte. Rückrufe werden nur benötigt, wenn die FRP-Logik mit der Außenwelt verbunden ist, z. B. Benutzereingaben, Bearbeiten von Text, Drücken einer Taste, AJAX-Aufruf kehrt zurück.Der Datenfluss wird explizit deklarativ unter Verwendung von FRP (implementiert von der Sodium-Bibliothek) beschrieben, sodass zur Beschreibung des Datenflusses keine Ereignisbehandlungs- / Rückruflogik erforderlich ist.
FRP (eine "strengere" Version von RX) ist eine Möglichkeit, ein Datenflussdiagramm zu beschreiben, das Knoten enthalten kann, die Status enthalten. Ereignisse lösen Statusänderungen in dem Status aus, der Knoten enthält (
Cell
s genannt).Natrium ist eine FRP-Bibliothek höherer Ordnung, was bedeutet, dass mit dem
flatMap
/switch
primitiven das Datenflussdiagramm zur Laufzeit neu angeordnet werden kann.Ich empfehle, einen Blick in das Natrium-Buch zu werfen. Es erklärt ausführlich, wie FRP alle Rückrufe beseitigt, die für die Beschreibung der Datenflusslogik, die mit der Aktualisierung des Anwendungsstatus als Reaktion auf einige externe Stimuli zu tun hat, nicht unbedingt erforderlich sind.
Bei Verwendung von FRP müssen nur die Rückrufe beibehalten werden, die die Interaktion mit der Außenwelt beschreiben. Mit anderen Worten, der Datenfluss wird funktional / deklarativ beschrieben, wenn ein FRP-Framework (z. B. Sodium) oder ein "FRP-ähnliches" Framework (z. B. RX) verwendet wird.
Natrium ist auch für Javascript / Typescript erhältlich.
quelle
Wenn Sie keine Kenntnisse über Rückruf und Rückruf zur Hölle haben, gibt es kein Problem. Das erste ist, dass Sie zurückrufen und die Hölle zurückrufen. Zum Beispiel: Der Rückruf zur Hölle ist wie ein Rückruf einer Klasse in einer Klasse. Wie Sie gehört haben über das in C, C ++ verschachtelte. Verschachtelt Bedeutet, dass eine Klasse in einer anderen Klasse.
quelle
Verwenden Sie jazz.js https://github.com/Javanile/Jazz.js
es vereinfacht sich wie folgt:
quelle