Come restituisco la risposta da una chiamata asincrona?
Ho una funzione foo
che fa una richiesta asincrona. Come posso restituire la risposta / risultato da foo
?
Ho provato a restituire il valore dal callback, oltre ad assegnare il risultato a una variabile locale all'interno della funzione e restituire quella, ma nessuno di questi modi restituisce effettivamente la risposta (restituiscono tutti undefined
o qualunque sia il valore iniziale della variabile result
) .
Esempio utilizzando la ajax
funzione di jQuery :
function foo() {
var result;
$.ajax({
url: '...',
success: function(response) {
result = response;
// return response; // <- I tried that one as well
}
});
return result; // It always returns `undefined`
}
Esempio utilizzando node.js:
function foo() {
var result;
fs.readFile("path/to/file", function(err, data) {
result = data;
// return data; // <- I tried that one as well
});
return result; // It always returns `undefined`
}
Esempio utilizzando il then
blocco di una promessa:
function foo() {
var result;
fetch(url).then(function(response) {
result = response;
// return response; // <- I tried that one as well
});
return result; // It always returns `undefined`
}
Risposte
→ Per una spiegazione più generale del comportamento asincrono con diversi esempi, vedere Perché la mia variabile rimane inalterata dopo averla modificata all'interno di una funzione? - Riferimento al codice asincrono
→ Se si comprende già il problema, passare alle possibili soluzioni di seguito.
Il problema
La A in Ajax sta per asincrono . Ciò significa che l'invio della richiesta (o meglio la ricezione della risposta) viene escluso 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 success
callback fosse chiamata.
Ecco un'analogia che si spera renda 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 'di tempo, aspetti al telefono e fissi il vuoto, finché il tuo amico non ti dà la risposta di cui avevi bisogno.
Lo stesso accade quando si effettua 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 l' findItem
esecuzione potrebbe richiedere molto tempo, qualsiasi codice successivo var item = findItem();
deve attendere fino a quando la funzione restituisce il risultato.
Asincrono
Chiami di nuovo il tuo amico per lo stesso motivo. Ma questa volta gli dici che hai fretta e che dovrebbe richiamarti sul tuo cellulare. Riattacchi, esci di casa e fai tutto ciò che avevi programmato di fare. Quando il tuo amico ti richiama, hai a che fare con le informazioni che ti ha dato.
Questo è esattamente ciò che accade quando fai una richiesta Ajax.
findItem(function(item) {
// Do something with the item
});
doSomethingElse();
Invece di attendere la risposta, l'esecuzione continua immediatamente e l'istruzione dopo la chiamata Ajax viene eseguita. Per ottenere la risposta alla fine, fornisci una funzione da chiamare una volta ricevuta la risposta, una richiamata (nota qualcosa? Richiama ?). Qualsiasi istruzione successiva a quella chiamata viene eseguita prima che venga chiamata la richiamata.
Soluzione / i
Abbraccia la natura asincrona di JavaScript! Sebbene alcune operazioni asincrone forniscano controparti sincrone (così fa "Ajax"), è generalmente sconsigliato utilizzarle, specialmente in un contesto di browser.
Perché è brutto lo chiedi?
JavaScript viene eseguito nel thread dell'interfaccia utente del browser e qualsiasi processo a esecuzione prolungata bloccherà l'interfaccia utente, impedendone la risposta. Inoltre, esiste un limite massimo al tempo di esecuzione per JavaScript e il browser chiederà all'utente se continuare o meno l'esecuzione.
Tutto questo è un'esperienza utente davvero negativa. L'utente non sarà in grado di dire se tutto funziona correttamente o meno. 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:
async/await
Promette con (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 di promesse)
Tutti e tre sono disponibili nei browser attuali e nel nodo 7+.
ES2017 +: promette 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 in modo 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
"scarta" una promessa e restituisce il valore con cui la promessa è stata risolta o genera un errore se la promessa è stata rifiutata.
Importante: puoi usare solo await
all'interno di una async
funzione. Al momento, il livello superiore await
non è ancora supportato, quindi potrebbe essere necessario creare un IIFE ( Espressione di funzione immediatamente invocata) asincrono per avviare un async
contesto.
Puoi leggere di più su asynce awaitsu MDN.
Ecco un esempio che si basa sul ritardo 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);
})();
Supporto delle versioni correnti del browser e del nodoasync/await
. Puoi anche supportare ambienti più vecchi trasformando il tuo codice in ES5 con l'aiuto del rigeneratore (o strumenti che utilizzano il rigeneratore, come Babel ).
Consentire alle funzioni di accettare i callback
Un callback è quando la funzione 1 viene passata alla funzione 2. La funzione 2 può chiamare la funzione 1 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, puoi fare in modo che foo
accetti una richiamata e success
usala come richiamata. 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 alla 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 success
. Cioè una volta che la richiesta Ajax ha successo, $.ajax
chiamerà callback
e passerà la risposta al callback (a cui ci si può riferire result
, poiché è così che abbiamo definito il callback).
Puoi anche elaborare la risposta prima di passarla alla richiamata:
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. Dopo tutto, JavaScript nel browser è fortemente guidato dagli eventi (eventi DOM). La ricezione della risposta Ajax non è altro che un evento.
Potrebbero sorgere difficoltà quando devi lavorare con codice di terze parti, ma la maggior parte dei problemi può essere risolta semplicemente pensando al flusso dell'applicazione.
ES2015 +: promette con then ()
L' API Promise è una nuova funzionalità di ECMAScript 6 (ES2015), ma ha già un buon supporto per i browser . Ci sono anche molte librerie che implementano l'API Promises standard e forniscono metodi aggiuntivi per facilitare l'uso e la composizione di funzioni asincrone (es. Bluebird ).
Le promesse sono contenitori di valori futuri . Quando la promessa riceve il valore (viene risolta ) o quando viene annullata ( rifiutata ), avvisa tutti i suoi "ascoltatori" che vogliono accedere a questo valore.
Il vantaggio rispetto ai callback semplici è che ti consentono di disaccoppiare il tuo codice e sono più facili da comporre.
Ecco un 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 la promessa offre va oltre lo scopo di questa risposta, ma se scrivi un nuovo codice, dovresti considerarli seriamente. Forniscono una grande astrazione e separazione del codice.
Ulteriori informazioni sulle promesse: HTML5 Rocks - JavaScript Promises
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: prometti trucchi
Tieni presente che le promesse e gli oggetti differiti sono solo contenitori per un valore futuro, non sono il valore stesso. Ad esempio, supponi 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 di cui sopra. In particolare, $.ajax()
non congela il codice mentre controlla la pagina '/ password' sul tuo 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 riceverà sempre questo oggetto Deferred, lo tratterà come true
e procederà come se l'utente fosse connesso. Non va bene.
Ma la soluzione è 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
});
Non consigliato: chiamate "Ajax" sincrone
Come ho già detto, alcune (!) Operazioni asincrone hanno controparti sincrone. Non ne sostengo l'uso, ma per completezza, ecco come eseguiresti una chiamata sincrona:
Senza jQuery
Se usi direttamente un XMLHttpRequestoggetto, passa false
come terzo argomento a .open.
jQuery
Se usi jQuery , puoi impostare l' async
opzione su false
. Nota che questa opzione è deprecata da jQuery 1.8. È quindi possibile utilizzare ancora una success
richiamata o accedere alla responseText
proprietà dell'oggetto jqXHR :
function foo() {
var jqXHR = $.ajax({
//...
async: false
});
return jqXHR.responseText;
}
Se si utilizza qualsiasi altro metodo di jQuery Ajax, come ad esempio $.get
, $.getJSON
e così via, è necessario cambiare a $.ajax
(dato che è possibile passare solo i parametri di configurazione a $.ajax
).
Dritta! Non è possibile effettuare una richiesta JSONP sincrona . JSONP per sua stessa natura è sempre asincrono (un motivo in più per non considerare nemmeno questa opzione).
Se stai non utilizzando jQuery nel codice, questa risposta è per voi
Il tuo codice dovrebbe essere qualcosa sulla falsariga di 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 ottimo 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 coloro che utilizzano la nuova fetchAPI, Angular o promesse ho aggiunto un'altra risposta di seguito )
Quello che stai affrontando
Questo è un breve riassunto della "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 meglio la ricezione della risposta) viene escluso dal normale flusso di esecuzione. Nel tuo esempio, .sendritorna immediatamente e l'istruzione successiva,, return result;
viene eseguita prima ancora che la funzione che hai passato come success
callback fosse chiamata.
Ciò significa che quando torni, il listener che hai definito non è stato ancora 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;
}
(Violino)
Il valore di a
restituito è undefined
poiché la a=5
parte non è stata ancora eseguita. AJAX si comporta in questo modo, stai restituendo il valore prima che il server abbia la possibilità di dire al tuo browser qual è quel valore.
Una possibile soluzione a questo problema è codificare in modo reattivo , dicendo al programma cosa fare una volta completato il calcolo.
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 getFive
un'azione da eseguire quando viene completata, stiamo dicendo al nostro codice come reagire quando un evento viene completato (come la nostra chiamata AJAX, o in questo caso il timeout).
L'utilizzo sarebbe:
getFive(onComplete);
Che dovrebbe avvisare "5" sullo schermo. (Violino) .
Possibili soluzioni
Esistono fondamentalmente due modi per risolvere questo problema:
- Rendi sincrona la chiamata AJAX (chiamiamola SJAX).
- Ristrutturare il codice per funzionare correttamente con i callback.
1. AJAX sincrono - Non farlo !!
Per quanto riguarda AJAX sincrono, non farlo! La risposta di Felix solleva alcuni argomenti convincenti sul motivo per cui è una cattiva idea. Per riassumere, bloccherà il browser dell'utente fino a quando il server non restituirà la risposta e creerà un'esperienza utente molto negativa. Ecco un altro breve riassunto tratto da MDN sul perché:
XMLHttpRequest supporta comunicazioni sincrone e asincrone. In generale, tuttavia, le richieste asincrone dovrebbero essere preferite alle richieste sincrone per motivi di prestazioni.
In breve, le richieste sincrone bloccano l'esecuzione del codice ... ... questo può causare seri problemi ...
Se si dispone di farlo, è possibile passare una bandiera: 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);
}
2. Codice di ristrutturazione
Lascia che la tua funzione accetti una richiamata. Nell'esempio è foo
possibile fare in modo che il codice accetti una richiamata. Diremo al nostro codice come reagire una volta foo
completato.
Così:
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 sembrare:
function myHandler(result) {
// code that depends on `result`
}
foo(myHandler);
Per maggiori dettagli su come viene fatto questo tipo di design di callback, controlla la risposta di Felix.
Ora, definiamo foo 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();
}
(violino)
Ora abbiamo fatto in modo che la nostra funzione foo accetti un'azione da eseguire quando AJAX viene completato con successo, possiamo estenderlo ulteriormente controllando se lo stato della risposta non è 200 e agendo di conseguenza (creare un gestore di errori e simili). Risolvendo efficacemente il nostro problema.
Se hai ancora difficoltà a capirlo, leggi la guida introduttiva di AJAX su MDN.
XMLHttpRequest 2 (prima di tutto leggi le risposte di Benjamin Gruenbaum e Felix Kling )
Se non usi jQuery e desideri un breve XMLHttpRequest 2 che funzioni sui browser moderni e anche sui browser 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:
- È più breve di tutte le altre funzioni elencate.
- La richiamata viene impostata direttamente (quindi nessuna chiusura extra non necessaria).
- Usa il nuovo onload (quindi non devi controllare lo stato readystate e lo stato)
- Ci sono altre situazioni che non ricordo che rendono fastidioso XMLHttpRequest 1.
Esistono due modi per ottenere la risposta di questa chiamata Ajax (tre utilizzando il nome var XMLHttpRequest):
Il più semplice:
this.response
O se per qualche motivo bind()
richiami una classe:
e.target.response
Esempio:
function callback(e){
console.log(this.response);
}
ajax('URL', callback);
Oppure (quello sopra è meglio le funzioni anonime sono sempre un problema):
ajax('URL', function(e){console.log(this.response)});
Niente di più facile.
Ora alcune persone probabilmente diranno che è meglio usare onreadystatechange o anche il nome della variabile XMLHttpRequest. È sbagliato.
Scopri le funzionalità avanzate di XMLHttpRequest
Supportava tutti i * browser moderni. E posso confermare che sto usando questo approccio poiché esiste XMLHttpRequest 2. Non ho mai avuto alcun tipo di problema su tutti i browser che utilizzo.
onreadystatechange è utile solo se vuoi ottenere le intestazioni sullo stato 2.
L'uso del XMLHttpRequest
nome della variabile è un altro grosso errore in quanto è necessario eseguire il callback all'interno delle chiusure onload / oreadystatechange altrimenti l'hai perso.
Ora se vuoi qualcosa di più complesso usando post e FormData puoi 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)
}
Ancora una volta ... è una funzione molto breve, ma riceve e invia.
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
Oppure passa un elemento completo del modulo ( document.getElementsByTagName('form')[0]
):
var fd = new FormData(form);
x(url, callback, 'post', fd);
Oppure imposta alcuni valori personalizzati:
var fd = new FormData();
fd.append('key', 'val')
x(url, callback, 'post', fd);
Come puoi vedere non ho implementato la sincronizzazione ... è una brutta cosa.
Detto questo ... perché non farlo nel modo più semplice?
Come accennato nel commento, l'uso di errore && sincrono interrompe completamente il punto della risposta. Qual è un bel modo breve per utilizzare 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);
Nello script precedente, hai un gestore degli errori che è definito staticamente in modo da non compromettere la funzione. Il gestore degli errori può essere utilizzato anche per altre funzioni.
Ma per ottenere davvero un errore l' unico modo è scrivere un URL sbagliato, nel qual caso ogni browser genera un errore.
I gestori di errori sono forse utili se si impostano intestazioni personalizzate, si imposta responseType sul buffer dell'array BLOB o qualsiasi altra cosa ...
Anche se passi "POSTAPAPAP" come metodo, non verrà generato un errore.
Anche se passi "fdggdgilfdghfldj" come formdata, non verrà generato un errore.
Nel primo caso l'errore è all'interno del displayAjax()
sotto this.statusText
as Method not Allowed
.
Nel secondo caso, funziona semplicemente. Devi controllare sul lato server se hai passato i dati corretti del post.
interdominio non consentito genera automaticamente un errore.
Nella risposta all'errore non sono presenti codici di errore.
C'è solo il this.type
che è impostato su errore.
Perché aggiungere un gestore degli errori se non hai il controllo totale sugli errori? La maggior parte degli errori vengono restituiti all'interno di questo nella funzione di callback displayAjax()
.
Quindi: non sono necessari controlli di errore se sei in grado di copiare e incollare correttamente l'URL. ;)
PS: Come primo test ho scritto x ('x', displayAjax) ..., e ha ottenuto totalmente una risposta ... ??? Quindi ho controllato la cartella in cui si trova l'HTML e c'era un file chiamato "x.xml". Quindi, anche se dimentichi l'estensione del tuo file XMLHttpRequest 2, LO TROVERAI . Mi sono divertito
Leggi un file sincrono
Non farlo.
Se vuoi bloccare il browser per un po 'carica un bel .txt
file grande sincrono.
function omg(a, c){ // URL
c = new XMLHttpRequest;
c.open('GET', a, true);
c.send();
return c; // Or c.response
}
Adesso puoi farlo
var res = omg('thisIsGonnaBlockThePage.txt');
Non c'è altro modo per farlo in modo non asincrono. (Sì, con setTimeout loop ... ma sul serio?)
Un altro punto è ... se lavori con le API o solo con i file della tua lista o qualsiasi altra cosa, usi sempre funzioni diverse per ogni richiesta ...
Solo se hai una pagina dove carichi sempre lo stesso XML / JSON o qualsiasi altra cosa ti serve una sola funzione. In tal caso, modifica un po 'la funzione Ajax e sostituisci b con la tua funzione speciale.
Le funzioni di cui sopra sono per uso di base.
Se vuoi ESTENDERE la funzione ...
Si, puoi.
Sto usando molte 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 molte cose con XMLHttpRequest 2:
Ho realizzato un download manager (utilizzando intervalli su entrambi i lati con curriculum, filereader, filesystem), vari convertitori di resizer di immagini usando canvas, popolando database SQL web con base64images e molto altro ... Ma in questi casi dovresti creare una funzione solo per quello scopo ... a volte hai bisogno di un blob, buffer di array, puoi impostare intestazioni, sovrascrivere il tipo Mime e c'è molto di più ...
Ma la domanda qui è come restituire una risposta Ajax ... (ho aggiunto un modo semplice).
Se stai usando le promesse, questa risposta è per te.
Ciò significa AngularJS, jQuery (con differito), sostituzione di XHR nativo (fetch), EmberJS, salvataggio di BackboneJS o qualsiasi libreria di nodi che restituisce promesse.
Il tuo codice dovrebbe essere qualcosa sulla falsariga di questo:
function foo() {
var data;
// or $.get(...).then, or request(...).then, or query(...).then
fetch("/echo/json").then(function(response){
data = response.json();
});
return data;
}
var result = foo(); // result is always undefined no matter what.
Felix Kling ha fatto un ottimo lavoro scrivendo una risposta per le persone che usano jQuery con callback per AJAX. Ho una risposta per XHR nativo. Questa risposta è per l'uso generico delle promesse sul frontend o sul backend.
La questione centrale
Il modello di concorrenza JavaScript nel browser e sul server con NodeJS / io.js è asincrono e reattivo .
Ogni volta che si chiama un metodo che restituisce una promessa, i then
gestori vengono sempre eseguiti in modo asincrono, ovvero dopo il codice sottostante che non si trova in un .then
gestore.
Ciò significa che quando stai restituendo data
il then
gestore che hai definito non è stato ancora eseguito. Questo a sua volta significa che il valore che stai restituendo non è stato impostato sul valore corretto nel tempo.
Ecco una semplice analogia per il problema:
function getFive(){
var data;
setTimeout(function(){ // set a timer for one second in the future
data = 5; // after a second, do this
}, 1000);
return data;
}
document.body.innerHTML = getFive(); // `undefined` here and not 5
Il valore di data
è undefined
poiché la data = 5
parte non è stata ancora eseguita. Probabilmente verrà eseguito in un secondo, ma a quel punto è irrilevante per il valore restituito.
Poiché l'operazione non è ancora avvenuta (AJAX, chiamata server, IO, timer) stai restituendo il valore prima che la richiesta abbia la possibilità di dire al tuo codice qual è quel valore.
Una possibile soluzione a questo problema è codificare in modo reattivo , dicendo al programma cosa fare una volta completato il calcolo. Le promesse consentono attivamente questo essendo di natura temporale (sensibile al tempo).
Riepilogo rapido delle promesse
Una promessa è un valore nel tempo . Le promesse hanno uno stato, iniziano come in sospeso senza valore e possono essere fissate a:
- soddisfatto, il che significa che il calcolo è stato completato con successo.
- rifiutato significa che il calcolo non è riuscito.
Una promessa può cambiare stato solo una volta, dopodiché rimarrà sempre nello stesso stato per sempre. È possibile associare then
gestori alle promesse per estrarne il valore e gestire gli errori. then
i gestori consentono il concatenamento delle chiamate. Le promesse vengono create utilizzando le API che le restituiscono . Ad esempio, la più moderna sostituzione AJAX fetch
o le $.get
promesse di ritorno di jQuery .
Quando chiediamo .then
una promessa e restituiamo qualcosa da essa, otteniamo una promessa per il valore elaborato . Se restituiamo un'altra promessa, otterremo cose incredibili, ma teniamo i nostri cavalli.
Con promesse
Vediamo come possiamo risolvere il problema di cui sopra con le promesse. Innanzitutto, dimostriamo la nostra comprensione degli stati di promessa dall'alto utilizzando il costruttore Promise per creare una funzione di ritardo:
function delay(ms){ // takes amount of milliseconds
// returns a new promise
return new Promise(function(resolve, reject){
setTimeout(function(){ // when the time is up
resolve(); // change the promise to the fulfilled state
}, ms);
});
}
Ora, dopo aver convertito setTimeout per usare le promesse, possiamo usarlo then
per farlo contare:
function delay(ms){ // takes amount of milliseconds
// returns a new promise
return new Promise(function(resolve, reject){
setTimeout(function(){ // when the time is up
resolve(); // change the promise to the fulfilled state
}, ms);
});
}
function getFive(){
// we're RETURNING the promise, remember, a promise is a wrapper over our value
return delay(100).then(function(){ // when the promise is ready
return 5; // return the value 5, promises are all about return values
})
}
// we _have_ to wrap it like this in the call site, we can't access the plain value
getFive().then(function(five){
document.body.innerHTML = five;
});
In sostanza, invece di restituire un valore di cui non possiamo fare a causa del modello di concorrenza - stiamo restituendo un involucro per un valore che siamo in grado di scartare con then
. È come una scatola con cui puoi aprire then
.
Applicando questo
Questo è lo stesso per la tua chiamata API originale, puoi:
function foo() {
// RETURN the promise
return fetch("/echo/json").then(function(response){
return response.json(); // process it inside the `then`
});
}
foo().then(function(response){
// access the value inside the `then`
})
Quindi funziona altrettanto bene. Abbiamo imparato che non possiamo restituire valori da chiamate già asincrone, ma possiamo usare promesse e concatenarle per eseguire l'elaborazione. Ora sappiamo come restituire la risposta da una chiamata asincrona.
ES2015 (ES6)
ES6 introduce i generatori che sono funzioni che possono tornare nel mezzo e quindi riprendere il punto in cui si trovavano. Questo è in genere utile per le sequenze, ad esempio:
function* foo(){ // notice the star, this is ES6 so new browsers/node/io only
yield 1;
yield 2;
while(true) yield 3;
}
È una funzione che restituisce un iteratore sulla sequenza 1,2,3,3,3,3,....
che può essere iterato. Anche se questo è interessante di per sé e apre spazio a molte possibilità, c'è un caso particolarmente interessante.
Se la sequenza che stiamo producendo è una sequenza di azioni piuttosto che di numeri, possiamo mettere in pausa la funzione ogni volta che viene restituita un'azione e aspettarla prima di riprendere la funzione. Quindi, invece di una sequenza di numeri, abbiamo bisogno di una sequenza di valori futuri , ovvero: promesse.
Questo trucco un po 'complicato ma molto potente ci consente di scrivere codice asincrono in modo sincrono. Ci sono diversi "corridori" che lo fanno per te, scriverne uno è di poche righe di codice ma va oltre lo scopo di questa risposta. Userò Bluebird's Promise.coroutine
qui, ma ci sono altri wrapper come co
o Q.async
.
var foo = coroutine(function*(){
var data = yield fetch("/echo/json"); // notice the yield
// code here only executes _after_ the request is done
return data.json(); // data is defined
});
Questo metodo restituisce una promessa stessa, che possiamo consumare da altre coroutine. Per esempio:
var main = coroutine(function*(){
var bar = yield foo(); // wait our earlier coroutine, it returns a promise
// server call done here, code below executes when done
var baz = yield fetch("/api/users/"+bar.userid); // depends on foo's result
console.log(baz); // runs after both requests done
});
main();
ES2016 (ES7)
In ES7, questo è ulteriormente standardizzato, ci sono diverse proposte in questo momento ma in tutte puoi await
promettere. Questo è solo "zucchero" (sintassi migliore) per la proposta ES6 sopra, aggiungendo le parole chiave async
e await
. Facendo l'esempio sopra:
async function foo(){
var data = await fetch("/echo/json"); // notice the await
// code here only executes _after_ the request is done
return data.json(); // data is defined
}
Restituisce comunque una promessa lo stesso :)
Stai usando Ajax in modo errato. L'idea è di non fare in modo che restituisca qualcosa, ma invece di trasferire i dati a qualcosa chiamato funzione di callback, che gestisce i dati.
Questo è:
function handleData( responseData ) {
// Do what you want with the data
console.log(responseData);
}
$.ajax({
url: "hi.php",
...
success: function ( data, status, XHR ) {
handleData(data);
}
});
Restituire qualcosa nel gestore di invio non farà nulla. Devi invece trasferire i dati o fare quello che vuoi direttamente all'interno della funzione di successo.
La soluzione più semplice è creare una funzione JavaScript e chiamarla per il success
callback Ajax .
function callServerAsync(){
$.ajax({ url: '...', success: function(response) { successCallback(response); } }); } function successCallback(responseObj){ // Do something like read the response and show data alert(JSON.stringify(responseObj)); // Only applicable to JSON response } function foo(callback) { $.ajax({
url: '...',
success: function(response) {
return callback(null, response);
}
});
}
var result = foo(function(err, result){
if (!err)
console.log(result);
});
Risponderò con un fumetto disegnato a mano dall'aspetto orribile. La seconda immagine è il motivo per cui si result
trova undefined
nel tuo esempio di codice.
Angular1
Per le persone che utilizzano AngularJS , è possibile gestire questa situazione utilizzando Promises
.
Qui dice
Le promesse possono essere utilizzate per annullare le funzioni asincrone e consentono di concatenare più funzioni insieme.
Puoi trovare una bella spiegazione anche qui .
Esempio trovato nei documenti menzionati di seguito.
promiseB = promiseA.then(
function onSuccess(result) {
return result + 1;
}
,function onError(err) {
//Handle error
}
);
// promiseB will be resolved immediately after promiseA is resolved
// and its value will be the result of promiseA incremented by 1.
Angular2 e versioni successive
In Angular2
con un'occhiata al seguente esempio, ma il suo raccomandato per l'uso Observables
con Angular2
.
search(term: string) {
return this.http
.get(`https://api.spotify.com/v1/search?q=${term}&type=artist`)
.map((response) => response.json())
.toPromise();
}
Puoi consumarlo in questo modo,
search() {
this.searchService.search(this.searchField.value)
.then((result) => {
this.result = result.artists.items;
})
.catch((error) => console.error(error));
}
Guarda il post originale qui. Ma Typescript non supporta le promesse es6 native , se vuoi usarlo, potresti aver bisogno di plugin per questo.
Inoltre, ecco le specifiche delle promesse definite qui.
La maggior parte delle risposte qui fornisce suggerimenti utili per quando si dispone di una singola operazione asincrona, ma a volte questo viene visualizzato quando è necessario eseguire un'operazione asincrona per ogni voce in un array o in un'altra struttura simile a un elenco. La tentazione è di fare questo:
// WRONG
var results = [];
theArray.forEach(function(entry) {
doSomethingAsync(entry, function(result) {
results.push(result);
});
});
console.log(results); // E.g., using them, returning them, etc.
Esempio:
// WRONG
var theArray = [1, 2, 3];
var results = [];
theArray.forEach(function(entry) {
doSomethingAsync(entry, function(result) {
results.push(result);
});
});
console.log("Results:", results); // E.g., using them, returning them, etc.
function doSomethingAsync(value, callback) {
console.log("Starting async operation for " + value);
setTimeout(function() {
console.log("Completing async operation for " + value);
callback(value * 2);
}, Math.floor(Math.random() * 200));
}
.as-console-wrapper {
max-height: 100% !important;
}
Il motivo per cui non funziona è che i callback di doSomethingAsync
non sono ancora stati eseguiti nel momento in cui stai tentando di utilizzare i risultati.
Quindi, se si dispone di un array (o di un elenco di qualche tipo) e si desidera eseguire operazioni asincrone per ciascuna voce, sono disponibili due opzioni: eseguire le operazioni in parallelo (sovrapposte) o in serie (una dopo l'altra in sequenza).
Parallelo
Puoi avviarli tutti e tenere traccia di quanti callback ti aspetti, quindi utilizzare i risultati quando hai ottenuto tanti callback:
var results = [];
var expecting = theArray.length;
theArray.forEach(function(entry, index) {
doSomethingAsync(entry, function(result) {
results[index] = result;
if (--expecting === 0) {
// Done!
console.log("Results:", results); // E.g., using the results
}
});
});
Esempio:
var theArray = [1, 2, 3];
var results = [];
var expecting = theArray.length;
theArray.forEach(function(entry, index) {
doSomethingAsync(entry, function(result) {
results[index] = result;
if (--expecting === 0) {
// Done!
console.log("Results:", results); // E.g., using the results
}
});
});
function doSomethingAsync(value, callback) {
console.log("Starting async operation for " + value);
setTimeout(function() {
console.log("Completing async operation for " + value);
callback(value * 2);
}, Math.floor(Math.random() * 200));
}
.as-console-wrapper {
max-height: 100% !important;
}
(Potremmo farla finita expecting
e usarla results.length === theArray.length
, ma questo ci lascia aperti alla possibilità che theArray
venga modificata mentre le chiamate sono in sospeso ...)
Si noti come utilizziamo index
from forEach
per salvare il risultato nella results
stessa posizione della voce a cui si riferisce, anche se i risultati arrivano fuori ordine (poiché le chiamate asincrone non vengono necessariamente completate nell'ordine in cui sono state avviate).
Ma cosa succede se devi restituire quei risultati da una funzione? Come hanno sottolineato le altre risposte, non puoi; devi fare in modo che la tua funzione accetti e richiami una richiamata (o restituisca una promessa ). Ecco una versione di callback:
function doSomethingWith(theArray, callback) {
var results = [];
var expecting = theArray.length;
theArray.forEach(function(entry, index) {
doSomethingAsync(entry, function(result) {
results[index] = result;
if (--expecting === 0) {
// Done!
callback(results);
}
});
});
}
doSomethingWith(theArray, function(results) {
console.log("Results:", results);
});
Esempio:
function doSomethingWith(theArray, callback) {
var results = [];
var expecting = theArray.length;
theArray.forEach(function(entry, index) {
doSomethingAsync(entry, function(result) {
results[index] = result;
if (--expecting === 0) {
// Done!
callback(results);
}
});
});
}
doSomethingWith([1, 2, 3], function(results) {
console.log("Results:", results);
});
function doSomethingAsync(value, callback) {
console.log("Starting async operation for " + value);
setTimeout(function() {
console.log("Completing async operation for " + value);
callback(value * 2);
}, Math.floor(Math.random() * 200));
}
.as-console-wrapper {
max-height: 100% !important;
}
Oppure ecco una versione che restituisce Promise
invece un :
function doSomethingWith(theArray) {
return new Promise(function(resolve) {
var results = [];
var expecting = theArray.length;
theArray.forEach(function(entry, index) {
doSomethingAsync(entry, function(result) {
results[index] = result;
if (--expecting === 0) {
// Done!
resolve(results);
}
});
});
});
}
doSomethingWith(theArray).then(function(results) {
console.log("Results:", results);
});
Ovviamente, se doSomethingAsync
ci trasmettessero errori, reject
rifiuteremmo la promessa quando otteniamo un errore.)
Esempio:
function doSomethingWith(theArray) {
return new Promise(function(resolve) {
var results = [];
var expecting = theArray.length;
theArray.forEach(function(entry, index) {
doSomethingAsync(entry, function(result) {
results[index] = result;
if (--expecting === 0) {
// Done!
resolve(results);
}
});
});
});
}
doSomethingWith([1, 2, 3]).then(function(results) {
console.log("Results:", results);
});
function doSomethingAsync(value, callback) {
console.log("Starting async operation for " + value);
setTimeout(function() {
console.log("Completing async operation for " + value);
callback(value * 2);
}, Math.floor(Math.random() * 200));
}
.as-console-wrapper {
max-height: 100% !important;
}
(O in alternativa, potresti creare un involucro per doSomethingAsync
restituire una promessa, e poi fare quanto segue ...)
Se doSomethingAsync
ti dà una promessa , puoi usare Promise.all:
function doSomethingWith(theArray) {
return Promise.all(theArray.map(function(entry) {
return doSomethingAsync(entry);
}));
}
doSomethingWith(theArray).then(function(results) {
console.log("Results:", results);
});
Se sai che doSomethingAsync
ignorerà un secondo e un terzo argomento, puoi semplicemente passarlo direttamente a map
( map
chiama il suo callback con tre argomenti, ma la maggior parte delle persone usa solo il primo per la maggior parte del tempo):
function doSomethingWith(theArray) {
return Promise.all(theArray.map(doSomethingAsync));
}
doSomethingWith(theArray).then(function(results) {
console.log("Results:", results);
});
Esempio:
function doSomethingWith(theArray) {
return Promise.all(theArray.map(doSomethingAsync));
}
doSomethingWith([1, 2, 3]).then(function(results) {
console.log("Results:", results);
});
function doSomethingAsync(value) {
console.log("Starting async operation for " + value);
return new Promise(function(resolve) {
setTimeout(function() {
console.log("Completing async operation for " + value);
resolve(value * 2);
}, Math.floor(Math.random() * 200));
});
}
.as-console-wrapper {
max-height: 100% !important;
}
Nota che Promise.all
risolve la sua promessa con una serie dei risultati di tutte le promesse che gli dai quando sono tutte risolte, o rifiuta la sua promessa quando la prima delle promesse che le dai viene rifiutata.
Serie
Supponi di non volere che le operazioni siano in parallelo? Se si desidera eseguirli uno dopo l'altro, è necessario attendere il completamento di ciascuna operazione prima di avviare la successiva. Ecco un esempio di una funzione che lo fa e chiama un callback con il risultato:
function doSomethingWith(theArray, callback) {
var results = [];
doOne(0);
function doOne(index) {
if (index < theArray.length) {
doSomethingAsync(theArray[index], function(result) {
results.push(result);
doOne(index + 1);
});
} else {
// Done!
callback(results);
}
}
}
doSomethingWith(theArray, function(results) {
console.log("Results:", results);
});
(Dal momento che stiamo facendo il lavoro in serie, possiamo semplicemente usarlo results.push(result)
poiché sappiamo che non otterremo risultati fuori ordine. In precedenza avremmo potuto usare results[index] = result;
, ma in alcuni dei seguenti esempi non abbiamo un indice usare.)
Esempio:
function doSomethingWith(theArray, callback) {
var results = [];
doOne(0);
function doOne(index) {
if (index < theArray.length) {
doSomethingAsync(theArray[index], function(result) {
results.push(result);
doOne(index + 1);
});
} else {
// Done!
callback(results);
}
}
}
doSomethingWith([1, 2, 3], function(results) {
console.log("Results:", results);
});
function doSomethingAsync(value, callback) {
console.log("Starting async operation for " + value);
setTimeout(function() {
console.log("Completing async operation for " + value);
callback(value * 2);
}, Math.floor(Math.random() * 200));
}
.as-console-wrapper {
max-height: 100% !important;
}
(O, ancora, costruisci un involucro per doSomethingAsync
questo ti dà una promessa e fai quanto segue ...)
Se doSomethingAsync
ti dà una promessa, se puoi usare la sintassi di ES2017 + (magari con un transpiler come Babel ), puoi usare una asyncfunzione con for-ofe await:
async function doSomethingWith(theArray) {
const results = [];
for (const entry of theArray) {
results.push(await doSomethingAsync(entry));
}
return results;
}
doSomethingWith(theArray).then(results => {
console.log("Results:", results);
});
Esempio:
async function doSomethingWith(theArray) {
const results = [];
for (const entry of theArray) {
results.push(await doSomethingAsync(entry));
}
return results;
}
doSomethingWith([1, 2, 3]).then(function(results) {
console.log("Results:", results);
});
function doSomethingAsync(value) {
console.log("Starting async operation for " + value);
return new Promise(function(resolve) {
setTimeout(function() {
console.log("Completing async operation for " + value);
resolve(value * 2);
}, Math.floor(Math.random() * 200));
});
}
.as-console-wrapper {
max-height: 100% !important;
}
Se non puoi (ancora) usare la sintassi di ES2017 +, puoi usare una variazione sul pattern "Promise reduce" (questo è più complesso del solito Promise reduce perché non stiamo passando il risultato da uno a quello successivo, ma invece raccogliendo i risultati in un array):
function doSomethingWith(theArray) {
return theArray.reduce(function(p, entry) {
return p.then(function(results) {
return doSomethingAsync(entry).then(function(result) {
results.push(result);
return results;
});
});
}, Promise.resolve([]));
}
doSomethingWith(theArray).then(function(results) {
console.log("Results:", results);
});
Esempio:
function doSomethingWith(theArray) {
return theArray.reduce(function(p, entry) {
return p.then(function(results) {
return doSomethingAsync(entry).then(function(result) {
results.push(result);
return results;
});
});
}, Promise.resolve([]));
}
doSomethingWith([1, 2, 3]).then(function(results) {
console.log("Results:", results);
});
function doSomethingAsync(value) {
console.log("Starting async operation for " + value);
return new Promise(function(resolve) {
setTimeout(function() {
console.log("Completing async operation for " + value);
resolve(value * 2);
}, Math.floor(Math.random() * 200));
});
}
.as-console-wrapper {
max-height: 100% !important;
}
... che è meno ingombrante con ES2015 + funzioni freccia :
function doSomethingWith(theArray) {
return theArray.reduce((p, entry) => p.then(results => doSomethingAsync(entry).then(result => {
results.push(result);
return results;
})), Promise.resolve([]));
}
doSomethingWith(theArray).then(results => {
console.log("Results:", results);
});
Esempio:
function doSomethingWith(theArray) {
return theArray.reduce((p, entry) => p.then(results => doSomethingAsync(entry).then(result => {
results.push(result);
return results;
})), Promise.resolve([]));
}
doSomethingWith([1, 2, 3]).then(function(results) {
console.log("Results:", results);
});
function doSomethingAsync(value) {
console.log("Starting async operation for " + value);
return new Promise(function(resolve) {
setTimeout(function() {
console.log("Completing async operation for " + value);
resolve(value * 2);
}, Math.floor(Math.random() * 200));
});
}
.as-console-wrapper {
max-height: 100% !important;
}
Dai un'occhiata a questo esempio:
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope,$http) {
var getJoke = function(){
return $http.get('http://api.icndb.com/jokes/random').then(function(res){
return res.data.value;
});
}
getJoke().then(function(res) {
console.log(res.joke);
});
});
Come puoi vedere getJoke
sta restituendo una promessa risolta (viene risolta al ritorno res.data.value
). Quindi aspetti fino al completamento della richiesta $ http.get e quindi viene eseguito console.log (res.joke) (come un normale flusso asincrono).
Questo è il plnkr:
http://embed.plnkr.co/XlNR7HpCaIhJxskMJfSg/
Modo ES6 (asincrono - attendi)
(function(){
async function getJoke(){
let response = await fetch('http://api.icndb.com/jokes/random');
let data = await response.json();
return data.value;
}
getJoke().then((joke) => {
console.log(joke);
});
})();
Questo è uno dei due modi in cui l'associazione dati o il concetto di archivio utilizzati in molti nuovi framework JavaScript funzioneranno alla grande per te ...
Quindi, se stai usando Angular, React o qualsiasi altro framework che esegue l'associazione dei dati in due modi o il concetto di archivio, questo problema è semplicemente risolto per te, quindi in parole semplici, il tuo risultato è undefined
nella prima fase, quindi hai result = undefined
prima di ricevere il dati, quindi non appena ottieni il risultato, verrà aggiornato e verrà assegnato al nuovo valore quale risposta della tua chiamata Ajax ...
Ma come puoi farlo in puro javascript o jQuery ad esempio come hai chiesto in questa domanda?
Puoi utilizzare una richiamata , una promessa e osservabile di recente per gestirla per te, ad esempio nelle promesse abbiamo una funzione simile success()
o then()
che verrà eseguita quando i tuoi dati saranno pronti per te, lo stesso con la funzione di richiamata o di sottoscrizione su osservabile .
Ad esempio, nel tuo caso in cui stai usando jQuery , puoi fare qualcosa del genere:
$(document).ready(function(){ function foo() { $.ajax({url: "api/data", success: function(data){
fooDone(data); //after we have data, we pass it to fooDone
}});
};
function fooDone(data) {
console.log(data); //fooDone has the data and console.log it
};
foo(); //call happens here
});
Per ulteriori informazioni, studiare le promesse e gli osservabili che sono modi più nuovi per eseguire queste operazioni asincrone.
È un problema molto comune che affrontiamo mentre siamo alle prese con i "misteri" di JavaScript. Fammi provare a demistificare questo mistero oggi.
Cominciamo con una semplice funzione JavaScript:
function foo(){
// do something
return 'wohoo';
}
let bar = foo(); // bar is 'wohoo' here
Questa è una semplice chiamata di funzione sincrona (in cui ogni riga di codice è "terminata con il suo lavoro" prima della successiva in sequenza) e il risultato è lo stesso previsto.
Ora aggiungiamo un po 'di svolta, introducendo un piccolo ritardo nella nostra funzione, in modo che tutte le righe di codice non siano "finite" in sequenza. Pertanto, emulerà il comportamento asincrono della funzione:
function foo(){
setTimeout( ()=>{
return 'wohoo';
}, 1000 )
}
let bar = foo() // bar is undefined here
Quindi ecco fatto, quel ritardo ha appena rotto la funzionalità che ci aspettavamo! Ma cosa è successo esattamente? Bene, in realtà è abbastanza logico se guardi il codice. la funzione foo()
, al momento dell'esecuzione, non restituisce nulla (quindi il valore restituito è undefined
), ma avvia un timer, che esegue una funzione dopo 1 s per restituire 'wohoo'. Ma come puoi vedere, il valore assegnato a bar è il materiale immediatamente restituito da foo (), che non è niente, cioè solo undefined
.
Allora, come affrontiamo questo problema?
Chiediamo una PROMESSA alla nostra funzione . La promessa riguarda davvero ciò che significa: significa che la funzione ti garantisce di fornire qualsiasi output che riceverà in futuro. quindi vediamo in azione per il nostro piccolo problema sopra:
function foo(){
return new Promise( (resolve, reject) => { // I want foo() to PROMISE me something
setTimeout ( function(){
// promise is RESOLVED , when execution reaches this line of code
resolve('wohoo')// After 1 second, RESOLVE the promise with value 'wohoo'
}, 1000 )
})
}
let bar ;
foo().then( res => {
bar = res;
console.log(bar) // will print 'wohoo'
});
Pertanto, il riepilogo è: per affrontare le funzioni asincrone come le chiamate basate su Ajax, ecc., È possibile utilizzare una promessa al resolve
valore (che si intende restituire). Quindi, in breve, si risolve il valore invece di restituire , nelle funzioni asincrone.
AGGIORNAMENTO (promette con async / await)
Oltre a utilizzare then/catch
per lavorare con le promesse, esiste un altro approccio. L'idea è di riconoscere una funzione asincrona e quindi attendere che le promesse si risolvano, prima di passare alla riga di codice successiva. È ancora solo promises
sotto il cofano, ma con un approccio sintattico diverso. Per rendere le cose più chiare, puoi trovare un confronto di seguito:
quindi / cattura versione:
function saveUsers(){
getUsers()
.then(users => {
saveSomewhere(users);
})
.catch(err => {
console.error(err);
})
}
versione asincrona / attesa:
async function saveUsers(){
try{
let users = await getUsers()
saveSomewhere(users);
}
catch(err){
console.error(err);
}
}
Un altro approccio per restituire un valore da una funzione asincrona consiste nel passare un oggetto che memorizzerà il risultato della funzione asincrona.
Ecco un esempio dello stesso:
var async = require("async");
// This wires up result back to the caller
var result = {};
var asyncTasks = [];
asyncTasks.push(function(_callback){
// some asynchronous operation
$.ajax({
url: '...',
success: function(response) {
result.response = response;
_callback();
}
});
});
async.parallel(asyncTasks, function(){
// result is available after performing asynchronous operation
console.log(result)
console.log('Done');
});
Sto usando l' result
oggetto per memorizzare il valore durante l'operazione asincrona. Ciò consente al risultato di essere disponibile anche dopo il lavoro asincrono.
Uso molto questo approccio. Sarei interessato a sapere quanto bene funziona questo approccio quando è coinvolto il cablaggio del risultato attraverso moduli consecutivi.
Mentre le promesse e le richiamate funzionano bene in molte situazioni, è un dolore nella parte posteriore esprimere qualcosa come:
if (!name) {
name = async1();
}
async2(name);
Finiresti per attraversare async1
; controlla se name
è indefinito o meno e chiama la richiamata di conseguenza.
async1(name, callback) {
if (name)
callback(name)
else {
doSomething(callback)
}
}
async1(name, async2)
Anche se va bene in piccoli esempi, diventa fastidioso quando si hanno molti casi simili e la gestione degli errori coinvolti.
Fibers
aiuta a risolvere il problema.
var Fiber = require('fibers')
function async1(container) {
var current = Fiber.current
var result
doSomething(function(name) {
result = name
fiber.run()
})
Fiber.yield()
return result
}
Fiber(function() {
var name
if (!name) {
name = async1()
}
async2(name)
// Make any number of async calls from here
}
Puoi controllare il progetto qui .
Il seguente esempio che ho scritto mostra come farlo
- Gestire le chiamate HTTP asincrone;
- Attendi la risposta da ogni chiamata API;
- Usa lo schema Promise ;
- Usa il pattern Promise.all per unirti a più chiamate HTTP;
Questo esempio funzionante è autonomo. Definirà un semplice oggetto richiesta che utilizza l' XMLHttpRequest
oggetto finestra per effettuare chiamate. Definirà una semplice funzione per attendere il completamento di un mucchio di promesse.
Contesto. L'esempio sta eseguendo una query sull'endpoint Spotify Web API per cercare playlist
oggetti per un determinato insieme di stringhe di query:
[
"search?type=playlist&q=%22doom%20metal%22",
"search?type=playlist&q=Adele"
]
Per ogni elemento, una nuova promessa attiverà un blocco ExecutionBlock
, analizzerà il risultato, pianificherà una nuova serie di promesse in base all'array dei risultati, ovvero un elenco di user
oggetti Spotify ed eseguirà la nuova chiamata HTTP in ExecutionProfileBlock
modo asincrono.
È quindi possibile visualizzare una struttura Promise nidificata, che consente di generare più chiamate HTTP nidificate completamente asincrone e unire i risultati di ogni sottoinsieme di chiamate Promise.all
.
NOTA Le search
API Spotify recenti richiederanno la specifica di un token di accesso nelle intestazioni della richiesta:
-H "Authorization: Bearer {your access token}"
Quindi, per eseguire il seguente esempio, devi inserire il tuo token di accesso nelle intestazioni della richiesta:
var spotifyAccessToken = "YourSpotifyAccessToken";
var console = {
log: function(s) {
document.getElementById("console").innerHTML += s + "<br/>"
}
}
// Simple XMLHttpRequest
// based on https://davidwalsh.name/xmlhttprequest
SimpleRequest = {
call: function(what, response) {
var request;
if (window.XMLHttpRequest) { // Mozilla, Safari, ...
request = new XMLHttpRequest();
} else if (window.ActiveXObject) { // Internet Explorer
try {
request = new ActiveXObject('Msxml2.XMLHTTP');
}
catch (e) {
try {
request = new ActiveXObject('Microsoft.XMLHTTP');
} catch (e) {}
}
}
// State changes
request.onreadystatechange = function() {
if (request.readyState === 4) { // Done
if (request.status === 200) { // Complete
response(request.responseText)
}
else
response();
}
}
request.open('GET', what, true);
request.setRequestHeader("Authorization", "Bearer " + spotifyAccessToken);
request.send(null);
}
}
//PromiseAll
var promiseAll = function(items, block, done, fail) {
var self = this;
var promises = [],
index = 0;
items.forEach(function(item) {
promises.push(function(item, i) {
return new Promise(function(resolve, reject) {
if (block) {
block.apply(this, [item, index, resolve, reject]);
}
});
}(item, ++index))
});
Promise.all(promises).then(function AcceptHandler(results) {
if (done) done(results);
}, function ErrorHandler(error) {
if (fail) fail(error);
});
}; //promiseAll
// LP: deferred execution block
var ExecutionBlock = function(item, index, resolve, reject) {
var url = "https://api.spotify.com/v1/"
url += item;
console.log( url )
SimpleRequest.call(url, function(result) {
if (result) {
var profileUrls = JSON.parse(result).playlists.items.map(function(item, index) {
return item.owner.href;
})
resolve(profileUrls);
}
else {
reject(new Error("call error"));
}
})
}
arr = [
"search?type=playlist&q=%22doom%20metal%22",
"search?type=playlist&q=Adele"
]
promiseAll(arr, function(item, index, resolve, reject) {
console.log("Making request [" + index + "]")
ExecutionBlock(item, index, resolve, reject);
}, function(results) { // Aggregated results
console.log("All profiles received " + results.length);
//console.log(JSON.stringify(results[0], null, 2));
///// promiseall again
var ExecutionProfileBlock = function(item, index, resolve, reject) {
SimpleRequest.call(item, function(result) {
if (result) {
var obj = JSON.parse(result);
resolve({
name: obj.display_name,
followers: obj.followers.total,
url: obj.href
});
} //result
})
} //ExecutionProfileBlock
promiseAll(results[0], function(item, index, resolve, reject) {
//console.log("Making request [" + index + "] " + item)
ExecutionProfileBlock(item, index, resolve, reject);
}, function(results) { // aggregated results
console.log("All response received " + results.length);
console.log(JSON.stringify(results, null, 2));
}
, function(error) { // Error
console.log(error);
})
/////
},
function(error) { // Error
console.log(error);
});
<div id="console" />
Ho discusso ampiamente questa soluzione qui .
La risposta breve è che devi implementare una richiamata come questa:
function callback(response) {
// Here you can do what ever you want with the response object.
console.log(response);
}
$.ajax({
url: "...",
success: callback
});
Risposta 2017: ora puoi fare esattamente quello che vuoi in ogni browser e nodo corrente
Questo è abbastanza semplice:
- Restituisci una promessa
- Usa "await" , che dirà a JavaScript di attendere che la promessa venga risolta in un valore (come la risposta HTTP)
- Aggiungi la parola chiave "async" alla funzione principale
Ecco una versione funzionante del tuo codice:
(async function(){
var response = await superagent.get('...')
console.log(response)
})()
await è supportato in tutti i browser attuali e nel nodo 8
Js è un singolo thread.
Il browser può essere diviso in tre parti:
1) Event Loop
2) API Web
3) Coda eventi
Event Loop viene eseguito per sempre, ovvero una sorta di loop infinito. L'Event Queue è dove tutte le tue funzioni vengono spinte su qualche evento (esempio: clic) questo viene eseguito uno ad uno dalla coda e messo in Event loop che esegue questa funzione e si prepara da solo per quella successiva dopo che la prima è stata eseguita. Ciò significa che l'esecuzione di una funzione non viene avviata fino a quando la funzione prima di essere in coda non viene eseguita nel ciclo di eventi.
Ora pensiamo di aver inserito due funzioni in una coda, una è per ottenere un dato dal server e un'altra utilizza quei dati. Abbiamo inserito prima la funzione serverRequest () in coda e poi la funzione utiliseData (). La funzione serverRequest va nel loop degli eventi ed effettua una chiamata al server poiché non sappiamo mai quanto tempo ci vorrà per ottenere i dati dal server, quindi questo processo dovrebbe richiedere tempo e quindi abbiamo occupato il nostro loop degli eventi appendendo la nostra pagina, è lì L'API entra nel ruolo prende questa funzione dal loop degli eventi e si occupa del server che rende libero il loop degli eventi in modo che possiamo eseguire la funzione successiva dalla coda.La funzione successiva in coda è utiliseData () che va in loop ma a causa di nessun dato disponibile va lo spreco e l'esecuzione della funzione successiva continua fino alla fine della coda (si chiama chiamata asincrona, cioè possiamo fare qualcos'altro finché non otteniamo dati)
Supponiamo che la nostra funzione serverRequest () avesse un'istruzione return in un codice, quando recuperiamo i dati dall'API Web del server, li metterà in coda alla fine della coda. Quando viene inserito alla fine in coda, non possiamo utilizzare i suoi dati poiché non è rimasta alcuna funzione nella nostra coda per utilizzare questi dati. Pertanto non è possibile restituire qualcosa da Async Call.
Quindi la soluzione a questo è richiamata o promessa .
Un'immagine da una delle risposte qui, spiega correttamente l'uso della richiamata ... Diamo la nostra funzione (funzione che utilizza i dati restituiti dal server) alla funzione che chiama il server.
function doAjax(callbackFunc, method, url) {
var xmlHttpReq = new XMLHttpRequest();
xmlHttpReq.open(method, url);
xmlHttpReq.onreadystatechange = function() {
if (xmlHttpReq.readyState == 4 && xmlHttpReq.status == 200) {
callbackFunc(xmlHttpReq.responseText);
}
}
xmlHttpReq.send(null);
}
Nel mio codice si chiama come
function loadMyJson(categoryValue){
if(categoryValue==="veg")
doAjax(print,"GET","http://localhost:3004/vegetables");
else if(categoryValue==="fruits")
doAjax(print,"GET","http://localhost:3004/fruits");
else
console.log("Data not found");
}
Richiamata Javscript.info
È possibile utilizzare questa libreria personalizzata (scritta utilizzando Promise) per effettuare una chiamata remota.
function $http(apiConfig) {
return new Promise(function (resolve, reject) {
var client = new XMLHttpRequest();
client.open(apiConfig.method, apiConfig.url);
client.send();
client.onload = function () {
if (this.status >= 200 && this.status < 300) {
// Performs the function "resolve" when this.status is equal to 2xx.
// Your logic here.
resolve(this.response);
}
else {
// Performs the function "reject" when this.status is different than 2xx.
reject(this.statusText);
}
};
client.onerror = function () {
reject(this.statusText);
};
});
}
Esempio di utilizzo semplice:
$http({
method: 'get',
url: 'google.com'
}).then(function(response) {
console.log(response);
}, function(error) {
console.log(error)
});
Un'altra soluzione è eseguire il codice tramite l'esecutore sequenziale nsynjs .
Se la funzione sottostante è promessa
nsynjs valuterà tutte le promesse in sequenza e inserirà il risultato della promessa nella data
proprietà:
function synchronousCode() {
var getURL = function(url) {
return window.fetch(url).data.text().data;
};
var url = 'https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js';
console.log('received bytes:',getURL(url).length);
};
nsynjs.run(synchronousCode,{},function(){
console.log('synchronousCode done');
});
<script src="https://rawgit.com/amaksr/nsynjs/master/nsynjs.js"></script>
Se la funzione sottostante non è promessa
Passaggio 1. Avvolgi la funzione con callback nel wrapper compatibile con nsynjs (se ha una versione promessa, puoi saltare questo passaggio):
var ajaxGet = function (ctx,url) {
var res = {};
var ex;
$.ajax(url)
.done(function (data) {
res.data = data;
})
.fail(function(e) {
ex = e;
})
.always(function() {
ctx.resume(ex);
});
return res;
};
ajaxGet.nsynjsHasCallback = true;
Passaggio 2. Metti in funzione la logica sincrona:
function process() {
console.log('got data:', ajaxGet(nsynjsCtx, "data/file1.json").data);
}
Passaggio 3. Esegui la funzione in modo sincrono tramite nsynjs:
nsynjs.run(process,this,function () {
console.log("synchronous function finished");
});
Nsynjs valuterà tutti gli operatori e le espressioni passo dopo passo, sospendendo l'esecuzione nel caso in cui il risultato di qualche funzione lenta non sia pronto.
Altri esempi qui: https://github.com/amaksr/nsynjs/tree/master/examples
ECMAScript 6 dispone di "generatori" che consentono di programmare facilmente in uno stile asincrono.
function* myGenerator() {
const callback = yield;
let [response] = yield $.ajax("https://stackoverflow.com", {complete: callback});
console.log("response is:", response);
// examples of other things you can do
yield setTimeout(callback, 1000);
console.log("it delayed for 1000ms");
while (response.statusText === "error") {
[response] = yield* anotherGenerator();
}
}
Per eseguire il codice sopra, fai questo:
const gen = myGenerator(); // Create generator
gen.next(); // Start it
gen.next((...args) => gen.next([...args])); // Set its callback function
Se è necessario scegliere come target browser che non supportano ES6, è possibile eseguire il codice tramite Babel o il compilatore di chiusura per generare ECMAScript 5.
I callback ...args
sono racchiusi in una matrice e destrutturati quando vengono letti in modo che il pattern possa far fronte ai callback che hanno più argomenti. Ad esempio con node fs :
const [err, data] = yield fs.readFile(filePath, "utf-8", callback);
Di seguito sono riportati alcuni approcci per lavorare con le richieste asincrone:
- Oggetto Promessa del browser
- D - Una libreria di promesse per JavaScript
- A + Promises.js
- jQuery differita
- API XMLHttpRequest
- Utilizzo del concetto di callback - Come implementazione nella prima risposta
Esempio: implementazione differita di jQuery per lavorare con più richieste
var App = App || {};
App = {
getDataFromServer: function(){
var self = this,
deferred = $.Deferred(), requests = []; requests.push($.getJSON('request/ajax/url/1'));
requests.push($.getJSON('request/ajax/url/2')); $.when.apply(jQuery, requests).done(function(xhrResponse) {
return deferred.resolve(xhrResponse.result);
});
return deferred;
},
init: function(){
this.getDataFromServer().done(_.bind(function(resp1, resp2) {
// Do the operations which you wanted to do when you
// get a response from Ajax, for example, log response.
}, this));
}
};
App.init();
Ci troviamo in un universo che sembra progredire lungo una dimensione che chiamiamo "tempo". Non capiamo veramente che cosa sia il tempo, ma abbiamo sviluppato astrazioni e vocaboli che ci permettono di ragionare e di parlarne: "passato", "presente", "futuro", "prima", "dopo".
I sistemi informatici che costruiamo - sempre di più - hanno il tempo come una dimensione importante. Certe cose sono programmate per accadere in futuro. Poi altre cose devono accadere dopo che quelle prime cose alla fine si verificano. Questa è la nozione di base chiamata "asincronicità". Nel nostro mondo sempre più connesso in rete, il caso più comune di asincronicità è l'attesa che qualche sistema remoto risponda a qualche richiesta.
Considera un esempio. Chiami il lattaio e ordini del latte. Quando arriva, lo vuoi mettere nel tuo caffè. Non puoi mettere il latte nel tuo caffè in questo momento, perché non è ancora qui. Devi aspettare che arrivi prima di metterlo nel tuo caffè. In altre parole, quanto segue non funzionerà:
var milk = order_milk();
put_in_coffee(milk);
Perché JS non ha modo di sapere che ha bisogno di aspettare per order_milk
alla fine prima di eseguire put_in_coffee
. In altre parole, non sa che order_milk
è asincrono, è qualcosa che non produrrà latte fino a qualche tempo futuro. JS e altri linguaggi dichiarativi eseguono un'istruzione dopo l'altra senza attendere.
Il classico approccio JS a questo problema, sfruttando il fatto che JS supporta funzioni come oggetti di prima classe che possono essere passati in giro, consiste nel passare una funzione come parametro alla richiesta asincrona, che verrà poi richiamata quando sarà completata il suo compito in futuro. Questo è l'approccio "callback". Assomiglia a questo:
order_milk(put_in_coffee);
order_milk
prende il via, ordina il latte, poi, quando e solo quando arriva, lo invoca put_in_coffee
.
Il problema con questo approccio di callback è che inquina la normale semantica di una funzione che riporta il suo risultato con return
; invece, le funzioni non devono riportare i loro risultati chiamando un callback dato come parametro. Inoltre, questo approccio può diventare rapidamente ingombrante quando si tratta di sequenze di eventi più lunghe. Ad esempio, diciamo che voglio aspettare che il latte venga messo nel caffè, e poi e solo allora eseguo un terzo passaggio, ovvero bere il caffè. Finisco per aver bisogno di scrivere qualcosa del genere:
order_milk(function(milk) { put_in_coffee(milk, drink_coffee); }
dove passo put_in_coffee
sia al milk per inserirlo, sia all'action ( drink_coffee
) da eseguire una volta che il milk è stato inserito. Tale codice diventa difficile da scrivere, leggere ed eseguire il debug.
In questo caso, potremmo riscrivere il codice nella domanda come:
var answer;
$.ajax('/foo.json') . done(function(response) {
callback(response.data);
});
function callback(data) {
console.log(data);
}
Inserisci le promesse
Questa era la motivazione per la nozione di "promessa", che è un particolare tipo di valore che rappresenta un risultato futuro o asincrono di qualche tipo. Può rappresentare qualcosa che è già accaduto, o che accadrà in futuro, o potrebbe non accadere mai. Le promesse hanno un unico metodo, denominato then
, al quale si passa un'azione da eseguire quando il risultato che la promessa rappresenta è stato realizzato.
Nel caso del nostro latte e caffè, progettiamo order_milk
di restituire una promessa per il latte in arrivo, quindi specificare put_in_coffee
come then
azione, come segue:
order_milk() . then(put_in_coffee)
Un vantaggio di questo è che possiamo metterli insieme per creare sequenze di eventi futuri ("concatenamento"):
order_milk() . then(put_in_coffee) . then(drink_coffee)
Applichiamo le promesse al tuo problema particolare. Avvolgeremo la nostra logica di richiesta all'interno di una funzione, che restituisce una promessa:
function get_data() {
return $.ajax('/foo.json');
}
In realtà, tutto ciò che abbiamo fatto è stato aggiunto return
a alla chiamata a $.ajax
. Funziona perché jQuery $.ajax
restituisce già una sorta di cosa simile a una promessa. (In pratica, senza entrare nei dettagli, preferiremmo avvolgere questa chiamata in modo da restituire una vera promessa, o utilizzare qualche alternativa a $.ajax
quella lo fa.) Ora, se vogliamo caricare il file e aspettare che finisca e poi fai qualcosa, possiamo semplicemente dire
get_data() . then(do_something)
per esempio,
get_data() .
then(function(data) { console.log(data); });
Quando si usano le promesse, finiamo per passare molte funzioni then
, quindi spesso è utile utilizzare le funzioni freccia più compatte in stile ES6:
get_data() .
then(data => console.log(data));
La async
parola chiave
Ma c'è ancora qualcosa di vagamente insoddisfacente nel dover scrivere codice in un modo se sincrono e in un modo completamente diverso se asincrono. Per sincrono, scriviamo
a();
b();
ma se a
è asincrono, con le promesse dobbiamo scrivere
a() . then(b);
Sopra, abbiamo detto, "JS non ha modo di sapere che deve aspettare che la prima chiamata finisca prima di eseguire la seconda". Non sarebbe bello se ci fosse un modo per dirlo a JS? Si scopre che c'è - la await
parola chiave, usata all'interno di un tipo speciale di funzione chiamata funzione "asincrona". Questa funzione fa parte della prossima versione di ES ma è già disponibile nei transpiler come Babel con i giusti preset. Questo ci permette di scrivere semplicemente
async function morning_routine() {
var milk = await order_milk();
var coffee = await put_in_coffee(milk);
await drink(coffee);
}
Nel tuo caso, potresti scrivere qualcosa di simile
async function foo() {
data = await get_data();
console.log(data);
}
Risposta breve : il foo()
metodo viene restituito immediatamente, mentre la $ajax()
chiamata viene eseguita in modo asincrono dopo il ritorno della funzione . Il problema è quindi come o dove memorizzare i risultati recuperati dalla chiamata asincrona una volta restituita.
Diverse soluzioni sono state fornite in questo thread. Forse il modo più semplice è passare un oggetto al foo()
metodo e memorizzare i risultati in un membro di quell'oggetto dopo il completamento della chiamata asincrona.
function foo(result) {
$.ajax({
url: '...',
success: function(response) {
result.response = response; // Store the async result
}
});
}
var result = { response: null }; // Object to hold the async result
foo(result); // Returns before the async completes
Notare che la chiamata a foo()
non restituirà comunque nulla di utile. Tuttavia, il risultato della chiamata asincrona verrà ora archiviato in result.response
.
Usa una callback()
funzione all'interno del foo()
successo. Prova in questo modo. È semplice e facile da capire.
var lat = "";
var lon = "";
function callback(data) {
lat = data.lat;
lon = data.lon;
}
function getLoc() {
var url = "http://ip-api.com/json"
$.getJSON(url, function(data) {
callback(data);
});
}
getLoc();
Utilizzando Promise
La risposta più perfetta a questa domanda sta usando Promise
.
function ajax(method, url, params) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.onload = function() {
resolve(this.responseText);
};
xhr.onerror = reject;
xhr.open(method, url);
xhr.send(params);
});
}
Utilizzo
ajax("GET", "/test", "acrive=1").then(function(result) {
// Code depending on result
})
.catch(function() {
// An error occurred
});
Ma aspetta...!
C'è un problema con l'utilizzo delle promesse!
Perché dovremmo usare la nostra Promessa personalizzata?
Stavo usando questa soluzione per un po 'finché non ho capito che c'era un errore nei vecchi browser:
Uncaught ReferenceError: Promise is not defined
Quindi ho deciso di implementare la mia classe Promise per ES3 sotto i compilatori js se non è definito. Basta aggiungere questo codice prima del codice principale e quindi utilizzare in sicurezza Promise!
if(typeof Promise === "undefined"){
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Promise = function () {
function Promise(main) {
var _this = this;
_classCallCheck(this, Promise);
this.value = undefined;
this.callbacks = [];
var resolve = function resolve(resolveValue) {
_this.value = resolveValue;
_this.triggerCallbacks();
};
var reject = function reject(rejectValue) {
_this.value = rejectValue;
_this.triggerCallbacks();
};
main(resolve, reject);
}
Promise.prototype.then = function then(cb) {
var _this2 = this;
var next = new Promise(function (resolve) {
_this2.callbacks.push(function (x) {
return resolve(cb(x));
});
});
return next;
};
Promise.prototype.catch = function catch_(cb) {
var _this2 = this;
var next = new Promise(function (reject) {
_this2.callbacks.push(function (x) {
return reject(cb(x));
});
});
return next;
};
Promise.prototype.triggerCallbacks = function triggerCallbacks() {
var _this3 = this;
this.callbacks.forEach(function (cb) {
cb(_this3.value);
});
};
return Promise;
}();
}
La domanda era:
Come restituisco la risposta da una chiamata asincrona?
che può essere interpretato come:
Come rendere sincrono il codice asincrono ?
La soluzione sarà evitare i callback e utilizzare una combinazione di Promises e async / await .
Vorrei fare un esempio per una richiesta Ajax.
(Sebbene possa essere scritto in Javascript, preferisco scriverlo in Python e compilarlo in Javascript usando Transcrypt . Sarà abbastanza chiaro.)
Abilita prima l'utilizzo di JQuery, per avere a $
disposizione come S
:
__pragma__ ('alias', 'S', '$')
Definisci una funzione che restituisca una promessa , in questo caso una chiamata Ajax:
def read(url: str):
deferred = S.Deferred()
S.ajax({'type': "POST", 'url': url, 'data': { },
'success': lambda d: deferred.resolve(d),
'error': lambda e: deferred.reject(e)
})
return deferred.promise()
Usa il codice asincrono come se fosse sincrono :
async def readALot():
try:
result1 = await read("url_1")
result2 = await read("url_2")
except Exception:
console.warn("Reading a lot failed")
Ovviamente ci sono molti approcci come la richiesta sincrona, la promessa, ma dalla mia esperienza penso che dovresti usare l'approccio di richiamata. È naturale il comportamento asincrono di Javascript. Quindi, il tuo frammento di codice può essere riscritto in modo leggermente diverso:
function foo() {
var result;
$.ajax({
url: '...',
success: function(response) {
myCallback(response);
}
});
return result;
}
function myCallback(response) {
// Does something.
}
Dopo aver letto tutte le risposte qui e con le mie esperienze, vorrei riprendere i dettagli di callback, promise and async/await
per la programmazione asincrona in JavaScript.
1) Callback: il motivo fondamentale per un callback è l'esecuzione di codice in risposta a un evento (vedere l'esempio seguente). Usiamo la richiamata in JavaScript ogni volta.
const body = document.getElementsByTagName('body')[0];
function callback() {
console.log('Hello');
}
body.addEventListener('click', callback);
Ma se è necessario utilizzare molti callback annidati nell'esempio seguente, sarà terribile per il refactoring del codice.
asyncCallOne(function callback1() {
asyncCallTwo(function callback2() {
asyncCallThree(function callback3() {
...
})
})
})
2) Promise: una sintassi ES6 - Promise risolve il problema dell'inferno di callback!
const myFirstPromise = new Promise((resolve, reject) => {
// We call resolve(...) when what we were doing asynchronously was successful, and reject(...) when it failed.
// In this example, we use setTimeout(...) to simulate async code.
// In reality, you will probably be using something like XHR request or an HTML5 API.
setTimeout(() => {
resolve("Success!") // Yay! Everything went well!
}, 250)
})
myFirstPromise
.then((res) => {
return res.json();
})
.then((data) => {
console.log(data);
})
.catch((e) => {
console.log(e);
});
myFirstPromise è un'istanza di Promise che rappresenta il processo di codici asincroni. La funzione di risoluzione segnala che l'istanza di Promise è terminata. Successivamente, possiamo chiamare .then () (una catena di .then come vuoi) e .catch () sull'istanza della promessa:
then — Runs a callback you pass to it when the promise has fulfilled.
catch — Runs a callback you pass to it when something went wrong.
3) Async / Await: una nuova sintassi ES6 - Await è fondamentalmente la sintassi zuccherina di Promise!
La funzione asincrona ci fornisce una sintassi pulita e concisa che ci consente di scrivere meno codice per ottenere lo stesso risultato che otterremmo con le promesse. Async / Await è simile al codice sincrono e il codice sincrono è molto più facile da leggere e scrivere. Per rilevare errori con Async / Await, possiamo usare il blocco try...catch
. Qui non è necessario scrivere una catena di .then () di sintassi Promise.
const getExchangeRate = async () => {
try {
const res = await fetch('https://getExchangeRateData');
const data = await res.json();
console.log(data);
} catch (err) {
console.error(err);
}
}
getExchangeRate();
Conclusione: queste sono totalmente le tre sintassi per la programmazione asincrona in JavaScript che dovresti capire bene. Quindi, se possibile, ti consiglio di usare "promise" o "async / await" per il refactoring dei tuoi codici asincroni (principalmente per richieste XHR) !
Piuttosto che lanciarti del codice, ci sono 2 concetti che sono fondamentali per capire come JS gestisce i callback e l'asincronicità. (è anche solo una parola?)
Il ciclo di eventi e il modello di concorrenza
Ci sono tre cose di cui devi essere consapevole; La fila; il ciclo di eventi e lo stack
In termini ampi e semplicistici, il ciclo di eventi è come il project manager, è in costante ascolto di tutte le funzioni che vogliono eseguire e comunica tra la coda e lo stack.
while (queue.waitForMessage()) {
queue.processNextMessage();
}
Una volta ricevuto un messaggio per eseguire qualcosa, lo aggiunge alla coda. La coda è l'elenco delle cose che sono in attesa di essere eseguite (come la tua richiesta AJAX). immaginalo in questo modo:
1. call foo.com/api/bar using foobarFunc
2. Go perform an infinite loop
... and so on
Quando uno di questi messaggi sta per essere eseguito, estrae il messaggio dalla coda e crea uno stack, lo stack è tutto ciò che JS deve eseguire per eseguire l'istruzione nel messaggio. Quindi nel nostro esempio gli viene detto di chiamarefoobarFunc
function foobarFunc (var) {
console.log(anotherFunction(var));
}
Quindi tutto ciò che foobarFunc deve eseguire (nel nostro caso anotherFunction
) verrà inserito nello stack. eseguito, e poi dimenticato - il ciclo di eventi si sposterà quindi sulla prossima cosa nella coda (o ascolterà i messaggi)
La cosa fondamentale qui è l'ordine di esecuzione. Questo è
QUANDO sta per funzionare qualcosa
Quando effettui una chiamata utilizzando AJAX a una parte esterna o esegui un codice asincrono (un setTimeout per esempio), Javascript dipende da una risposta prima di poter procedere.
La grande domanda è quando riceverà la risposta? La risposta è che non lo sappiamo, quindi il ciclo di eventi attende che il messaggio dica "hey run me". Se JS aspettasse solo quel messaggio in modo sincrono, la tua app si bloccherebbe e farà schifo. Quindi JS continua a eseguire l'elemento successivo nella coda in attesa che il messaggio venga aggiunto di nuovo alla coda.
Ecco perché con la funzionalità asincrona usiamo cose chiamate callback . È un po 'come una promessa letteralmente. Come in Prometto di restituire qualcosa ad un certo punto jQuery utilizza callback specifici chiamati deffered.done
deffered.fail
e deffered.always
(tra gli altri). Puoi vederli tutti qui
Quindi quello che devi fare è passare una funzione che è promessa di essere eseguita a un certo punto con i dati che le vengono passati.
Perché un callback non viene eseguito immediatamente ma in un secondo momento è importante passare il riferimento alla funzione non eseguita. così
function foo(bla) {
console.log(bla)
}
quindi la maggior parte del tempo (ma non sempre) si passa foo
nonfoo()
Si spera che abbia un senso. Quando incontri cose come questa che sembrano confuse, consiglio vivamente di leggere completamente la documentazione per comprenderla almeno. Ti renderà uno sviluppatore molto migliore.