→ Per una spiegazione più generale del comportamento asincrono con diversi esempi, vedere Perché la mia variabile non viene modificata dopo averla modificata all'interno di una funzione? - Riferimento asincrono al codice
→ Se hai già capito il problema, passa alle possibili soluzioni di seguito.
Il problema
La A in Ajax sta per asincrono . Ciò significa che l'invio della richiesta (o piuttosto la ricezione della risposta) viene escluso dal normale flusso di esecuzione. Nel tuo esempio, $.ajax
restituisce immediatamente e l'istruzione successiva return result;
, viene eseguita prima ancora che la funzione che hai passato come success
callback fosse chiamata.
Ecco un'analogia che, si spera, rende più chiara la differenza tra flusso sincrono e asincrono:
Sincrono
Immagina di fare una telefonata a un amico e chiedergli di cercare qualcosa per te. Anche se potrebbe volerci un po ', aspetti al telefono e guardi nello spazio, finché il tuo amico non ti dà la risposta di cui hai bisogno.
Lo stesso accade quando si effettua una chiamata di funzione contenente un 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 richiedere molto tempo per l'esecuzione, qualsiasi codice successivo var item = findItem();
deve attendere fino a quando la funzione non restituisce il risultato.
asincrono
Chiami di nuovo il tuo amico per lo stesso motivo. Ma questa volta gli dici che hai fretta e dovrebbe richiamarti sul tuo cellulare. Riattacchi, esci di casa e fai qualunque cosa tu abbia pianificato di fare. Una volta che il tuo amico ti ha richiamato, hai a che fare con le informazioni che ti ha dato.
Questo è esattamente ciò che accade quando si effettua una richiesta Ajax.
findItem(function(item) {
// Do something with item
});
doSomethingElse();
Invece di attendere la risposta, l'esecuzione continua immediatamente e viene eseguita l'istruzione dopo la chiamata Ajax. Per ottenere la risposta alla fine, si fornisce una funzione da chiamare una volta ricevuta la risposta, un callback (notare qualcosa? Callback ?). Qualsiasi istruzione proveniente dopo quella chiamata viene eseguita prima che venga richiamata la richiamata.
Soluzione (s)
Abbraccia la natura asincrona di JavaScript! Mentre alcune operazioni asincrone forniscono controparti sincrone (così come "Ajax"), è generalmente sconsigliato usarle, specialmente in un contesto di browser.
Perché è male chiedi?
JavaScript viene eseguito nel thread dell'interfaccia utente del browser e qualsiasi processo di lunga durata bloccherà l'interfaccia utente, rendendola non rispondente. Inoltre, esiste un limite massimo per i tempi di esecuzione di JavaScript e il browser chiederà all'utente se continuare o meno l'esecuzione.
Tutto ciò è un'esperienza utente davvero negativa. L'utente non sarà in grado di dire 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 si stanno costruendo una sopra l'altra:
- Promesse con
async/await
(ES2017 +, disponibile nei browser meno recenti se si utilizza un transpiler o un rigeneratore)
- Callback (popolari nel nodo)
- Promesse con
then()
(ES2015 +, disponibile nei browser meno recenti se si utilizza una delle tante librerie promessa)
Tutti e tre sono disponibili nei browser attuali e nel nodo 7+.
ES2017 +: promesse con async/await
La versione ECMAScript rilasciata nel 2017 ha introdotto il supporto a livello di sintassi per le funzioni asincrone. Con l'aiuto di async
e await
, puoi scrivere asincrono in uno "stile sincrono". Il codice è ancora asincrono, ma è più facile da leggere / comprendere.
async/await
si basa sulle promesse: una async
funzione restituisce sempre una promessa. await
"scartare" una promessa e determinare il valore con cui la promessa è stata risolta o genera un errore se la promessa è stata respinta.
Importante: è possibile utilizzare solo await
all'interno di una async
funzione. Al momento, il livello superiore await
non è ancora supportato, quindi potrebbe essere necessario creare un IIFE asincrono ( espressione di funzione richiamata immediatamente ) per avviare un async
contesto.
Puoi leggere di più su async
e await
su MDN.
Ecco un esempio che si basa sul ritardo sopra riportato:
// 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);
})();
Supporto delle versioni correnti di browser e nodiasync/await
. Puoi anche supportare ambienti più vecchi trasformando il tuo codice in ES5 con l'aiuto di rigeneratore (o strumenti che usano il rigeneratore, come Babel ).
Consenti alle funzioni di accettare i callback
Un callback è semplicemente una funzione passata a un'altra funzione. L'altra funzione può chiamare la funzione passata ogni volta che è pronta. Nel contesto di un processo asincrono, il callback verrà chiamato ogni volta che viene eseguito il processo asincrono. Di solito, il risultato viene passato al callback.
Nell'esempio della domanda, è possibile foo
accettare un callback e utilizzarlo come success
callback. Così 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 puoi passare qualsiasi riferimento di funzione:
function myCallback(result) {
// Code that depends on 'result'
}
foo(myCallback);
foo
stesso è definito come segue:
function foo(callback) {
$.ajax({
// ...
success: callback
});
}
callback
farà riferimento alla funzione a cui passiamo foo
quando la chiamiamo e la passiamo semplicemente a success
. Cioè una volta che la richiesta Ajax ha esito positivo,$.ajax
chiamerà callback
e passerà la risposta al callback (a cui si può fare riferimento con result
, poiché è così che abbiamo definito il callback).
È inoltre possibile elaborare la risposta prima di passarla al callback:
function foo(callback) {
$.ajax({
// ...
success: function(response) {
// For example, filter the response
callback(filtered_response);
}
});
}
È più facile scrivere codice usando i callback di quanto possa sembrare. Dopotutto, JavaScript nel browser è fortemente guidato dagli eventi (eventi DOM). Ricevere la risposta Ajax non è altro che un evento.
Potrebbero sorgere difficoltà quando si deve lavorare con codice di terze parti, ma la maggior parte dei problemi può essere risolta semplicemente riflettendo sul flusso dell'applicazione.
ES2015 +: promesse con then ()
L' API Promise è una nuova funzionalità di ECMAScript 6 (ES2015), ma ha già un buon supporto per il browser . Esistono anche molte librerie che implementano l'API Promises standard e forniscono metodi aggiuntivi per facilitare l'uso e la composizione di funzioni asincrone (ad es. bluebird ).
Le promesse sono contenitori per valori futuri . Quando la promessa riceve il valore (viene risolto ) o quando viene annullata ( rifiutata ), avvisa tutti i suoi "ascoltatori" che desiderano accedere a questo valore.
Il vantaggio rispetto ai semplici callback è che ti permettono di disaccoppiare il tuo 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).
});
Applicati 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 offerti dall'offerta promessa va oltre lo scopo di questa risposta, ma se scrivi un nuovo codice, dovresti prenderli seriamente in considerazione. Forniscono un'ottima astrazione e separazione del codice.
Maggiori informazioni sulle promesse: HTML5 rock - Promesse JavaScript
Nota a margine: gli oggetti differiti di jQuery
Gli oggetti differiti sono l'implementazione personalizzata delle promesse di jQuery (prima che l'API Promise fosse standardizzata). Si comportano quasi come 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 puoi semplicemente restituire dalla tua funzione:
function ajax() {
return $.ajax(...);
}
ajax().done(function(result) {
// Code depending on result
}).fail(function() {
// An error occurred
});
Nota a margine: gotchas promettenti
Tieni presente che le promesse e gli oggetti differiti sono solo contenitori per un valore futuro, non sono il valore stesso. Ad esempio, supponiamo di avere quanto segue:
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 sopra indicati. In particolare, $.ajax()
non blocca il codice mentre controlla la pagina '/ password' sul server - invia una richiesta al server e mentre attende, restituisce immediatamente un oggetto jQuery Ajax Deferred, non la risposta dal server. Ciò significa che l' if
istruzione otterrà sempre questo oggetto differito, lo considererà come true
e procederà come se l'utente avesse effettuato l'accesso. Non va bene.
Ma la correzione è semplice:
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
});
Sconsigliato: chiamate "Ajax" sincrone
Come ho già detto, alcune (!) Operazioni asincrone hanno controparti sincrone. Non sostengo il loro utilizzo, ma per completezza, ecco come eseguire una chiamata sincrona:
Senza jQuery
Se si utilizza direttamente un XMLHTTPRequest
oggetto, passare false
come terzo argomento a .open
.
jQuery
Se si utilizza jQuery , è possibile impostare l' async
opzione su false
. Nota che questa opzione è obsoleta da jQuery 1.8. È quindi possibile utilizzare ancora un success
callback o accedere alla responseText
proprietà dell'oggetto jqXHR :
function foo() {
var jqXHR = $.ajax({
//...
async: false
});
return jqXHR.responseText;
}
Se si utilizza qualsiasi altro metodo jQuery Ajax, come $.get
, $.getJSON
ecc., È necessario modificarlo in $.ajax
(poiché è possibile solo passare i parametri di configurazione a $.ajax
).
Dritta! Non è possibile effettuare una richiesta JSONP sincrona . JSONP per sua natura è sempre asincrono (un motivo in più per non considerare nemmeno questa opzione).