→ Eine allgemeinere Erklärung des asynchronen Verhaltens anhand verschiedener Beispiele finden Sie unter Warum bleibt meine Variable unverändert, nachdem ich sie innerhalb einer Funktion geändert habe? - Asynchrone Code-Referenz
→ Wenn Sie das Problem bereits verstanden haben, fahren Sie mit den folgenden möglichen Lösungen fort.
Das Problem
Das A in Ajax steht für asynchron . Das bedeutet, dass das Senden der Anforderung (oder vielmehr das Empfangen der Antwort) aus dem normalen Ausführungsfluss herausgenommen wird. In Ihrem Beispiel wird $.ajax
sofort zurückgegeben und die nächste Anweisung return result;
wird ausgeführt, bevor die Funktion, die Sie als success
Rückruf übergeben haben, überhaupt aufgerufen wurde.
Hier ist eine Analogie, die hoffentlich den Unterschied zwischen synchronem und asynchronem Fluss klarer macht:
Synchron
Stellen Sie sich vor, Sie telefonieren mit einem Freund und bitten ihn, etwas für Sie nachzuschlagen. Obwohl es eine Weile dauern kann, warten Sie am Telefon und starren in den Weltraum, bis Ihr Freund Ihnen die Antwort gibt, die Sie benötigen.
Das gleiche passiert, wenn Sie einen Funktionsaufruf mit "normalem" Code ausführen:
function findItem() {
var item;
while(item_not_found) {
// search
}
return item;
}
var item = findItem();
// Do something with item
doSomethingElse();
Auch wenn findItem
eine lange Zeit ausführen dauern kann, kommt jeder Code nach var item = findItem();
muss warten , bis die Funktion das Ergebnis zurück.
Asynchron
Sie rufen Ihren Freund aus dem gleichen Grund erneut an. Aber diesmal sagst du ihm, dass du es eilig hast und er dich auf deinem Handy zurückrufen sollte. Sie legen auf, verlassen das Haus und tun, was Sie vorhatten. Sobald Ihr Freund Sie zurückruft, haben Sie es mit den Informationen zu tun, die er Ihnen gegeben hat.
Genau das passiert, wenn Sie eine Ajax-Anfrage stellen.
findItem(function(item) {
// Do something with item
});
doSomethingElse();
Anstatt auf die Antwort zu warten, wird die Ausführung sofort fortgesetzt und die Anweisung nach dem Ajax-Aufruf ausgeführt. Um die Antwort schließlich zu erhalten, geben Sie eine Funktion an, die aufgerufen werden kann, sobald die Antwort empfangen wurde, einen Rückruf (beachten Sie etwas? Rückruf ?). Jede Anweisung, die nach diesem Aufruf kommt, wird ausgeführt, bevor der Rückruf aufgerufen wird.
Lösung (en)
Umfassen Sie die asynchrone Natur von JavaScript! Während bestimmte asynchrone Operationen synchrone Gegenstücke bereitstellen (ebenso wie "Ajax"), wird generell davon abgeraten, diese zu verwenden, insbesondere in einem Browserkontext.
Warum ist es schlecht, fragst du?
JavaScript wird im UI-Thread des Browsers ausgeführt, und bei einem lang laufenden Prozess wird die Benutzeroberfläche gesperrt, sodass sie nicht mehr reagiert. Darüber hinaus gibt es eine Obergrenze für die Ausführungszeit von JavaScript, und der Browser fragt den Benutzer, ob die Ausführung fortgesetzt werden soll oder nicht.
All dies ist eine wirklich schlechte Benutzererfahrung. Der Benutzer kann nicht feststellen, ob alles einwandfrei funktioniert oder nicht. Darüber hinaus ist der Effekt für Benutzer mit einer langsamen Verbindung schlechter.
Im Folgenden werden drei verschiedene Lösungen betrachtet, die alle aufeinander aufbauen:
- Versprechen mit
async/await
(ES2017 +, verfügbar in älteren Browsern, wenn Sie einen Transpiler oder Regenerator verwenden)
- Rückrufe (beliebt im Knoten)
- Versprechen mit
then()
(ES2015 +, verfügbar in älteren Browsern, wenn Sie eine der vielen Versprechen-Bibliotheken verwenden)
Alle drei sind in aktuellen Browsern und Knoten 7+ verfügbar.
ES2017 +: Versprechen mit async/await
Mit der 2017 veröffentlichten ECMAScript-Version wurde die Unterstützung für asynchrone Funktionen auf Syntaxebene eingeführt. Mit Hilfe von async
und await
können Sie asynchron in einem "synchronen Stil" schreiben. Der Code ist immer noch asynchron, aber leichter zu lesen / verstehen.
async/await
baut auf Versprechungen auf: Eine async
Funktion gibt immer ein Versprechen zurück. await
"packt" ein Versprechen aus und führt entweder zu dem Wert, mit dem das Versprechen gelöst wurde, oder wirft einen Fehler aus, wenn das Versprechen abgelehnt wurde.
Wichtig: Sie können nur await
innerhalb einer async
Funktion verwenden. await
Derzeit wird die oberste Ebene noch nicht unterstützt. Daher müssen Sie möglicherweise ein asynchrones IIFE ( sofort aufgerufener Funktionsausdruck ) erstellen , um einen async
Kontext zu starten .
Sie können mehr über async
und await
auf MDN lesen .
Hier ist ein Beispiel, das auf der obigen Verzögerung aufbaut:
// Using 'superagent' which will return a promise.
var superagent = require('superagent')
// This is isn't declared as `async` because it already returns a promise
function delay() {
// `delay` returns a promise
return new Promise(function(resolve, reject) {
// Only `delay` is able to resolve or reject the promise
setTimeout(function() {
resolve(42); // After 3 seconds, resolve the promise with value 42
}, 3000);
});
}
async function getAllBooks() {
try {
// GET a list of book IDs of the current user
var bookIDs = await superagent.get('/user/books');
// wait for 3 seconds (just for the sake of this example)
await delay();
// GET information about each book
return await superagent.get('/books/ids='+JSON.stringify(bookIDs));
} catch(error) {
// If any of the awaited promises was rejected, this catch block
// would catch the rejection reason
return null;
}
}
// Start an IIFE to use `await` at the top level
(async function(){
let books = await getAllBooks();
console.log(books);
})();
Aktuelle Browser- und Knotenversionen unterstützen async/await
. Sie können auch ältere Umgebungen unterstützen, indem Sie Ihren Code mithilfe von Regenerator (oder Tools, die Regenerator verwenden, wie z. B. Babel ) in ES5 umwandeln .
Lassen Sie Funktionen Rückrufe akzeptieren
Ein Rückruf ist einfach eine Funktion, die an eine andere Funktion übergeben wird. Diese andere Funktion kann die übergebene Funktion aufrufen, wann immer sie bereit ist. Im Kontext eines asynchronen Prozesses wird der Rückruf immer dann aufgerufen, wenn der asynchrone Prozess ausgeführt wird. Normalerweise wird das Ergebnis an den Rückruf übergeben.
Im Beispiel der Frage können Sie foo
einen Rückruf annehmen und als success
Rückruf verwenden. Also das
var result = foo();
// Code that depends on 'result'
wird
foo(function(result) {
// Code that depends on 'result'
});
Hier haben wir die Funktion "inline" definiert, aber Sie können jede Funktionsreferenz übergeben:
function myCallback(result) {
// Code that depends on 'result'
}
foo(myCallback);
foo
selbst ist wie folgt definiert:
function foo(callback) {
$.ajax({
// ...
success: callback
});
}
callback
wird sich auf die Funktion beziehen, an die wir übergeben, foo
wenn wir sie aufrufen, und wir geben sie einfach an weiter success
. Dh wenn die Ajax-Anfrage erfolgreich ist, $.ajax
wird callback
die Antwort aufgerufen und an den Rückruf weitergeleitet (auf den verwiesen werden kann result
, da wir den Rückruf auf diese Weise definiert haben).
Sie können die Antwort auch verarbeiten, bevor Sie sie an den Rückruf weiterleiten:
function foo(callback) {
$.ajax({
// ...
success: function(response) {
// For example, filter the response
callback(filtered_response);
}
});
}
Es ist einfacher, Code mithilfe von Rückrufen zu schreiben, als es scheint. Schließlich ist JavaScript im Browser stark ereignisgesteuert (DOM-Ereignisse). Das Empfangen der Ajax-Antwort ist nichts anderes als ein Ereignis.
Wenn Sie mit Code von Drittanbietern arbeiten müssen, können Schwierigkeiten auftreten. Die meisten Probleme können jedoch gelöst werden, indem Sie nur den Anwendungsfluss durchdenken.
ES2015 +: Verspricht mit then ()
Die Promise-API ist eine neue Funktion von ECMAScript 6 (ES2015), bietet jedoch bereits eine gute Browserunterstützung . Es gibt auch viele Bibliotheken, die die Standard-Promises-API implementieren und zusätzliche Methoden bereitstellen, um die Verwendung und Zusammensetzung von asynchronen Funktionen (z . B. Bluebird ) zu vereinfachen .
Versprechen sind Container für zukünftige Werte. Wenn das Versprechen den Wert erhält (es wird aufgelöst ) oder wenn es storniert ( abgelehnt ) wird, benachrichtigt es alle "Listener", die auf diesen Wert zugreifen möchten.
Der Vorteil gegenüber einfachen Rückrufen besteht darin, dass Sie Ihren Code entkoppeln können und einfacher zu erstellen sind.
Hier ist ein einfaches Beispiel für die Verwendung eines Versprechens:
function delay() {
// `delay` returns a promise
return new Promise(function(resolve, reject) {
// Only `delay` is able to resolve or reject the promise
setTimeout(function() {
resolve(42); // After 3 seconds, resolve the promise with value 42
}, 3000);
});
}
delay()
.then(function(v) { // `delay` returns a promise
console.log(v); // Log the value once it is resolved
})
.catch(function(v) {
// Or do something else if it is rejected
// (it would not happen in this example, since `reject` is not called).
});
Auf unseren Ajax-Aufruf angewendet könnten wir solche Versprechen verwenden:
function ajax(url) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.onload = function() {
resolve(this.responseText);
};
xhr.onerror = reject;
xhr.open('GET', url);
xhr.send();
});
}
ajax("/echo/json")
.then(function(result) {
// Code depending on result
})
.catch(function() {
// An error occurred
});
Die Beschreibung aller Vorteile, die das Versprechen bietet, würde den Rahmen dieser Antwort sprengen. Wenn Sie jedoch neuen Code schreiben, sollten Sie diese ernsthaft in Betracht ziehen. Sie bieten eine großartige Abstraktion und Trennung Ihres Codes.
Weitere Informationen zu Versprechungen: HTML5 rockt - JavaScript-Versprechungen
Randnotiz: Zurückgestellte Objekte von jQuery
Zurückgestellte Objekte sind die benutzerdefinierte Implementierung von Versprechen durch jQuery (bevor die Promise-API standardisiert wurde). Sie verhalten sich fast wie Versprechen, zeigen jedoch eine etwas andere API.
Jede Ajax-Methode von jQuery gibt bereits ein "zurückgestelltes Objekt" zurück (eigentlich ein Versprechen eines zurückgestellten Objekts), das Sie einfach von Ihrer Funktion zurückgeben können:
function ajax() {
return $.ajax(...);
}
ajax().done(function(result) {
// Code depending on result
}).fail(function() {
// An error occurred
});
Randnotiz: Versprechen Fallstricke
Denken Sie daran, dass Versprechen und zurückgestellte Objekte nur Container für einen zukünftigen Wert sind, sie sind nicht der Wert selbst. Angenommen, Sie hatten Folgendes:
function checkPassword() {
return $.ajax({
url: '/password',
data: {
username: $('#username').val(),
password: $('#password').val()
},
type: 'POST',
dataType: 'json'
});
}
if (checkPassword()) {
// Tell the user they're logged in
}
Dieser Code versteht die oben genannten Asynchronitätsprobleme falsch. Insbesondere wird $.ajax()
der Code nicht eingefroren, während die Seite '/ password' auf Ihrem Server überprüft wird. Er sendet eine Anforderung an den Server und gibt während des Wartens sofort ein jQuery Ajax Deferred-Objekt zurück, nicht die Antwort vom Server. Das bedeutet, dass die if
Anweisung dieses verzögerte Objekt immer abruft, als behandelt true
und so fortfährt, als wäre der Benutzer angemeldet. Nicht gut.
Aber die Lösung ist einfach:
checkPassword()
.done(function(r) {
if (r) {
// Tell the user they're logged in
} else {
// Tell the user their password was bad
}
})
.fail(function(x) {
// Tell the user something bad happened
});
Nicht empfohlen: Synchrone "Ajax" -Aufrufe
Wie bereits erwähnt, haben einige (!) Asynchrone Operationen synchrone Gegenstücke. Ich befürworte ihre Verwendung nicht, aber der Vollständigkeit halber würden Sie hier einen synchronen Anruf ausführen:
Ohne jQuery
Wenn Sie ein XMLHTTPRequest
Objekt direkt verwenden , übergeben Sie es false
als drittes Argument an .open
.
jQuery
Wenn Sie jQuery verwenden , können Sie die async
Option auf setzen false
. Beachten Sie, dass diese Option seit jQuery 1.8 veraltet ist. Sie können dann entweder noch einen success
Rückruf verwenden oder auf die responseText
Eigenschaft des jqXHR-Objekts zugreifen :
function foo() {
var jqXHR = $.ajax({
//...
async: false
});
return jqXHR.responseText;
}
Wenn Sie eine andere jQuery Ajax - Methode verwenden, wie zum Beispiel $.get
, $.getJSON
usw., müssen Sie es ändern $.ajax
(da Sie nur Konfigurationsparameter passieren können $.ajax
).
Kopf hoch! Es ist nicht möglich, eine synchrone JSONP- Anfrage zu stellen. JSONP ist von Natur aus immer asynchron (ein weiterer Grund, diese Option nicht einmal in Betracht zu ziehen).