Warum bleibt meine Variable unverändert, nachdem ich sie innerhalb einer Funktion geändert habe? - Asynchrone Code-Referenz
Warum ist anhand der folgenden Beispiele outerScopeVar
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 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 JavaScript-Asynchronität . Sie können diese Frage gerne verbessern und vereinfachte Beispiele hinzufügen, mit denen sich die Community identifizieren kann.
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:
Antwort von @Felix Kling auf "Wie kann ich die Antwort von einem asynchronen Anruf zurückgeben?" . Siehe seine hervorragende Antwort zur Erläuterung synchroner und asynchroner Abläufe sowie den Abschnitt "Restrukturierungscode".
@Benjamin Gruenbaum hat sich auch viel Mühe gegeben, die Asynchronität im selben Thread zu erklären.Die Antwort von @Matt Esch auf "Daten aus fs.readFile abrufen" erklärt auch die Asynchronität auf einfache Weise sehr gut.
Die Antwort auf die vorliegende Frage
Lassen Sie uns zuerst das allgemeine Verhalten verfolgen. In allen Beispielen wird das outerScopeVar
innerhalb einer Funktion geändert . Diese Funktion wird eindeutig nicht sofort ausgeführt, sondern zugewiesen oder als Argument übergeben. Das nennen wir einen Rückruf .
Die Frage ist nun, wann dieser Rückruf aufgerufen wird.
Das hängt vom Fall ab. 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 das Timeout nicht von abgebrochen wurdeclearTimeout
. Hinweis: Selbst bei Verwendung0
als Verzögerung haben alle Browser eine minimale Zeitüberschreitungsverzögerung (in der HTML5-Spezifikation auf 4 ms festgelegt).$.post
Der Rückruf von jQuery wird möglicherweise irgendwann in der Zukunft aufgerufen , wenn (und wenn) die Ajax-Anforderung erfolgreich abgeschlossen wurde.- Node.js wird
fs.readFile
möglicherweise irgendwann in der Zukunft 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 "irgendwann in der Zukunft" wird als asynchroner Fluss bezeichnet .
Die asynchrone Ausführung wird aus dem synchronen Fluss verdrängt. Das heißt, der asynchrone Code wird niemals ausgeführt, während der synchrone Codestapel ausgeführt wird. Dies ist die Bedeutung von JavaScript als Single-Thread.
Insbesondere wenn die JS-Engine inaktiv ist und keinen Stapel von (a) synchronem Code ausführt, werden Ereignisse abgefragt, die möglicherweise asynchrone Rückrufe ausgelöst haben (z. B. abgelaufenes Zeitlimit, empfangene Netzwerkantwort), und diese nacheinander ausgeführt. Dies wird als Ereignisschleife angesehen .
Das heißt, der in den handgezeichneten roten Formen hervorgehobene asynchrone Code kann erst ausgeführt werden, 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 nur 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 innerhalb dieser asynchronen Funktion gestartet / aufgerufen werden. Wenn Sie beispielsweise alert
s und console.log
s auch innerhalb der Rückruffunktion verschieben, wird das erwartete Ergebnis ausgegeben, da das Ergebnis zu diesem Zeitpunkt verfügbar ist.
Implementierung Ihrer eigenen Rückruflogik
Oft müssen Sie mit dem Ergebnis einer asynchronen Funktion mehr oder mit dem Ergebnis verschiedene Dinge tun, je nachdem, wo die asynchrone Funktion aufgerufen wurde. Lassen Sie uns ein etwas komplexeres Beispiel angehen:
var outerScopeVar;
helloCatAsync();
alert(outerScopeVar);
function helloCatAsync() {
setTimeout(function() {
outerScopeVar = 'Nya';
}, Math.random() * 2000);
}
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.
Lassen Sie uns ein eigenes Rückrufsystem implementieren. Zunächst einmal 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 Rückruf. 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):
// 1. Call helloCatAsync passing a callback function,
// which will be called receiving the result from the async operation
helloCatAsync(function(result) {
// 5. Received the result from the async function,
// now do whatever you want with it:
alert(result);
});
// 2. The "callback" parameter is a reference to the function which
// was passed as argument from the helloCatAsync call
function helloCatAsync(callback) {
// 3. Start async operation:
setTimeout(function() {
// 4. Finished async operation,
// call the callback passing the result as argument
callback('Nya');
}, Math.random() * 2000);
}
Codefragment des obigen Beispiels:
// 1. Call helloCatAsync passing a callback function,
// which will be called receiving the result from the async operation
console.log("1. function called...")
helloCatAsync(function(result) {
// 5. Received the result from the async function,
// now do whatever you want with it:
console.log("5. result is: ", result);
});
// 2. The "callback" parameter is a reference to the function which
// was passed as argument from the helloCatAsync call
function helloCatAsync(callback) {
console.log("2. callback here is the function passed as argument above...")
// 3. Start async operation:
setTimeout(function() {
console.log("3. start async operation...")
console.log("4. finished async operation, calling the callback, passing the result...")
// 4. Finished async operation,
// call the callback passing the result as argument
callback('Nya');
}, Math.random() * 2000);
}
In realen Anwendungsfällen stellen die DOM-API und die meisten Bibliotheken in den meisten Fällen bereits die Rückruffunktion bereit (die helloCatAsync
Implementierung in diesem demonstrativen Beispiel). Sie müssen nur die Rückruffunktion übergeben und verstehen, dass sie außerhalb des synchronen Flusses ausgeführt wird, und Ihren Code neu strukturieren, um dies zu berücksichtigen.
Sie werden auch feststellen, dass es aufgrund der asynchronen Natur unmöglich ist, return
einen Wert von einem asynchronen Fluss zurück zu dem synchronen Fluss zu setzen, 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 verwenden, müssen Sie das Rückrufmuster verwenden oder ... Versprechen.
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 Promise - MDN ).
Versprechen (auch bekannt als Futures) bieten ein lineareres und damit angenehmeres Lesen des asynchronen Codes, aber die Erklärung ihrer gesamten Funktionalität fällt nicht in den Geltungsbereich dieser Frage. Stattdessen überlasse ich diese hervorragenden Ressourcen den Interessierten:
Weitere Informationen zur JavaScript-Asynchronität
- Die Kunst des Knotens - Rückrufe erklärt asynchronen Code und Rückrufe sehr gut mit Vanilla JS-Beispielen und Node.js-Code.
Hinweis: Ich habe diese Antwort als Community-Wiki markiert, daher kann jeder mit mindestens 100 Reputationen sie bearbeiten und verbessern! Bitte zögern Sie nicht, diese Antwort zu verbessern, oder senden Sie eine völlig neue Antwort, wenn Sie dies ebenfalls möchten.
Ich möchte diese Frage in ein kanonisches Thema verwandeln, um Asynchronitätsprobleme zu beantworten, die nichts mit Ajax zu tun haben (es gibt die Möglichkeit, die Antwort von einem AJAX-Aufruf zurückzugeben? ). Daher benötigt dieses Thema Ihre Hilfe, um so gut und hilfreich wie möglich zu sein !
Fabrícios Antwort ist genau richtig; aber ich wollte seine Antwort durch etwas weniger Technisches 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 verlief das Gespräch:
Me : Hallo Bob, ich muss wissen , wie wir foo ‚d der Bar ‘ letzte Woche d. Jim möchte einen Bericht darüber, und Sie sind der einzige, der die Details darüber kennt.
Bob : Sicher, aber ich brauche ungefähr 30 Minuten?
Ich : Das ist großartig, Bob. Rufen Sie mich zurück, wenn Sie die Informationen haben!
Zu diesem Zeitpunkt legte ich auf. Da ich Informationen von Bob brauchte, um meinen Bericht zu vervollständigen, verließ ich den Bericht und ging stattdessen auf einen Kaffee, dann holte ich mir eine E-Mail. 40 Minuten später (Bob ist langsam) rief Bob zurück und gab mir die Informationen, die ich brauchte. Zu diesem Zeitpunkt 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.
Me : Hallo Bob, ich muss wissen , wie wir foo ‚d der Bar ‘ letzte Woche d. Jim möchte einen Bericht darüber, und Sie sind der einzige, der die Details darüber kennt.
Bob : Sicher, aber ich brauche ungefähr 30 Minuten?
Ich : Das ist großartig, Bob. Ich werde warten.
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 asynchrones oder 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 alles langsame Vorgänge (im Kontext des modernen Rechnens).
Anstatt auf den Abschluss dieser langsamen Vorgänge zu warten , können Sie mit JavaScript eine Rückruffunktion registrieren, die ausgeführt wird, wenn der langsame Vorgang abgeschlossen ist. In der Zwischenzeit wird JavaScript jedoch weiterhin anderen Code ausführen. Die Tatsache, dass JavaScript anderen Code ausführt , während auf den Abschluss des langsamen Vorgangs gewartet wird, macht das Verhalten asynchron . Hätte JavaScript gewartet, bis der Vorgang abgeschlossen war, bevor ein anderer Code ausgeführt wurde, wäre dies ein synchrones Verhalten gewesen.
var outerScopeVar;
var img = document.createElement('img');
// Here we register the callback function.
img.onload = function() {
// Code within this function will be executed once the image has loaded.
outerScopeVar = this.width;
};
// But, while the image is loading, JavaScript continues executing, and
// processes the following lines of JavaScript.
img.src = 'lolcat.png';
alert(outerScopeVar);
Im obigen Code bitten wir JavaScript zu laden lolcat.png
, was eine langsame Operation ist. Die Rückruffunktion wird ausgeführt, sobald dieser langsame Vorgang ausgeführt wurde. In der Zwischenzeit verarbeitet JavaScript die nächsten Codezeilen weiter. dh alert(outerScopeVar)
.
Aus diesem Grund wird die Warnung angezeigt undefined
. da das alert()
sofort verarbeitet wird, anstatt nachdem das Bild geladen wurde.
Um unseren Code zu beheben, alles , was wir tun müssen , ist die bewegen alert(outerScopeVar)
Code in der Callback - Funktion. Infolgedessen benötigen wir die outerScopeVar
als globale Variable deklarierte Variable nicht mehr .
var img = document.createElement('img');
img.onload = function() {
var localScopeVar = this.width;
alert(localScopeVar);
};
img.src = 'lolcat.png';
Sie werden immer sehen, dass ein Rückruf als Funktion angegeben wird, da dies die einzige * Möglichkeit in JavaScript 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 können Sie auch verwenden eval()
, ist aber für diesen Zweck eval()
böse
Wie lasse ich meinen Anrufer warten?
Möglicherweise haben Sie derzeit einen ähnlichen Code.
function getWidthOfImage(src) {
var outerScopeVar;
var img = document.createElement('img');
img.onload = function() {
outerScopeVar = this.width;
};
img.src = src;
return outerScopeVar;
}
var width = getWidthOfImage('lolcat.png');
alert(width);
Jetzt wissen wir jedoch, dass return outerScopeVar
dies sofort geschieht. bevor die onload
Rückruffunktion die Variable aktualisiert hat. Dies führt dazu, dass Sie getWidthOfImage()
zurückkehren undefined
und undefined
alarmiert werden.
Um dies zu beheben, müssen wir dem Funktionsaufruf erlauben getWidthOfImage()
, einen Rückruf zu registrieren, und dann die Warnung der Breite so verschieben, dass sie innerhalb dieses Rückrufs liegt.
function getWidthOfImage(src, cb) {
var img = document.createElement('img');
img.onload = function() {
cb(this.width);
};
img.src = src;
}
getWidthOfImage('lolcat.png', function (width) {
alert(width);
});
... beachten Sie nach wie vor, dass wir die globalen Variablen (in diesem Fall width
) entfernen konnten .
Hier finden Sie eine präzisere Antwort für Personen, die nach einer Kurzreferenz suchen, sowie einige Beispiele, die Versprechen und Async / Warten verwenden.
Beginnen Sie mit dem naiven Ansatz (der nicht funktioniert) für eine Funktion, die eine asynchrone Methode aufruft (in diesem Fall setTimeout
) und eine Nachricht zurückgibt:
function getMessage() {
var outerScopeVar;
setTimeout(function() {
outerScopeVar = 'Hello asynchronous world!';
}, 0);
return outerScopeVar;
}
console.log(getMessage());
undefined
wird in diesem Fall protokolliert, da getMessage
zurückgegeben wird, bevor der setTimeout
Rückruf aufgerufen und aktualisiert wird outerScopeVar
.
Die beiden Hauptlösungsmöglichkeiten sind Rückrufe und Versprechen :
Rückrufe
Die Änderung hier ist, dass getMessage
ein callback
Parameter akzeptiert wird, der aufgerufen wird, um die Ergebnisse an den aufrufenden Code zurückzusenden, sobald dieser verfügbar ist.
function getMessage(callback) {
setTimeout(function() {
callback('Hello asynchronous world!');
}, 0);
}
getMessage(function(message) {
console.log(message);
});
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 wird nativ in node.js (0.12+) und vielen aktuellen Browsern bereitgestellt, ist aber auch in Bibliotheken wie Bluebird und Q implementiert .
function getMessage() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Hello asynchronous world!');
}, 0);
});
}
getMessage().then(function(message) {
console.log(message);
});
jQuery Deferreds
jQuery bietet Funktionen, die Versprechungen mit seinen Zurückstellungen ähneln.
function getMessage() {
var deferred = $.Deferred();
setTimeout(function() {
deferred.resolve('Hello asynchronous world!');
}, 0);
return deferred.promise();
}
getMessage().done(function(message) {
console.log(message);
});
async / warten
Wenn Ihre JavaScript-Umgebung Unterstützung für async
und await
(wie Node.js 7.6+) enthält, können Sie Versprechen synchron innerhalb von async
Funktionen verwenden:
function getMessage () {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Hello asynchronous world!');
}, 0);
});
}
async function main() {
let message = await getMessage();
console.log(message);
}
main();
Um das Offensichtliche auszudrücken, repräsentiert die Tasse outerScopeVar
.
Asynchrone Funktionen sind wie ...
Die anderen Antworten sind ausgezeichnet und ich möchte nur eine direkte Antwort darauf geben. Nur auf asynchrone jQuery-Aufrufe beschränkt
Alle Ajax-Aufrufe (einschließlich $.get
oder $.post
oder $.ajax
) sind asynchron.
Betrachten Sie Ihr Beispiel
var outerScopeVar; //line 1
$.post('loldog', function(response) { //line 2
outerScopeVar = response;
});
alert(outerScopeVar); //line 3
Die Codeausführung beginnt in Zeile 1, deklariert die Variable und löst einen asynchronen Aufruf in Zeile 2 (dh die Nachanforderung) aus und setzt ihre Ausführung in Zeile 3 fort, ohne darauf zu warten, dass die Nachanforderung ihre Ausführung abschließt.
Nehmen wir an, die Post-Anfrage dauert 10 Sekunden. Der Wert von outerScopeVar
wird erst nach diesen 10 Sekunden festgelegt.
Ausprobieren,
var outerScopeVar; //line 1
$.post('loldog', function(response) { //line 2, takes 10 seconds to complete
outerScopeVar = response;
});
alert("Lets wait for some time here! Waiting is fun"); //line 3
alert(outerScopeVar); //line 4
Wenn Sie dies jetzt ausführen, wird in Zeile 3 eine Warnung angezeigt. Warten Sie nun einige Zeit, bis Sie sicher sind, dass die Post-Anfrage einen Wert zurückgegeben hat. Wenn Sie dann im Warnfeld auf OK klicken, wird bei der nächsten Warnung der erwartete Wert gedruckt, da Sie darauf gewartet haben.
Im realen Szenario wird der Code,
var outerScopeVar;
$.post('loldog', function(response) {
outerScopeVar = response;
alert(outerScopeVar);
});
Der gesamte Code, der von den asynchronen Aufrufen abhängt, wird in den asynchronen Block verschoben oder durch Warten auf die asynchronen Aufrufe.
In all diesen Szenarien outerScopeVar
wird ein Wert geändert oder asynchron zugewiesen oder tritt zu einem späteren Zeitpunkt auf (Warten oder Abwarten auf ein Ereignis), auf das 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 Ereignislisten, 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 alert(outerScopeVar);
währenddessen tritt das Ereignis möglicherweise nicht auf. Das heißt, die Funktion img.onload
wartet asynchron , bis das referenzierte Bild geladen ist. Dies geschieht im folgenden Beispiel - das Ereignis kann unterschiedlich sein.
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 der Event Queue
zur Ausführung angefügt wird , wodurch die garantierte Verzögerung erreicht wird.
3.
4.
Der Knoten kann als König der asynchronen Codierung betrachtet werden. Hier wird die markierte Funktion als Rückruf-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 Zurückgestellt, Versprechen und Zukunft in JavaScript?
https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript