Ik heb een functie foo
die een Ajax verzoek doet. Hoe kan ik het antwoord van foo
teruggeven?
Ik heb geprobeerd om de waarde van de success
callback terug te geven, maar ook om het antwoord toe te wijzen aan een lokale variabele in de functie en die terug te geven, maar geen van deze manieren geeft daadwerkelijk het antwoord terug.
function foo() {
var result;
$.ajax({
url: '...',
success: function(response) {
result = response;
// return response; // <- I tried that one as well
}
});
return result;
}
var result = foo(); // It always ends up being `undefined`.
→ Voor een meer algemene uitleg van async gedrag met verschillende voorbeelden, zie https://stackoverflow.com/q/23667086/218196
→ Als u het probleem al begrijpt, ga dan naar de mogelijke oplossingen hieronder.
Het probleem
De A in Ajax staat voor asynchroon . Dat betekent dat het versturen van het verzoek (of beter gezegd het ontvangen van het antwoord) uit de normale uitvoeringsstroom wordt gehaald. In jouw voorbeeld komt
$.ajax
onmiddellijk terug en het volgende statement,return result;
, wordt uitgevoerd voordat de functie die je alssuccess
callback hebt doorgegeven zelfs maar is aangeroepen. Hier is een analogie die hopelijk het verschil tussen synchrone en asynchrone flow duidelijker maakt:Synchroon
Stel je voor dat je een telefoontje pleegt naar een vriend en hem vraagt iets voor je op te zoeken. Hoewel het even kan duren, wacht je aan de telefoon en staar je in de ruimte, totdat je vriend je het antwoord geeft dat je nodig had. Hetzelfde gebeurt wanneer je een functie aanroept die "normale" code bevat:
function findItem() {
var item;
while(item_not_found) {
// search
}
return item;
}
var item = findItem();
// Do something with item
doSomethingElse();
Ook al duurt findItem
lang om uit te voeren, alle code die na var item = findItem();
komt, moet wachten totdat de functie het resultaat teruggeeft.
Je belt je vriend weer voor dezelfde reden. Maar deze keer vertel je hem dat je haast hebt en dat hij je moet terugbellen op je mobiele telefoon. U hangt op, verlaat het huis en doet wat u van plan was te doen. Zodra uw vriend u terugbelt, bent u bezig met de informatie die hij u gaf. Dat'is precies wat'er gebeurt als je een Ajax verzoek doet.
findItem(function(item) {
// Do something with item
});
doSomethingElse();
**Hoewel bepaalde asynchrone operaties synchrone tegenhangers hebben (zo ook "Ajax"), wordt het over het algemeen afgeraden om ze te gebruiken, vooral in een browser context. Waarom is het slecht vraag je je af? JavaScript draait in de UI thread van de browser en elk lang lopend proces zal de UI blokkeren, waardoor deze niet meer reageert. Bovendien is er een bovengrens aan de uitvoeringstijd voor JavaScript en zal de browser de gebruiker vragen of hij met de uitvoering wil doorgaan of niet. Dit alles is echt een slechte gebruikerservaring. De gebruiker zal niet in staat zijn om te zien of alles goed werkt of niet. Bovendien zal het effect erger zijn voor gebruikers met een trage verbinding. In het volgende zullen we drie verschillende oplossingen bekijken die allemaal op elkaar voortbouwen:
async/await
(ES2017+, beschikbaar in oudere browsers als je een transpiler of regenerator gebruikt)then()
(ES2015+, beschikbaar in oudere browsers als je een van de vele belofte bibliotheken gebruikt)
Alledrie zijn beschikbaar in de huidige browsers, en in node 7+.async/await
De ECMAScript versie uitgebracht in 2017 introduceerde syntax-level support voor asynchrone functies. Met behulp van async
en await
kun je asynchroon schrijven in een "synchrone stijl". De code is nog steeds asynchroon, maar het'is makkelijker te lezen/begrijpen.
async/await
bouwt bovenop beloften: een async
functie retourneert altijd een belofte. await
"unwraps" een belofte en resulteert ofwel in de waarde waarmee de belofte werd opgelost of gooit een fout als de belofte werd afgewezen.
Belangrijk: Je kunt await
alleen gebruiken binnen een async
functie. Op dit moment wordt top-level await
nog niet ondersteund, dus moet je misschien een async IIFE (Immediately Invoked Function Expression) maken om een async
context te starten.
Je kunt meer lezen over async
en await
op MDN.
Hier is een voorbeeld dat voortbouwt op bovenstaande vertraging:
// 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);
})();
async/await
. Je kunt ook oudere omgevingen ondersteunen door je code om te zetten naar ES5 met behulp van regenerator (of tools die regenerator gebruiken, zoals Babel).Een callback is eenvoudigweg een functie die aan een andere functie wordt doorgegeven. Die andere functie kan de doorgegeven functie aanroepen wanneer het klaar is. In de context van een asynchroon proces zal de callback worden aangeroepen wanneer het asynchroon proces klaar is. Gewoonlijk wordt het resultaat doorgegeven aan de callback.
In het voorbeeld van de vraag, kun je foo
een callback laten accepteren en deze gebruiken als succes
callback. Dus dit
var result = foo();
// Code that depends on 'result'
wordt
foo(function(result) {
// Code that depends on 'result'
});
Hier hebben we de functie "inline" gedefinieerd, maar je kunt elke functie verwijzing doorgeven:
function myCallback(result) {
// Code that depends on 'result'
}
foo(myCallback);
foo
zelf is als volgt gedefinieerd:
function foo(callback) {
$.ajax({
// ...
success: callback
});
}
callback
zal verwijzen naar de functie die we doorgeven aan foo
wanneer we deze aanroepen en we geven deze simpelweg door aan success
. Dat wil zeggen, zodra het Ajax verzoek succesvol is, zal $.ajax
callback
aanroepen en het antwoord doorgeven aan de callback (waarnaar verwezen kan worden met result
, omdat dit is hoe we de callback gedefinieerd hebben).
Je kunt het antwoord ook verwerken voordat je het aan de callback doorgeeft:
function foo(callback) {
$.ajax({
// ...
success: function(response) {
// For example, filter the response
callback(filtered_response);
}
});
}
De Promise API is een nieuwe functie van ECMAScript 6 (ES2015), maar het heeft al goede browser ondersteuning. Er zijn ook veel bibliotheken die de standaard Promises API implementeren en aanvullende methoden bieden om het gebruik en de samenstelling van asynchrone functies te vergemakkelijken (bijv. bluebird). Beloften zijn containers voor toekomstige waarden. Wanneer de belofte de waarde ontvangt (het is opgelost) of wanneer het wordt geannuleerd (afgewezen), informeert het al zijn "listeners" die toegang willen tot deze waarde. Het voordeel ten opzichte van gewone callbacks is dat ze je toelaten je code te ontkoppelen en dat ze gemakkelijker zijn samen te stellen. Hier is een eenvoudig voorbeeld van het gebruik van een belofte:
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).
});
Toegepast op onze Ajax call zouden we beloftes als volgt kunnen gebruiken:
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
});
Het beschrijven van alle voordelen die promise bieden valt buiten het bestek van dit antwoord, maar als je nieuwe code schrijft, zou je ze serieus moeten overwegen. Ze zorgen voor een grote abstractie en scheiding van je code. Meer informatie over beloften: HTML5 rocks - JavaScript Promises
Uitgestelde objecten zijn jQuery's aangepaste implementatie van beloften (voordat de belofte-API werd gestandaardiseerd). Ze gedragen zich bijna als promises maar stellen een iets andere API bloot. Elke Ajax methode van jQuery retourneert al een "deferred object" (eigenlijk een belofte van een deferred object) die je gewoon vanuit je functie kunt retourneren:
function ajax() {
return $.ajax(...);
}
ajax().done(function(result) {
// Code depending on result
}).fail(function() {
// An error occurred
});
Hou in gedachten dat beloftes en uitgestelde objecten enkel containers zijn voor een toekomstige waarde, ze zijn niet de waarde zelf. Bijvoorbeeld, stel dat je het volgende had:
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
}
Deze code begrijpt de bovenstaande asynchronie problemen niet. Specifiek, $.ajax()
bevriest de code niet terwijl het de '/password' pagina op je server controleert - het stuurt een verzoek naar de server en terwijl het wacht, retourneert het onmiddellijk een jQuery Ajax Deferred object, niet het antwoord van de server. Dat betekent dat het if
statement altijd dit Deferred object krijgt, het als true
behandelt, en verder gaat alsof de gebruiker is ingelogd. Dat is niet goed.
Maar de oplossing is eenvoudig:
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
});
Zoals ik al zei, sommige(!) asynchrone operaties hebben synchrone tegenhangers. Ik ben geen voorstander van het gebruik ervan, maar voor de volledigheid volgt hier hoe je een synchrone oproep zou uitvoeren:
Als je direct een XMLHTTPRequest
object gebruikt, geef dan false
door als derde argument aan .open
.
Als je jQuery gebruikt, kun je de async
optie op false
zetten. Merk op dat deze optie deprecated is sinds jQuery 1.8.
Je kunt dan nog steeds een success
callback gebruiken of de responseText
eigenschap van het jqXHR object benaderen:
function foo() {
var jqXHR = $.ajax({
//...
async: false
});
return jqXHR.responseText;
}
Als je een andere jQuery Ajax methode gebruikt, zoals $.get
, $.getJSON
, etc., moet je deze veranderen in $.ajax
(omdat je alleen configuratie parameters kunt doorgeven aan $.ajax
).
**Het is niet mogelijk om een synchroon JSONP verzoek te doen. JSONP is van nature altijd asynchroon (nog een reden om deze optie niet eens te overwegen).
Je code zou iets in de trant van dit moeten zijn:
function foo() {
var httpRequest = new XMLHttpRequest();
httpRequest.open('GET', "/echo/json");
httpRequest.send();
return httpRequest.responseText;
}
var result = foo(); // always ends up being 'undefined'
Felix Kling heeft goed werk verricht met het schrijven van een antwoord voor mensen die jQuery gebruiken voor AJAX, ik'heb besloten om een alternatief te bieden voor mensen die'dat niet doen.
Dit is een korte samenvatting van "Uitleg van het probleem" uit het andere antwoord, als je'niet zeker bent na het lezen van dit, lees dat dan.
De A in AJAX staat voor asynchroon. Dat betekent dat het verzenden van het verzoek (of liever het ontvangen van het antwoord) uit de normale uitvoeringsstroom wordt gehaald. In uw voorbeeld, .send
keert onmiddellijk terug en het volgende statement, return result;
, wordt uitgevoerd voordat de functie die u als success
callback doorgaf zelfs maar is aangeroepen.
Dit betekent dat wanneer je'teruggaat, de luisteraar die je'hebt gedefinieerd nog niet is uitgevoerd, wat betekent dat de waarde die je'teruggeeft nog niet is gedefinieerd.
Hier is een eenvoudige analogie
function getFive(){
var a;
setTimeout(function(){
a=5;
},10);
return a;
}
[(Fiddle)][2]
De geretourneerde waarde van a
is undefined
omdat het a=5
gedeelte nog niet is uitgevoerd. AJAX werkt zo, je'retourneert de waarde voordat de server de kans heeft gekregen om je browser te vertellen wat die waarde is.
Een mogelijke oplossing voor dit probleem is om re-actief te coderen, door je programma te vertellen wat het moet doen als de berekening voltooid is.
function onComplete(a){ // When the code completes, do this
alert(a);
}
function getFive(whenDone){
var a;
setTimeout(function(){
a=5;
whenDone(a);
},10);
}
Dit wordt CPS genoemd. In principe geven we getFive
een actie door om uit te voeren wanneer het is voltooid, we vertellen onze code hoe te reageren wanneer een gebeurtenis is voltooid (zoals onze AJAX-oproep, of in dit geval de time-out).
Gebruik zou zijn:
getFive(onComplete);
Dat zou "5" op het scherm moeten brengen. [(Fiddle)][4].
Er zijn in principe twee manieren om dit op te lossen:
Wat synchrone AJAX betreft, doe het niet! Felix's antwoord geeft een aantal overtuigende argumenten waarom het'een slecht idee is. Om het samen te vatten, het'zal de browser van de gebruiker bevriezen totdat de server het antwoord terugstuurt en een zeer slechte gebruikerservaring creëren. Hier is nog een korte samenvatting van MDN over waarom:
XMLHttpRequest ondersteunt zowel synchrone als asynchrone communicatie. In het algemeen moeten asynchrone verzoeken echter worden verkozen boven synchrone verzoeken om prestatieredenen.
In het kort, synchrone verzoeken blokkeren de uitvoering van code... ...dit kan serieuze problemen veroorzaken...
Als je het moet doen, kun je een vlag doorgeven: Hier staat hoe:
var request = new XMLHttpRequest();
request.open('GET', 'yourURL', false); // `false` makes the request synchronous
request.send(null);
if (request.status === 200) {// That's HTTP for 'ok'
console.log(request.responseText);
}
Laat je functie een callback accepteren. In de voorbeeldcode kan foo
een callback accepteren. We'zullen onze code vertellen hoe te reageren wanneer foo
voltooid is.
Dus:
var result = foo();
// code that depends on `result` goes here
Wordt:
foo(function(result) {
// code that depends on `result`
});
Hier hebben we een anonieme functie doorgegeven, maar we kunnen net zo goed een verwijzing naar een bestaande functie doorgeven, zodat het er zo uitziet:
function myHandler(result) {
// code that depends on `result`
}
foo(myHandler);
Voor meer details over hoe dit soort callback-ontwerp wordt gedaan, bekijk Felix's antwoord.
Laten we nu foo zelf definiëren om dienovereenkomstig te handelen
function foo(callback) {
var httpRequest = new XMLHttpRequest();
httpRequest.onload = function(){ // when the request is loaded
callback(httpRequest.responseText);// we're calling our method
};
httpRequest.open('GET', "/echo/json");
httpRequest.send();
}
[(fiddle)][6]
We hebben nu onze foo functie een actie laten accepteren om uit te voeren wanneer de AJAX succesvol is voltooid, we kunnen dit verder uitbreiden door te controleren of de respons status niet 200 is en dienovereenkomstig te handelen (maak een fail handler en dergelijke). Effectief oplossen van ons probleem.
Als je nog steeds moeite hebt om dit te begrijpen lees de AJAX getting started guide op MDN.
XMLHttpRequest 2 (lees eerst de antwoorden van Benjamin Gruenbaum & Felix Kling) Als je geen jQuery gebruikt en een mooie korte XMLHttpRequest 2 wilt die werkt op de moderne browsers en ook op de mobiele browsers stel ik voor om het op deze manier te gebruiken:
function ajax(a, b, c){ // URL, callback, just a placeholder
c = new XMLHttpRequest;
c.open('GET', a);
c.onload = b;
c.send()
}
Zoals je kan zien:
this.response
Of als je om een of andere reden bind()
de callback aan een class:
e.target.response
Voorbeeld:
function callback(e){
console.log(this.response);
}
ajax('URL', callback);
Of (de bovenstaande is beter anonieme functies zijn altijd een probleem):
ajax('URL', function(e){console.log(this.response)});
XMLHttpRequest
variabele naam is een andere grote fout omdat je de callback moet uitvoeren binnen de onload/oreadystatechange closures anders ben je het kwijt.Als je nu iets complexers wilt met post en FormData kun je deze functie eenvoudig uitbreiden:
function x(a, b, e, d, c){ // URL, callback, method, formdata or {key:val},placeholder
c = new XMLHttpRequest;
c.open(e||'get', a);
c.onload = b;
c.send(d||null)
}
Nogmaals ... het'is een zeer korte functie, maar het krijgt wel & post. Voorbeelden van gebruik:
x(url, callback); // By default it's get so no need to set
x(url, callback, 'post', {'key': 'val'}); // No need to set post data
Of geef een volledig formulier-element door (document.getElementsByTagName('form')[0]
):
var fd = new FormData(form);
x(url, callback, 'post', fd);
Of stel een aantal aangepaste waarden in:
var fd = new FormData();
fd.append('key', 'val')
x(url, callback, 'post', fd);
Zoals vermeld in de opmerking breekt het gebruik van fout && synchroon wel volledig het punt van het antwoord. Wat is een mooie korte manier om Ajax op de juiste manier te gebruiken? Error handler
function x(a, b, e, d, c){ // URL, callback, method, formdata or {key:val}, placeholder
c = new XMLHttpRequest;
c.open(e||'get', a);
c.onload = b;
c.onerror = error;
c.send(d||null)
}
function error(e){
console.log('--Error--', this.type);
console.log('this: ', this);
console.log('Event: ', e)
}
function displayAjax(e){
console.log(e, this);
}
x('WRONGURL', displayAjax);
displayAjax()
onder this.statusText
als Method not Allowed
.
In het tweede geval werkt het gewoon. Je moet aan de server kant controleren of je de juiste post data hebt doorgegeven.
cross-domain not allowed gooit automatisch error.
In de foutmelding staan geen foutcodes.
Er is alleen de this.type
die is ingesteld op error.
Waarom een error handler toevoegen als je totaal geen controle hebt over fouten?
De meeste fouten worden hierbinnen geretourneerd in de callback functie displayAjax()
.
Dus: Foutcontroles zijn niet nodig als je'de URL goed kunt kopiëren en plakken. ;)
PS: Als eerste test schreef ik x('x', displayAjax)..., en het kreeg totaal geen reactie...?? Dus ik controleerde de map waar de HTML zich bevindt, en er was een bestand met de naam 'x.xml'. Dus zelfs als je de extensie van je bestand vergeet, zal XMLHttpRequest 2 het vinden. Ik LOL'dLeest een bestand synchroon
Don't do that.
Als je de browser een tijdje wil blokkeren laad dan een mooi groot .txt
bestand synchroon.
function omg(a, c){ // URL
c = new XMLHttpRequest;
c.open('GET', a, true);
c.send();
return c; // Or c.response
}
Nu kun je
var res = omg('thisIsGonnaBlockThePage.txt');
De bovenstaande functies zijn voor basisgebruik. Als je de functie wilt UITBREIDEN... Ja, dat kan. Ik'gebruik veel APIs en een van de eerste functies die ik in elke HTML pagina integreer is de eerste Ajax functie in dit antwoord, met alleen GET... Maar je kunt een heleboel dingen doen met XMLHttpRequest 2: Ik heb een download manager gemaakt (die aan beide kanten reeksen gebruikt met resume, filereader, filesystem), verschillende image resizers converters die canvas gebruiken, web SQL databases vullen met base64images en nog veel meer... Maar in deze gevallen zou je een functie alleen voor dat doel moeten maken... soms heb je een blob nodig, array buffers, je kunt headers instellen, mimetype overschrijven en er is nog veel meer... Maar de vraag hier is hoe je een Ajax antwoord terugstuurt... (Ik heb een makkelijke manier toegevoegd.)