Hoe retourneer ik het antwoord van een asynchrone oproep?
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`.
5183
3
Ook al duurt
findItem
lang om uit te voeren, alle code die navar item = findItem();
komt, moet wachten totdat de functie het resultaat teruggeeft.Asynchroon
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.
In plaats van te wachten op het antwoord, gaat de uitvoering meteen door en wordt het statement na de Ajax-aanroep uitgevoerd. Om het antwoord uiteindelijk te krijgen, voorzie je een functie die moet worden aangeroepen zodra het antwoord is ontvangen, een callback (merk je iets? callback ?). Elk statement dat na die call komt wordt uitgevoerd voordat de callback wordt aangeroepen.
Solution(s)
**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)Beloften met
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+.ES2017+: Beloftes met
async/await
De ECMAScript versie uitgebracht in 2017 introduceerde syntax-level support voor asynchrone functies. Met behulp van
async
enawait
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: eenasync
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 kuntawait
alleen gebruiken binnen eenasync
functie. Op dit moment wordt top-levelawait
nog niet ondersteund, dus moet je misschien een async IIFE (Immediately Invoked Function Expression) maken om eenasync
context te starten. Je kunt meer lezen overasync
enawait
op MDN. Hier is een voorbeeld dat voortbouwt op bovenstaande vertraging:De huidige browser en node versies ondersteunen
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).Laat functies callbacks accepteren
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 alssucces
callback. Dus ditwordt
Hier hebben we de functie "inline" gedefinieerd, maar je kunt elke functie verwijzing doorgeven:
foo
zelf is als volgt gedefinieerd:callback
zal verwijzen naar de functie die we doorgeven aanfoo
wanneer we deze aanroepen en we geven deze simpelweg door aansuccess
. Dat wil zeggen, zodra het Ajax verzoek succesvol is, zal$.ajax
callback
aanroepen en het antwoord doorgeven aan de callback (waarnaar verwezen kan worden metresult
, omdat dit is hoe we de callback gedefinieerd hebben). Je kunt het antwoord ook verwerken voordat je het aan de callback doorgeeft:Het's makkelijker om code te schrijven met behulp van callbacks dan het misschien lijkt. Immers, JavaScript in de browser is zwaar event-driven (DOM events). Het ontvangen van het Ajax antwoord is niets anders dan een gebeurtenis.
Er kunnen problemen ontstaan wanneer je moet werken met code van derden, maar de meeste problemen kunnen worden opgelost door gewoon de applicatie flow te doordenken.
ES2015+: Beloftes met then()
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:
Toegepast op onze Ajax call zouden we beloftes als volgt kunnen gebruiken:
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
Kanttekening: jQuery's uitgestelde objecten
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:
Kanttekening: Belofte gotchas
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:
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 hetif
statement altijd dit Deferred object krijgt, het alstrue
behandelt, en verder gaat alsof de gebruiker is ingelogd. Dat is niet goed. Maar de oplossing is eenvoudig:Niet aanbevolen: Synchrone "Ajax" aanroepen
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:
Zonder jQuery
Als je direct een
XMLHTTPRequest
object gebruikt, geef danfalse
door als derde argument aan.open
.jQuery
Als je jQuery gebruikt, kun je de
async
optie opfalse
zetten. Merk op dat deze optie deprecated is sinds jQuery 1.8. Je kunt dan nog steeds eensuccess
callback gebruiken of deresponseText
eigenschap van het jqXHR object benaderen: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).Als je geen jQuery in je code gebruikt, is dit antwoord voor jou.
Je code zou iets in de trant van dit moeten zijn:
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.
(Opmerking, voor degenen die de nieuwe
fetch
API, Angular of promises gebruiken heb ik'hieronder een ander antwoord toegevoegd)Wat u're geconfronteerd
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 alssuccess
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
[(Fiddle)][2]
De geretourneerde waarde van
a
isundefined
omdat heta=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.
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:
Dat zou "5" op het scherm moeten brengen. [(Fiddle)][4].
Mogelijke oplossingen
Er zijn in principe twee manieren om dit op te lossen:
1. Synchrone AJAX - Doe het niet!!!
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:
Als je het moet doen, kun je een vlag doorgeven: Hier staat hoe:
2. Herstructureer code
Laat je functie een callback accepteren. In de voorbeeldcode kan
foo
een callback accepteren. We'zullen onze code vertellen hoe te reageren wanneerfoo
voltooid is.Dus:
Wordt:
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:
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
[(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:
Zoals je kan zien:
Of als je om een of andere reden
bind()
de callback aan een class:Voorbeeld:
Of (de bovenstaande is beter anonieme functies zijn altijd een probleem):
Niets eenvoudiger. Nu zullen sommige mensen waarschijnlijk zeggen dat het'beter is om onreadystatechange of de zelfs de XMLHttpRequest variabele naam te gebruiken. Dat's verkeerd. Kijk eens naar XMLHttpRequest geavanceerde functies Het ondersteunt alle *moderne browsers. En ik kan het bevestigen aangezien ik'm deze aanpak gebruik sinds XMLHttpRequest 2 bestaat. Ik heb nooit enig type probleem gehad op alle browsers die ik gebruik. onreadystatechange is alleen nuttig als je de headers op state 2 wilt krijgen. Het gebruik van de
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:
Nogmaals ... het'is een zeer korte functie, maar het krijgt wel & post. Voorbeelden van gebruik:
Of geef een volledig formulier-element door (
document.getElementsByTagName('form')[0]
):Of stel een aantal aangepaste waarden in:
Zoals je kunt zien heb ik'geen sync geïmplementeerd ... het is een slechte zaak. Dat gezegd hebbende ... waarom doe je het niet op de gemakkelijke manier?
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
In het bovenstaande script heb je een error handler die statisch gedefinieerd is zodat het de functie niet in gevaar brengt. De error handler kan ook voor andere functies gebruikt worden. Maar om echt een foutmelding te krijgen is de enige manier om een verkeerde URL te schrijven in welk geval elke browser een foutmelding geeft. Error handlers zijn misschien nuttig als je custom headers instelt, de responseType instelt op blob array buffer of wat dan ook... Zelfs als je 'POSTAPAP' doorgeeft als de methode zal het geen fout geven. Zelfs als je 'fdggdgilfdghfldj' als formdata doorgeeft zal het geen fout geven. In het eerste geval zit de fout in de
displayAjax()
onderthis.statusText
alsMethod 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 dethis.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 functiedisplayAjax()
. 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.Nu kun je
Er is geen andere manier om dit op een niet-asynchrone manier te doen. (Ja, met setTimeout loop... maar serieus?) Een ander punt is... als je werkt met API's of gewoon je eigen list's files of wat dan ook gebruik je altijd verschillende functies voor elk request... Alleen als je een pagina hebt waar je altijd dezelfde XML/JSON of wat dan ook laadt heb je maar één functie nodig. In dat geval, pas een beetje de Ajax functie aan en vervang b door je speciale functie.
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.)