Jeg har en funktion foo
, som foretager en Ajax-forespørgsel. Hvordan kan jeg returnere svaret fra foo
?
Jeg har prøvet at returnere værdien fra success
callback samt at tildele svaret til en lokal variabel inde i funktionen og returnere denne, men ingen af disse måder returnerer faktisk svaret.
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`.
→ For en mere generel forklaring på asynkron adfærd med forskellige eksempler, se https://stackoverflow.com/q/23667086/218196
→ Hvis du allerede forstår problemet, kan du springe til de mulige løsninger nedenfor.
Problemet
A i Ajax står for asynkron . Det betyder, at afsendelse af anmodningen (eller rettere modtagelse af svaret) er taget ud af det normale udførelsesflow. I dit eksempel vender
$.ajax
tilbage med det samme, og den næste erklæring,return result;
, udføres før den funktion, du har givet somsuccess
callback, overhovedet er blevet kaldt. Her er en analogi, som forhåbentlig gør forskellen mellem synkront og asynkront flow tydeligere:Synkront
Forestil dig, at du ringer til en ven og beder ham om at slå noget op for dig. Selv om det kan tage et stykke tid, venter du i telefonen og stirrer ud i luften, indtil din ven giver dig det svar, du havde brug for. Det samme sker, når du foretager et funktionsopkald, der indeholder "normal" kode:
function findItem() {
var item;
while(item_not_found) {
// search
}
return item;
}
var item = findItem();
// Do something with item
doSomethingElse();
Selvom findItem
måske tager lang tid at udføre, skal enhver kode, der kommer efter var item = findItem();
, vente indtil funktionen returnerer resultatet.
Du kalder din ven igen af samme grund. Men denne gang fortæller du ham, at du har travlt, og at han skal ringe dig tilbage på din mobiltelefon. Du lægger på, forlader huset og gør det, du havde planlagt at gøre. Når din ven ringer dig tilbage, beskæftiger du dig med de oplysninger, som han gav dig. Det er præcis det samme, der sker, når du foretager en Ajax-forespørgsel.
findItem(function(item) {
// Do something with item
});
doSomethingElse();
Opnå JavaScript's asynkrone natur! Selv om visse asynkrone operationer har synkrone modstykker (det samme gælder "Ajax"), frarådes det generelt at bruge dem, især i en browserkontekst. Hvorfor er det dårligt, spørger du? JavaScript kører i browserens UI-tråd, og enhver proces, der kører længe, vil låse UI'et, hvilket gør det uopmærksomt. Desuden er der en øvre grænse for JavaScript's eksekveringstid, og browseren vil spørge brugeren, om han/hun vil fortsætte eksekveringen eller ej. Alt dette er en virkelig dårlig brugeroplevelse. Brugeren vil ikke kunne se, om alt fungerer fint eller ej. Desuden vil effekten være værre for brugere med en langsom forbindelse. I det følgende vil vi se på tre forskellige løsninger, der alle bygger oven på hinanden:
async/await
(ES2017+, tilgængelig i ældre browsere, hvis du bruger en transpiler eller regenerator)then()
(ES2015+, tilgængelig i ældre browsere, hvis du bruger et af de mange promise-biblioteker)
Alle tre er tilgængelige i aktuelle browsere og node 7+.async/await
Den ECMAScript-version, der blev udgivet i 2017, introducerede understøttelse på syntaksniveau for asynkrone funktioner. Ved hjælp af async
og await
kan du skrive asynkrone i en "synkron stil". Koden er stadig asynkron, men den er nemmere at læse/forstå.
async/await
bygger ovenpå promises: en async
-funktion returnerer altid et løfte. await
"unwraps" et løfte og enten resulterer i den værdi løftet blev løst med eller kaster en fejl, hvis løftet blev afvist.
Vigtigt: Du kan kun bruge await
inden for en async
-funktion. Lige nu er await
på øverste niveau endnu ikke understøttet, så du skal muligvis lave en async IIFE (Immediately Invoked Function Expression)) for at starte en async
-kontekst.
Du kan læse mere om async
og await
på MDN.
Her er et eksempel, der bygger på ovenstående delay:
// 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
. Du kan også understøtte ældre miljøer ved at omdanne din kode til ES5 ved hjælp af regenerator (eller værktøjer, der bruger regenerator, som f.eks. Babel).En callback er simpelthen en funktion, der overgives til en anden funktion. Den anden funktion kan kalde den overdragne funktion, når den er klar. I forbindelse med en asynkron proces vil callbacken blive kaldt, når den asynkrone proces er færdig. Normalt overføres resultatet til callbacken.
I eksemplet i spørgsmålet kan du få foo
til at acceptere en callback og bruge den som success
callback. Så dette
var result = foo();
// Code that depends on 'result'
bliver til
foo(function(result) {
// Code that depends on 'result'
});
Her har vi defineret funktionen "inline" men du kan overdrage en hvilken som helst funktionsreference:
function myCallback(result) {
// Code that depends on 'result'
}
foo(myCallback);
foo
er defineret som følger:
function foo(callback) {
$.ajax({
// ...
success: callback
});
}
callback
vil henvise til den funktion, som vi overdrager til foo
, når vi kalder den, og vi overdrager den blot til success
. Dvs. når Ajax-forespørgslen er lykkedes, vil $.ajax
kalde callback
og videregive svaret til callbacken (som der kan henvises til med result
, da det er sådan vi har defineret callbacken).
Du kan også behandle svaret, før du sender det til callbacken:
function foo(callback) {
$.ajax({
// ...
success: function(response) {
// For example, filter the response
callback(filtered_response);
}
});
}
API'et Promise API er en ny funktion i ECMAScript 6 (ES2015), men det har allerede god browserunderstøttelse. Der findes også mange biblioteker, som implementerer standard-Promises API'et og leverer yderligere metoder til at lette brugen og sammensætningen af asynkrone funktioner (f.eks. bluebird). Promises er containere for fremtidige værdier. Når løftet modtager værdien (den er resolved) eller når den annulleres (rejected), meddeler det alle sine "lyttere" der ønsker at få adgang til denne værdi. Fordelen i forhold til almindelige callbacks er, at de giver dig mulighed for at afkoble din kode, og de er lettere at sammensætte. Her er et simpelt eksempel på brug af et løfte:
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).
});
Anvendt på vores Ajax-kald kunne vi bruge promises som følger:
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
});
At beskrive alle de fordele, som promise tilbyder, ligger uden for rammerne af dette svar, men hvis du skriver ny kode, bør du seriøst overveje dem. De giver en fantastisk abstraktion og adskillelse af din kode. Flere oplysninger om promises: HTML5 rocks - JavaScript Promises
Deferred objects er jQuery's brugerdefinerede implementering af promises (før Promise API'et blev standardiseret). De opfører sig næsten som promises, men har en lidt anderledes API. Hver Ajax-metode i jQuery returnerer allerede et "deferred object" (faktisk et løfte om et deferred object), som du bare kan returnere fra din funktion:
function ajax() {
return $.ajax(...);
}
ajax().done(function(result) {
// Code depending on result
}).fail(function() {
// An error occurred
});
Husk på, at løfter og deferred objects blot er containere for en fremtidig værdi, de er ikke selve værdien. For eksempel, hvis du havde følgende:
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
}
Denne kode misforstår de ovennævnte asynkronitetsproblemer. Specifikt fryser $.ajax()
ikke koden, mens den kontrollerer '/password' siden på din server - den sender en anmodning til serveren og mens den venter, returnerer den straks et jQuery Ajax Deferred objekt, ikke svaret fra serveren. Det betyder, at if
-erklæringen altid vil få dette Deferred-objekt, behandle det som true
og fortsætte som om brugeren er logget ind. Det er ikke godt.
Men løsningen er nem:
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
});
Som jeg nævnte, har nogle(!) asynkrone operationer synkrone modstykker. Jeg anbefaler ikke deres brug, men for fuldstændighedens skyld er her, hvordan du ville udføre et synkront kald:
Hvis du direkte bruger et XMLHTTPRequest
-objekt, skal du sende false
som tredje argument til .open
.
Hvis du bruger jQuery, kan du indstille async
-indstillingen til false
. Bemærk, at denne indstilling er deprecated siden jQuery 1.8.
Du kan derefter enten stadig bruge en success
callback eller få adgang til egenskaben responseText
i jqXHR-objektet:
function foo() {
var jqXHR = $.ajax({
//...
async: false
});
return jqXHR.responseText;
}
Hvis du bruger en anden jQuery Ajax-metode, såsom $.get
, $.getJSON
osv., skal du ændre den til $.ajax
(da du kun kan videregive konfigurationsparametre til $.ajax
).
Hovedpunkter! Det er ikke muligt at lave en synkron JSONP-anmodning. JSONP er i sagens natur altid asynkron (endnu en grund til ikke engang at overveje denne mulighed).
Din kode bør være noget i retning af dette:
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 gjorde et godt stykke arbejde med at skrive et svar til folk der bruger jQuery til AJAX, jeg har besluttet at give et alternativ til folk der ikke gør det.
([Bemærk, for dem der bruger den nye fetch
API, Angular eller løfter har jeg tilføjet et andet svar nedenfor] (https://stackoverflow.com/a/30180679/1348195))
Dette er et kort resumé af "Forklaring af problemet" fra det andet svar, hvis du'er ikke sikker efter at have læst dette, så læs det.
A i AJAX står for asynkron. Det betyder, at afsendelse af anmodningen (eller rettere modtagelse af svaret) er taget ud af det normale udførelsesflow. I dit eksempel vender .send
tilbage med det samme, og den næste erklæring, return result;
, udføres før den funktion, du har givet som success
-callback, overhovedet er blevet kaldt.
Det betyder, at når du returnerer, er den lytter, du har defineret, ikke blevet eksekveret endnu, hvilket betyder, at den værdi, du returnerer, ikke er blevet defineret.
Her er en simpel analogi
function getFive(){
var a;
setTimeout(function(){
a=5;
},10);
return a;
}
[(Fiddle)][2]
Den returnerede værdi af a
er undefined
, da a=5
-delen ikke er blevet udført endnu. AJAX fungerer på denne måde, du returnerer værdien før serveren har haft mulighed for at fortælle din browser hvad værdien er.
En mulig løsning på dette problem er at kode re-aktivt , og fortælle dit program, hvad det skal gøre, når beregningen er afsluttet.
function onComplete(a){ // When the code completes, do this
alert(a);
}
function getFive(whenDone){
var a;
setTimeout(function(){
a=5;
whenDone(a);
},10);
}
Dette kaldes CPS. Dybest set giver vi getFive
en handling, som skal udføres, når den er færdig, vi fortæller vores kode, hvordan den skal reagere, når en begivenhed er afsluttet (som vores AJAX-opkald eller i dette tilfælde timeout).
Anvendelse ville være:
getFive(onComplete);
Hvilket skulle advare "5" på skærmen. [(Fiddle)][4].
Der er grundlæggende to måder, hvordan man kan løse dette:
Hvad angår synkron AJAX, gør det ikke! Felix's svar giver nogle overbevisende argumenter for, hvorfor det er en dårlig idé. For at opsummere det, vil det fryse brugerens browser, indtil serveren returnerer svaret, og skabe en meget dårlig brugeroplevelse. Her er et andet kort resumé taget fra MDN om hvorfor:
XMLHttpRequest understøtter både synkron og asynkron kommunikation. Generelt bør asynkrone anmodninger dog foretrækkes frem for synkrone anmodninger af hensyn til ydelsen.
Kort sagt blokerer synkrone anmodninger udførelsen af kode... ... dette kan give alvorlige problemer...
Hvis du skal gøre det, kan du give et flag: Her er hvordan:
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);
}
Lad din funktion acceptere en callback. I eksempelkoden kan foo
gøres til at acceptere en callback. Vi skal fortælle vores kode, hvordan den skal reagere, når foo
er færdig.
Så:
var result = foo();
// code that depends on `result` goes here
Bliver:
foo(function(result) {
// code that depends on `result`
});
Her har vi overgivet en anonym funktion, men vi kunne lige så let overgive en reference til en eksisterende funktion, så det ser ud som:
function myHandler(result) {
// code that depends on `result`
}
foo(myHandler);
For flere detaljer om, hvordan denne form for callback-design udføres, se Felix's svar.
Lad os nu definere foo selv til at handle i overensstemmelse hermed
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]
Vi har nu gjort vores foo-funktion til at acceptere en handling, der skal køres, når AJAX afsluttes med succes, vi kan udvide dette yderligere ved at kontrollere, om svarstatus ikke er 200 og handle i overensstemmelse hermed (oprette en fail-handler og lignende). Effektivt løser vores problem.
Hvis du stadig har svært ved at forstå dette læs AJAX getting started guide på MDN.
XMLHttpRequest 2 (læs først og fremmest svarene fra Benjamin Gruenbaum & Felix Kling) Hvis du ikke bruger jQuery og ønsker en dejlig kort XMLHttpRequest 2, som virker på de moderne browsere og også på de mobile browsere, foreslår jeg at bruge den på denne måde:
function ajax(a, b, c){ // URL, callback, just a placeholder
c = new XMLHttpRequest;
c.open('GET', a);
c.onload = b;
c.send()
}
Som du kan se:
this.response
Eller hvis du af en eller anden grund bind()
callback'et til en klasse:
e.target.response
Eksempel:
function callback(e){
console.log(this.response);
}
ajax('URL', callback);
Eller (den ovenstående er bedre anonyme funktioner er altid et problem):
ajax('URL', function(e){console.log(this.response)});
XMLHttpRequest
-variabelnavnet er en anden stor fejl, da du er nødt til at udføre callback inden for onload/oreadystatechange closures, ellers er du tabt det.Hvis du nu vil have noget mere komplekst ved hjælp af post og FormData kan du nemt udvide denne funktion:
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)
}
Igen ... det er en meget kort funktion, men den får & post. Eksempler på brug:
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
Eller videregiv et fuldt formularelement (document.getElementsByTagName('form')[0]
):
var fd = new FormData(form);
x(url, callback, 'post', fd);
Eller indstil nogle brugerdefinerede værdier:
var fd = new FormData();
fd.append('key', 'val')
x(url, callback, 'post', fd);
Som nævnt i kommentaren bryder brugen af fejl && synkronous fuldstændig pointen med svaret. Hvilken er en dejlig kort måde at bruge Ajax på den rigtige måde? 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()
under this.statusText
som Method not Allowed
.
I det andet tilfælde virker det simpelthen. Du skal kontrollere på serversiden, om du har overgivet de rigtige postdata.
cross-domain not allowed kaster automatisk fejl.
I fejlsvaret er der ingen fejlkoder.
Der er kun this.type
, som er sat til error.
Hvorfor tilføje en fejlhåndtering, hvis du slet ikke har kontrol over fejl?
De fleste af fejlene returneres inde i denne i callback-funktionen displayAjax()
.
Så: Så: Ingen grund til fejlkontrol, hvis du kan kopiere og indsætte URL'en korrekt ;)
PS: Som den første test skrev jeg x('x', displayAjax)..., og det fik helt sikkert et svar...????? Så jeg tjekkede den mappe hvor HTML'en ligger, og der var en fil der hed 'x.xml'. Så selv hvis du glemmer filtypenavnet på din fil, vil XMLHttpRequest 2 finde den. Jeg LOL'dLæs en fil synkront
Don't do that.
Hvis du vil blokere browseren i et stykke tid, skal du indlæse en dejlig stor .txt
-fil synkront.
function omg(a, c){ // URL
c = new XMLHttpRequest;
c.open('GET', a, true);
c.send();
return c; // Or c.response
}
Nu kan du gøre
var res = omg('thisIsGonnaBlockThePage.txt');
Ovenstående funktioner er til grundlæggende brug. Hvis du ønsker at UDVIDERE funktionen ... Ja, det kan du godt. I'm bruger mange API'er og en af de første funktioner jeg integrerer i hver HTML-side er den første Ajax-funktion i dette svar, med kun GET... Men du kan gøre en masse ting med XMLHttpRequest 2: Jeg har lavet en download manager (ved hjælp af intervaller på begge sider med resume, filereader, filsystem), forskellige billedformatkonverterere ved hjælp af lærred, udfylde web SQL-databaser med base64images og meget mere... Men i disse tilfælde bør du oprette en funktion kun til det formål ... nogle gange har du brug for en blob, arraybuffere, du kan indstille headere, overskrive mimetype og der er meget mere ... Men spørgsmålet her er hvordan man returnerer et Ajax-svar... (Jeg har tilføjet en nem måde.)