Warum ist outerScopeVar
in den folgenden Beispielen in allen Fällen undefiniert?
var outerScopeVar;
var img = document.createElement('img');
img.onload = function() {
outerScopeVar = this.width;
};
img.src = 'lolcat.png';
alert(outerScopeVar);
var outerScopeVar;
setTimeout(function() {
outerScopeVar = 'Hello Asynchronous World!';
}, 0);
alert(outerScopeVar);
// Example using some jQuery
var outerScopeVar;
$.post('loldog', function(response) {
outerScopeVar = response;
});
alert(outerScopeVar);
// Node.js example
var outerScopeVar;
fs.readFile('./catdog.html', function(err, data) {
outerScopeVar = data;
});
console.log(outerScopeVar);
// with promises
var outerScopeVar;
myPromise.then(function (response) {
outerScopeVar = response;
});
console.log(outerScopeVar);
// geolocation API
var outerScopeVar;
navigator.geolocation.getCurrentPosition(function (pos) {
outerScopeVar = pos;
});
console.log(outerScopeVar);
Warum wird es undefined
in all diesen Beispielen ausgegeben ? Ich möchte keine Problemumgehungen, ich möchte wissen, warum dies geschieht.
Hinweis: Dies ist eine kanonische Frage zur Asynchronität von JavaScript . Sie können diese Frage gerne verbessern und einfachere Beispiele hinzufügen, mit denen sich die Community identifizieren kann.
javascript
asynchronous
Fabrício Matté
quelle
quelle
Antworten:
Ein Wort Antwort: Asynchronität .
Vorworte
Dieses Thema wurde hier in Stack Overflow mindestens ein paar tausend Mal wiederholt. Daher möchte ich zunächst auf einige äußerst nützliche Ressourcen hinweisen:
@Felix Kling's Antwort auf "Rückgabe der Antwort von einem asynchronen Anruf" . Siehe seine ausgezeichnete Antwort, die synchrone und asynchrone Abläufe erklärt, sowie den Abschnitt "Restrukturierungscode".
@Benjamin Gruenbaum hat sich auch viel Mühe gegeben, um Asynchronität im selben Thread zu erklären.
Die Antwort von @Matt Esch auf "Get data from fs.readFile" erklärt auch die Asynchronität auf einfache Weise sehr gut.
Die Antwort auf die Frage zur Hand
Lassen Sie uns zuerst das übliche Verhalten verfolgen. In allen Beispielen wird das
outerScopeVar
innerhalb einer Funktion geändert . Diese Funktion wird offensichtlich nicht sofort ausgeführt, sondern als Argument zugewiesen oder übergeben. Das nennen wir einen Rückruf .Nun ist die Frage, wann dieser Rückruf aufgerufen wird.
Es kommt auf den Fall an. Versuchen wir noch einmal, ein häufiges Verhalten zu verfolgen:
img.onload
kann irgendwann in der Zukunft aufgerufen werden , wenn (und wenn) das Bild erfolgreich geladen wurde.setTimeout
kann irgendwann in der Zukunft aufgerufen werden , nachdem die Verzögerung abgelaufen ist und die Zeitüberschreitung von nicht abgebrochen wurdeclearTimeout
. Hinweis: Auch bei Verwendung0
als Verzögerung haben alle Browser eine minimale Zeitlimitüberschreitung (in der HTML5-Spezifikation auf 4 ms festgelegt).$.post
Der Rückruf von jQuery kann zu einem späteren Zeitpunkt aufgerufen werden , wenn (und wenn) die Ajax-Anforderung erfolgreich abgeschlossen wurde.fs.readFile
möglicherweise zu einem späteren Zeitpunkt aufgerufen , wenn die Datei erfolgreich gelesen wurde oder ein Fehler aufgetreten ist.In allen Fällen haben wir einen Rückruf, der möglicherweise irgendwann in der Zukunft ausgeführt wird . Dies ist "irgendwann in der Zukunft", was wir als asynchronen Fluss bezeichnen .
Die asynchrone Ausführung wird aus dem synchronen Datenfluss entfernt. Das heißt, der asynchrone Code wird niemals ausgeführt, während der synchrone Codestack ausgeführt wird. Dies ist die Bedeutung von Single-Threaded-JavaScript.
Wenn die JS-Engine im Leerlauf ist und keinen Stapel von (a) synchronem Code ausführt, sucht sie nach Ereignissen, die möglicherweise asynchrone Rückrufe ausgelöst haben (z. B. abgelaufenes Timeout, empfangene Netzwerkantwort), und führt sie nacheinander aus. Dies wird als Ereignisschleife angesehen .
Das heißt, der in den von Hand gezeichneten roten Formen hervorgehobene asynchrone Code wird möglicherweise erst ausgeführt, nachdem der gesamte verbleibende synchrone Code in den jeweiligen Codeblöcken ausgeführt wurde:
Kurz gesagt, die Rückruffunktionen werden synchron erstellt, aber asynchron ausgeführt. Sie können sich erst dann auf die Ausführung einer asynchronen Funktion verlassen, wenn Sie wissen, dass sie ausgeführt wurde, und wie geht das?
Es ist wirklich einfach. Die Logik, die von der Ausführung der asynchronen Funktion abhängt, sollte in dieser asynchronen Funktion gestartet / aufgerufen werden. Wenn Sie beispielsweise
alert
s undconsole.log
s innerhalb der Rückruffunktion verschieben, wird das erwartete Ergebnis ausgegeben, da das Ergebnis zu diesem Zeitpunkt verfügbar ist.Implementierung einer eigenen Callback-Logik
Je nachdem, wo die asynchrone Funktion aufgerufen wurde, müssen Sie häufig mehr mit dem Ergebnis einer asynchronen Funktion oder mit dem Ergebnis tun. Betrachten wir ein etwas komplexeres Beispiel:
Hinweis: Ich verwende
setTimeout
mit einer zufälligen Verzögerung als eine allgemeine asynchrone Funktion gilt das gleiche Beispiel zu Ajax,readFile
,onload
und anderer asynchroner Strömung.Dieses Beispiel hat eindeutig das gleiche Problem wie die anderen Beispiele. Es wartet nicht, bis die asynchrone Funktion ausgeführt wird.
Nehmen wir uns vor, ein eigenes Rückrufsystem zu implementieren. Zuerst werden wir das Hässliche los,
outerScopeVar
das in diesem Fall völlig nutzlos ist. Dann fügen wir einen Parameter hinzu, der ein Funktionsargument akzeptiert, unseren Callback. Wenn die asynchrone Operation beendet ist, rufen wir diesen Rückruf auf und übergeben das Ergebnis. Die Implementierung (bitte lesen Sie die Kommentare in der Reihenfolge):Code-Snippet des obigen Beispiels:
In den meisten realen Anwendungsfällen bieten die DOM-API und die meisten Bibliotheken bereits die Callback-Funktionalität (die
helloCatAsync
Implementierung in diesem anschaulichen Beispiel). Sie müssen nur die Rückruffunktion übergeben und verstehen, dass sie außerhalb des synchronen Ablaufs ausgeführt wird, und Ihren Code neu strukturieren, um dies zu berücksichtigen.Sie werden auch bemerken, dass es aufgrund der asynchronen Natur unmöglich ist,
return
einen Wert von einem asynchronen Rückfluss zu dem synchronen Rückfluss zu ändern, in dem der Rückruf definiert wurde, da die asynchronen Rückrufe ausgeführt werden, lange nachdem der synchrone Code bereits ausgeführt wurde.Anstatt
return
einen Wert aus einem asynchronen Rückruf zu erhalten, müssen Sie das Rückrufmuster oder ... Promises verwenden.Versprechen
Obwohl es Möglichkeiten gibt, die Callback-Hölle mit Vanilla JS in Schach zu halten , werden Versprechen immer beliebter und werden derzeit in ES6 standardisiert (siehe Versprechen - MDN ).
Promises (auch als Futures bezeichnet) ermöglichen ein lineareres und damit angenehmeres Lesen des asynchronen Codes. Die Erklärung der gesamten Funktionalität ist jedoch nicht Gegenstand dieser Frage. Stattdessen überlasse ich den Interessierten diese hervorragenden Ressourcen:
Weiteres Lesematerial zu JavaScript-Asynchronität
quelle
Fabrícios Antwort ist genau richtig; aber ich wollte seine Antwort mit etwas weniger Technischem ergänzen, das sich auf eine Analogie konzentriert, um das Konzept der Asynchronität zu erklären .
Eine Analogie ...
Gestern erforderte meine Arbeit einige Informationen von einem Kollegen. Ich habe ihn angerufen; So lief das Gespräch ab:
An diesem Punkt legte ich den Hörer auf. Da ich Informationen von Bob benötigte, um meinen Bericht zu vervollständigen, verließ ich den Bericht und ging stattdessen auf einen Kaffee, um eine E-Mail zu erhalten. 40 Minuten später (Bob ist langsam) rief Bob zurück und gab mir die Informationen, die ich brauchte. An diesem Punkt nahm ich meine Arbeit mit meinem Bericht wieder auf, da ich alle Informationen hatte, die ich brauchte.
Stellen Sie sich vor, das Gespräch wäre stattdessen so verlaufen.
Und ich saß da und wartete. Und wartete. Und wartete. Für 40 Minuten. Nichts tun als warten. Schließlich gab Bob mir die Informationen, wir legten auf und ich vervollständigte meinen Bericht. Aber ich hatte 40 Minuten Produktivität verloren.
Dies ist ein asynchrones vs. synchrones Verhalten
Genau das passiert in allen Beispielen unserer Frage. Das Laden eines Bildes, das Laden einer Datei von der Festplatte und das Anfordern einer Seite über AJAX sind allesamt langsame Vorgänge (im Kontext des modernen Computing).
Anstatt auf den Abschluss dieser langsamen Vorgänge zu warten , können Sie in JavaScript eine Rückruffunktion registrieren, die ausgeführt wird, wenn der langsame Vorgang abgeschlossen ist. In der Zwischenzeit führt JavaScript jedoch weiterhin anderen Code aus. Die Tatsache, dass JavaScript anderen Code ausführt , während auf den Abschluss des langsamen Vorgangs gewartet wird , führt dazu, dass das Verhalten asynchron ist . Wenn JavaScript gewartet hätte, bis der Vorgang abgeschlossen ist, bevor ein anderer Code ausgeführt wurde, wäre dies ein synchrones Verhalten gewesen.
Im obigen Code bitten wir Sie, JavaScript zu laden
lolcat.png
. Dies ist eine langsame Operation. Die Rückruffunktion wird ausgeführt, sobald diese langsame Operation ausgeführt wurde. In der Zwischenzeit verarbeitet JavaScript die nächsten Codezeilen weiter. dhalert(outerScopeVar)
.Aus diesem Grund wird die Warnung angezeigt
undefined
. da deralert()
sofort verarbeitet wird, anstatt nachdem das bild geladen wurde.Um unseren Code zu reparieren, müssen wir den
alert(outerScopeVar)
Code nur in die Rückruffunktion verschieben. Infolgedessen brauchen wir dieouterScopeVar
als globale Variable deklarierte Variable nicht mehr .Sie werden immer sehen, dass ein Rückruf als Funktion angegeben wird, da dies in JavaScript die einzige Möglichkeit ist, Code zu definieren, ihn jedoch erst später auszuführen.
Daher ist in allen unseren Beispielen
function() { /* Do something */ }
der Rückruf; Um alle Beispiele zu korrigieren , müssen wir nur den Code, der die Antwort der Operation benötigt, dorthin verschieben!* Technisch kann man das auch nutzen
eval()
, ist abereval()
böse für diesen ZweckWie lasse ich meinen Anrufer warten?
Möglicherweise haben Sie derzeit einen ähnlichen Code.
Wir wissen jedoch jetzt, dass das
return outerScopeVar
sofort passiert; bevor dieonload
Rückruffunktion die Variable aktualisiert hat. Dies führt dazu, dass SiegetWidthOfImage()
zurückkehrenundefined
undundefined
alarmiert werden.Um dies zu beheben, müssen wir zulassen, dass die aufrufende Funktion
getWidthOfImage()
einen Rückruf registriert, und dann den Alert der Breite in diesen Rückruf verschieben.... wie zuvor, beachten Sie, dass wir die globalen Variablen (in diesem Fall
width
) entfernen konnten .quelle
Hier finden Sie eine präzisere Antwort für Personen, die nach einer Kurzreferenz suchen, sowie einige Beispiele für die Verwendung von Versprechen und Async / Warten.
Beginnen Sie mit dem naiven Ansatz (der nicht funktioniert) für eine Funktion, die (in diesem Fall
setTimeout
) eine asynchrone Methode aufruft und eine Nachricht zurückgibt:undefined
wird in diesem Fall protokolliert, dagetMessage
vor demsetTimeout
Aufruf des Rückrufs und der Aktualisierung zurückgegeben wirdouterScopeVar
.Die beiden wichtigsten Lösungsansätze sind Rückrufe und Versprechen :
Rückrufe
Die Änderung hier ist, dass
getMessage
eincallback
Parameter akzeptiert wird, der aufgerufen wird, um die Ergebnisse an den aufrufenden Code zurückzugeben, sobald dieser verfügbar ist.Versprechen
Versprechen bieten eine Alternative, die flexibler ist als Rückrufe, da sie auf natürliche Weise kombiniert werden können, um mehrere asynchrone Vorgänge zu koordinieren. Eine Promises / A + -Standardimplementierung ist nativ in node.js (0.12+) und vielen aktuellen Browsern enthalten, wird aber auch in Bibliotheken wie Bluebird und Q implementiert .
jQuery- Zurückstellung
jQuery bietet Funktionen, die mit seinen Zurückgestellten den Versprechungen ähneln.
async / erwarten
Wenn Ihre JavaScript-Umgebung Unterstützung für
async
undawait
(wie Node.js 7.6+) enthält, können Sie Versprechen innerhalb vonasync
Funktionen synchron verwenden:quelle
function getMessage(param1, param2, callback) {...}
.async/await
Probe, aber ich stoße auf Probleme. Anstatt a zu instanziierennew Promise
,.Get()
rufe ich an und habe daher keinen Zugriff auf eineresolve()
Methode. Also gibt meingetMessage()
das Versprechen zurück und nicht das Ergebnis. Könnten Sie Ihre Antwort ein wenig bearbeiten, um eine funktionierende Syntax dafür zu zeigen?.Get()
Anruf meinst . Vermutlich am besten eine neue Frage posten.Um das Offensichtliche auszudrücken, stellt die Tasse dar
outerScopeVar
.Asynchrone Funktionen sind wie ...
quelle
Die anderen Antworten sind ausgezeichnet und ich möchte nur eine direkte Antwort darauf geben. Nur auf asynchrone jQuery-Aufrufe beschränken
Alle Ajax-Aufrufe (einschließlich
$.get
oder$.post
oder$.ajax
) sind asynchron.Betrachten Sie Ihr Beispiel
Die Codeausführung beginnt in Zeile 1, deklariert die Variable und löst einen asynchronen Aufruf in Zeile 2 aus (dh die Nachanforderung) und setzt die Ausführung in Zeile 3 fort, ohne auf den Abschluss der Nachanforderung zu warten.
Nehmen wir an, dass die Nachbearbeitungsanforderung 10 Sekunden dauert. Der Wert von
outerScopeVar
wird erst nach diesen 10 Sekunden festgelegt.Ausprobieren,
Wenn Sie dies ausführen, wird in Zeile 3 eine Warnung angezeigt. Warten Sie nun einige Zeit, bis Sie sicher sind, dass die Post-Anfrage einen bestimmten Wert zurückgegeben hat. Wenn Sie dann im Warnungsfeld auf OK klicken, gibt die nächste Warnung den erwarteten Wert aus, da Sie darauf gewartet haben.
Im realen Szenario wird der Code,
Der gesamte Code, der von den asynchronen Aufrufen abhängt, wird innerhalb des asynchronen Blocks oder durch Warten auf die asynchronen Aufrufe verschoben.
quelle
or by waiting on the asynchronous calls
Wie macht man das?In all diesen Szenarien
outerScopeVar
wird ein Wert asynchron oder zu einem späteren Zeitpunkt (Warten oder Abwarten auf ein Ereignis) geändert oder zugewiesen , worauf die aktuelle Ausführung nicht wartet. In all diesen Fällen führt der aktuelle Ausführungsfluss zuouterScopeVar = undefined
Lassen Sie uns die einzelnen Beispiele diskutieren (ich habe den Teil markiert, der asynchron oder verzögert aufgerufen wird, damit einige Ereignisse auftreten):
1.
Hier registrieren wir einen Ereignislistener, der bei diesem bestimmten Ereignis ausgeführt wird. Hier wird das Bild geladen. Dann wird die aktuelle Ausführung mit den nächsten Zeilen fortgesetzt
img.src = 'lolcat.png';
und in deralert(outerScopeVar);
Zwischenzeit tritt das Ereignis möglicherweise nicht auf. Das heißt, die Funktionimg.onload
wartet darauf, dass das betreffende Bild asynchron geladen wird. Dies wird in allen folgenden Beispielen geschehen - das Ereignis kann davon abweichen.2.
Hier spielt das Timeout-Ereignis die Rolle, die den Handler nach der angegebenen Zeit aufruft. Hier ist es
0
, aber es registriert immer noch ein asynchrones Ereignis, das an die letzte Position derEvent Queue
zur Ausführung hinzuaddiert wird, was die garantierte Verzögerung bewirkt.3.
Diesmal Ajax Callback.
4.
Der Knoten kann als König der asynchronen Codierung betrachtet werden. Hier wird die markierte Funktion als Callback-Handler registriert, der nach dem Lesen der angegebenen Datei ausgeführt wird.
5.
Offensichtliches Versprechen (etwas wird in Zukunft getan) ist asynchron. Siehe Was sind die Unterschiede zwischen Aufgeschoben, Versprechen und Zukünftig in JavaScript?
https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-inJavascript
quelle