Ho una funzione foo
che fa una richiesta Ajax. Come posso restituire la risposta di foo
?
Ho provato a restituire il valore dal callback success
e ad assegnare la risposta ad una variabile locale all'interno della funzione e a restituirla, ma nessuno di questi modi restituisce effettivamente la risposta.
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`.
→ Per una spiegazione più generale del comportamento asincrono con diversi esempi, si veda https://stackoverflow.com/q/23667086/218196
→ Se hai già capito il problema, passa alle possibili soluzioni qui sotto.
Il problema.
La A in Ajax sta per asincrono . Ciò significa che l'invio della richiesta (o piuttosto la ricezione della risposta) è tolto dal normale flusso di esecuzione. Nel tuo esempio,
$.ajax
ritorna immediatamente e l'istruzione successiva,return result;
, viene eseguita prima ancora che la funzione che hai passato come callbacksuccess
sia stata chiamata. Ecco un'analogia che spero renda più chiara la differenza tra flusso sincrono e asincrono:Sincrono
Immaginate di fare una telefonata ad un amico e chiedergli di cercare qualcosa per voi. Anche se potrebbe volerci un po' di tempo, aspettate al telefono e fissate il vuoto, fino a quando il vostro amico vi darà la risposta di cui avete bisogno. Lo stesso accade quando fate una chiamata di funzione contenente codice "normale":
function findItem() {
var item;
while(item_not_found) {
// search
}
return item;
}
var item = findItem();
// Do something with item
doSomethingElse();
Anche se findItem
potrebbe impiegare molto tempo per essere eseguito, qualsiasi codice che viene dopo var item = findItem();
deve attendere fino a quando la funzione restituisce il risultato.
Chiamate di nuovo il vostro amico per lo stesso motivo. Ma questa volta gli dici che sei di fretta e che dovrebbe richiamarti sul tuo cellulare. Riattacchi, esci di casa e fai quello che avevi programmato di fare. Una volta che il tuo amico ti richiama, hai a che fare con le informazioni che ti ha dato. Questo è esattamente quello che succede quando fai una richiesta Ajax.
findItem(function(item) {
// Do something with item
});
doSomethingElse();
**Mentre alcune operazioni asincrone forniscono delle controparti sincrone (così fa Ajax), è generalmente sconsigliato usarle, specialmente in un contesto di browser. Perché è un male vi chiederete? JavaScript viene eseguito nel thread UI del browser e qualsiasi processo di lunga durata bloccherà l'UI, rendendola non reattiva. Inoltre, c'è un limite superiore al tempo di esecuzione di JavaScript e il browser chiederà all'utente se continuare l'esecuzione o meno. Tutto questo è davvero una cattiva esperienza utente. L'utente non sarà in grado di capire se tutto funziona bene o no. Inoltre, l'effetto sarà peggiore per gli utenti con una connessione lenta. Di seguito vedremo tre diverse soluzioni che sono tutte costruite l'una sull'altra:
async/await
(ES2017+, disponibile nei vecchi browser se si usa un transpiler o un rigeneratore)then()
(ES2015+, disponibile nei vecchi browser se si usa una delle molte librerie di promesse)
Tutti e tre sono disponibili nei browser attuali e in node 7+.async/await
La versione di ECMAScript rilasciata nel 2017 ha introdotto un supporto a livello di sintassi per le funzioni asincrone. Con l'aiuto di async
e await
, si può scrivere asincrono in uno "stile sincrono". Il codice è ancora asincrono, ma è più facile da leggere/comprendere.
async/await
si basa sulle promesse: una funzione async
restituisce sempre una promessa. await
"unwraps" una promessa e o restituisce il valore con cui la promessa è stata risolta o lancia un errore se la promessa è stata rifiutata.
Importante: Si può usare solo await
all'interno di una funzione async
. In questo momento, il livello superiore await
non è ancora supportato, quindi potresti dover fare un async IIFE (Immediately Invoked Function Expression) per avviare un contesto async
.
Potete leggere di più su async
e await
su MDN.
Ecco un esempio che si basa sul ritardo di cui sopra:
// 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
. Puoi anche supportare ambienti più vecchi trasformando il tuo codice in ES5 con l'aiuto di regenerator (o strumenti che usano regenerator, come Babel).Una callback è semplicemente una funzione passata ad un'altra funzione. Quest'altra funzione può chiamare la funzione passata ogni volta che è pronta. Nel contesto di un processo asincrono, la callback sarà chiamata ogni volta che il processo asincrono è finito. Di solito, il risultato viene passato alla callback.
Nell'esempio della domanda, si può fare in modo che foo
accetti una callback e la usi come callback di successo
. Quindi questo
var result = foo();
// Code that depends on 'result'
diventa
foo(function(result) {
// Code that depends on 'result'
});
Qui abbiamo definito la funzione "inline" ma potete passare qualsiasi riferimento di funzione:
function myCallback(result) {
// Code that depends on 'result'
}
foo(myCallback);
Lo stesso foo
è definito come segue:
function foo(callback) {
$.ajax({
// ...
success: callback
});
}
callback
farà riferimento alla funzione che passiamo a foo
quando la chiamiamo e la passiamo semplicemente a success
. Cioè, una volta che la richiesta Ajax ha successo, $.ajax
chiamerà callback
e passerà la risposta alla callback (a cui si può fare riferimento con result
, dato che è così che abbiamo definito la callback).
Puoi anche processare la risposta prima di passarla alla callback:
function foo(callback) {
$.ajax({
// ...
success: function(response) {
// For example, filter the response
callback(filtered_response);
}
});
}
La Promise API è una nuova caratteristica di ECMAScript 6 (ES2015), ma ha già un buon supporto del browser. Ci sono anche molte librerie che implementano l'API standard Promises e forniscono metodi aggiuntivi per facilitare l'uso e la composizione di funzioni asincrone (ad esempio bluebird). Le promesse sono contenitori di valori futuri. Quando la promessa riceve il valore (è risolto) o quando viene cancellata (rifiutata), notifica tutti i suoi "ascoltatori" che vogliono accedere a questo valore. Il vantaggio rispetto ai semplici callback è che vi permettono di disaccoppiare il vostro codice e sono più facili da comporre. Ecco un semplice esempio di utilizzo di una promessa:
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).
});
Applicato alla nostra chiamata Ajax potremmo usare promesse come questa:
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
});
Descrivere tutti i vantaggi che le promesse offrono va oltre lo scopo di questa risposta, ma se scrivete nuovo codice, dovreste seriamente considerarle. Forniscono una grande astrazione e separazione del vostro codice. Maggiori informazioni sulle promesse: Rocce HTML5 - Promesse JavaScript
Gli oggetti differiti Deferred objects sono l'implementazione personalizzata di jQuery delle promesse (prima che l'API Promise fosse standardizzata). Si comportano quasi come le promesse ma espongono un'API leggermente diversa. Ogni metodo Ajax di jQuery restituisce già un "oggetto differito" (in realtà una promessa di un oggetto differito) che si può semplicemente restituire dalla propria funzione:
function ajax() {
return $.ajax(...);
}
ajax().done(function(result) {
// Code depending on result
}).fail(function() {
// An error occurred
});
Tenete a mente che le promesse e gli oggetti differiti sono solo contenitori di un valore futuro, non sono il valore stesso. Per esempio, supponiamo di avere il seguente:
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
}
Questo codice fraintende i problemi di asincronia di cui sopra. In particolare, $.ajax()
non congela il codice mentre controlla la '/password' pagina sul tuo server - invia una richiesta al server e mentre aspetta, restituisce immediatamente un oggetto jQuery Ajax Deferred, non la risposta dal server. Ciò significa che l'istruzione if
otterrà sempre questo oggetto Deferred, lo tratterà come true
, e procederà come se l'utente avesse fatto il login. Non va bene.
Ma la soluzione è facile:
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
});
Come ho detto, alcune(!) operazioni asincrone hanno controparti sincrone. Non ne raccomando l'uso, ma per completezza, ecco come si dovrebbe eseguire una chiamata sincrona:
Se usi direttamente un oggetto XMLHTTPRequest
, passa false
come terzo argomento a .open
.
Se usi jQuery, puoi impostare l'opzione async
a false
. Nota che questa opzione è deprecata da jQuery 1.8.
È quindi possibile utilizzare ancora un callback success
o accedere alla proprietà responseText
dell'oggetto jqXHR:
function foo() {
var jqXHR = $.ajax({
//...
async: false
});
return jqXHR.responseText;
}
Se usi qualsiasi altro metodo jQuery Ajax, come $.get
, $.getJSON
, etc., devi cambiarlo in $.ajax
(poiché puoi passare solo parametri di configurazione a $.ajax
).
**Non è possibile fare una richiesta sincrona JSONP. JSONP per sua natura è sempre asincrono (una ragione in più per non considerare questa opzione).
Il tuo codice dovrebbe essere qualcosa di simile a questo:
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 ha fatto un buon lavoro scrivendo una risposta per le persone che usano jQuery per AJAX, ho deciso di fornire un'alternativa per le persone che non lo sono.
(Nota, per chi usa la nuova API fetch
, Angular o promises ho aggiunto un'altra risposta qui sotto)
Questo è un breve riassunto di "Spiegazione del problema" dall'altra risposta, se non sei sicuro dopo aver letto questo, leggi quello.
La A in AJAX sta per asincrono. Ciò significa che l'invio della richiesta (o piuttosto la ricezione della risposta) è tolto dal normale flusso di esecuzione. Nel tuo esempio, .send
ritorna immediatamente e l'istruzione successiva, return result;
, viene eseguita prima ancora che la funzione che hai passato come callback success
sia stata chiamata.
Questo significa che quando stai tornando, l'ascoltatore che hai definito non è ancora stato eseguito, il che significa che il valore che stai restituendo non è stato definito.
Ecco una semplice analogia
function getFive(){
var a;
setTimeout(function(){
a=5;
},10);
return a;
}
[(Fiddle)][2]
Il valore di a
restituito è undefined
poiché la parte a=5
non è ancora stata eseguita. AJAX si comporta in questo modo, stai restituendo il valore prima che il server abbia avuto la possibilità di dire al tuo browser qual è quel valore.
Una possibile soluzione a questo problema è quella di codificare re-attivamente, dicendo al tuo programma cosa fare quando il calcolo è stato completato.
function onComplete(a){ // When the code completes, do this
alert(a);
}
function getFive(whenDone){
var a;
setTimeout(function(){
a=5;
whenDone(a);
},10);
}
Questo si chiama CPS. Fondamentalmente, stiamo passando a getFive
un'azione da eseguire al suo completamento, stiamo dicendo al nostro codice come reagire quando un evento si completa (come la nostra chiamata AJAX, o in questo caso il timeout).
L'uso sarebbe:
getFive(onComplete);
Che dovrebbe avvisare "5" sullo schermo. [(Fiddle)][4].
Ci sono fondamentalmente due modi per risolvere questo problema:
Per quanto riguarda AJAX sincrono, non farlo! La risposta di Felix solleva alcuni argomenti convincenti sul perché è una cattiva idea. Per riassumere, bloccherà il browser dell'utente fino a quando il server non restituirà la risposta e creerà una pessima esperienza utente. Ecco un altro breve riassunto tratto da MDN sul perché:
XMLHttpRequest supporta sia le comunicazioni sincrone che quelle asincrone. In generale, tuttavia, le richieste asincrone dovrebbero essere preferite a quelle sincrone per ragioni di prestazioni.
In breve, le richieste sincrone bloccano l'esecuzione del codice... ...questo può causare seri problemi...
Se dovete farlo, potete passare un flag: Ecco come:
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);
}
Lasciate che la vostra funzione accetti una callback. Nel codice di esempio foo
può essere fatto per accettare una callback. Diremo al nostro codice come reagire quando foo
completa.
Quindi:
var result = foo();
// code that depends on `result` goes here
Diventa:
foo(function(result) {
// code that depends on `result`
});
Qui abbiamo passato una funzione anonima, ma potremmo altrettanto facilmente passare un riferimento a una funzione esistente, facendola apparire come:
function myHandler(result) {
// code that depends on `result`
}
foo(myHandler);
Per maggiori dettagli su come questo tipo di design di callback è fatto, controlla la risposta di Felix.
Ora, definiamo pippo stesso per agire di conseguenza
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]
Ora abbiamo fatto in modo che la nostra funzione pippo accetti un'azione da eseguire quando l'AJAX si completa con successo, possiamo estendere ulteriormente questo controllando se lo stato della risposta non è 200 e agendo di conseguenza (creare un gestore di fallimento e simili). Risolvendo efficacemente il nostro problema.
Se hai ancora difficoltà a capire questo leggi la guida AJAX getting started su MDN.
XMLHttpRequest 2 (prima di tutto leggere le risposte di Benjamin Gruenbaum & Felix Kling) Se non usi jQuery e vuoi un bel XMLHttpRequest 2 corto che funzioni sui browser moderni e anche su quelli mobili ti consiglio di usarlo in questo modo:
function ajax(a, b, c){ // URL, callback, just a placeholder
c = new XMLHttpRequest;
c.open('GET', a);
c.onload = b;
c.send()
}
Come potete vedere:
this.response
O se per qualche motivo si bind()
il callback a una classe:
e.target.response
Esempio:
function callback(e){
console.log(this.response);
}
ajax('URL', callback);
Oppure (la precedente è meglio le funzioni anonime sono sempre un problema):
ajax('URL', function(e){console.log(this.response)});
XMLHttpRequest
è un altro grande errore in quanto è necessario eseguire il callback all'interno delle chiusure onload/oreadystatechange altrimenti lo si perde.Ora se volete qualcosa di più complesso usando post e FormData potete facilmente estendere questa funzione:
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)
}
Di nuovo ... è una funzione molto breve, ma ottiene & post. Esempi di utilizzo:
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
O passare un elemento completo del modulo (document.getElementsByTagName('form')[0]
):
var fd = new FormData(form);
x(url, callback, 'post', fd);
O imposta alcuni valori personalizzati:
var fd = new FormData();
fd.append('key', 'val')
x(url, callback, 'post', fd);
Come detto nel commento l'uso di error && synchronous fa rompere completamente il punto della risposta. Qual è un bel modo breve per usare Ajax nel modo corretto? Gestore degli errori
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()
sotto this.statusText
come Method not Allowed
.
Nel secondo caso, funziona semplicemente. Devi controllare sul lato server se hai passato i dati del post giusti.
cross-domain not allowed lancia automaticamente un errore.
Nella risposta di errore, non ci sono codici di errore.
C'è solo il this.type
che è impostato su error.
Perché aggiungere un gestore di errori se non si ha alcun controllo sugli errori?
La maggior parte degli errori sono restituiti all'interno di questo nella funzione di callback displayAjax()
.
Quindi: Non c'è bisogno di un controllo degli errori se sei in grado di copiare e incollare l'URL correttamente. ;)
PS: Come primo test ho scritto x('x', displayAjax)..., e ha ottenuto totalmente una risposta...?? Allora ho controllato la cartella dove si trova l'HTML, e c'era un file chiamato 'x.xml'. Quindi anche se dimentichi l'estensione del tuo file XMLHttpRequest 2 lo troverà. LOL'dLeggi un file in modo sincrono
**Non fatelo.
Se volete bloccare il browser per un po', caricate un bel file .txt
sincrono.
function omg(a, c){ // URL
c = new XMLHttpRequest;
c.open('GET', a, true);
c.send();
return c; // Or c.response
}
Ora puoi fare
var res = omg('thisIsGonnaBlockThePage.txt');
Le funzioni di cui sopra sono per un uso di base. Se vuoi ESTENDERE la funzione... Sì, è possibile. Sto usando un sacco di API e una delle prime funzioni che integro in ogni pagina HTML è la prima funzione Ajax in questa risposta, con solo GET... Ma puoi fare un sacco di cose con XMLHttpRequest 2: Ho fatto un download manager (usando intervalli su entrambi i lati con resume, filereader, filesystem), vari convertitori di ridimensionamento di immagini usando canvas, popolare database web SQL con base64images e molto altro... Ma in questi casi dovresti creare una funzione solo per quello scopo... a volte hai bisogno di un blob, di array di buffer, puoi impostare intestazioni, sovrascrivere mimetype e c'è molto di più... Ma la domanda qui è come restituire una risposta Ajax... (Ho aggiunto un modo semplice).