Perché la mia variabile rimane inalterata dopo averla modificata all'interno di una funzione? - Riferimento al codice asincrono
Dati i seguenti esempi, perché è outerScopeVar
indefinito in tutti i casi?
var outerScopeVar;
var img = document.createElement('img');
img.onload = function() {
outerScopeVar = this.width;
};
img.src = 'lolcat.png';
alert(outerScopeVar);
var outerScopeVar;
setTimeout(function() {
outerScopeVar = 'Hello Asynchronous World!';
}, 0);
alert(outerScopeVar);
// Example using some jQuery
var outerScopeVar;
$.post('loldog', function(response) {
outerScopeVar = response;
});
alert(outerScopeVar);
// Node.js example
var outerScopeVar;
fs.readFile('./catdog.html', function(err, data) {
outerScopeVar = data;
});
console.log(outerScopeVar);
// with promises
var outerScopeVar;
myPromise.then(function (response) {
outerScopeVar = response;
});
console.log(outerScopeVar);
// geolocation API
var outerScopeVar;
navigator.geolocation.getCurrentPosition(function (pos) {
outerScopeVar = pos;
});
console.log(outerScopeVar);
Perché viene visualizzato undefined
in tutti questi esempi? Non voglio soluzioni alternative, voglio sapere perché sta accadendo.
Nota: questa è una domanda canonica per l' asincronicità di JavaScript . Sentiti libero di migliorare questa domanda e aggiungere esempi più semplificati con cui la comunità può identificarsi.
Risposte
Risposta in una parola: asincronicità .
Prefazioni
Questo argomento è stato ripetuto almeno un paio di migliaia di volte, qui, in Stack Overflow. Quindi, prima di tutto vorrei segnalare alcune risorse estremamente utili:
Risposta di @Felix Kling a "Come restituisco la risposta da una chiamata asincrona?" . Vedi la sua eccellente risposta che spiega i flussi sincroni e asincroni, nonché la sezione "Ristruttura il codice".
Anche @Benjamin Gruenbaum si è impegnato molto per spiegare l'asincronicità nello stesso thread.La risposta di @Matt Esch a "Ottieni dati da fs.readFile" spiega anche l'asincronicità estremamente bene in modo semplice.
La risposta alla domanda in questione
Tracciamo prima il comportamento comune. In tutti gli esempi, outerScopeVar
viene modificato all'interno di una funzione . Quella funzione chiaramente non viene eseguita immediatamente, viene assegnata o passata come argomento. Questo è ciò che chiamiamo callback .
La domanda è: quando viene chiamata la richiamata?
Dipende dal caso. Proviamo di nuovo a tracciare un comportamento comune:
img.onload
potrebbe essere richiamato in futuro , quando (e se) l'immagine è stata caricata correttamente.setTimeout
può essere chiamato in futuro , dopo che il ritardo è scaduto e il timeout non è stato annullato daclearTimeout
. Nota: anche quando si utilizza0
come ritardo, tutti i browser hanno un limite di ritardo di timeout minimo (specificato per essere 4 ms nelle specifiche HTML5).$.post
La richiamata di jQuery potrebbe essere chiamata in futuro , quando (e se) la richiesta Ajax sarà stata completata con successo.- Node.js
fs.readFile
potrebbe essere chiamato in futuro , quando il file è stato letto con successo o ha generato un errore.
In tutti i casi, abbiamo una richiamata che potrebbe essere eseguita in futuro . Questo "in futuro" è ciò che chiamiamo flusso asincrono .
L'esecuzione asincrona viene espulsa dal flusso sincrono. Ovvero, il codice asincrono non verrà mai eseguito durante l'esecuzione dello stack di codice sincrono. Questo è il significato di JavaScript a thread singolo.
Più specificamente, quando il motore JS è inattivo, non eseguendo uno stack di (a) codice sincrono, eseguirà il polling per eventi che potrebbero aver attivato callback asincroni (ad esempio timeout scaduto, risposta di rete ricevuta) e li eseguirà uno dopo l'altro. Questo è considerato come Event Loop .
Cioè, il codice asincrono evidenziato nelle forme rosse disegnate a mano può essere eseguito solo dopo che tutto il codice sincrono rimanente nei rispettivi blocchi di codice è stato eseguito:
In breve, le funzioni di callback vengono create in modo sincrono ma eseguite in modo asincrono. Non puoi fare affidamento sull'esecuzione di una funzione asincrona finché non sai che è stata eseguita, e come farlo?
È semplice, davvero. La logica che dipende dall'esecuzione della funzione asincrona dovrebbe essere avviata / chiamata dall'interno di questa funzione asincrona. Ad esempio, spostando anche alert
s ed console.log
s all'interno della funzione di callback si produrrebbe il risultato atteso, perché il risultato è disponibile a quel punto.
Implementazione della propria logica di callback
Spesso è necessario fare più cose con il risultato di una funzione asincrona o fare cose diverse con il risultato a seconda di dove è stata chiamata la funzione asincrona. Affrontiamo un esempio un po 'più complesso:
var outerScopeVar;
helloCatAsync();
alert(outerScopeVar);
function helloCatAsync() {
setTimeout(function() {
outerScopeVar = 'Nya';
}, Math.random() * 2000);
}
Nota: sto usando setTimeout
con un ritardo casuale come funzione asincrona generica, lo stesso esempio si applica ad Ajax readFile
, onload
e qualsiasi altro flusso asincrono.
Questo esempio soffre chiaramente dello stesso problema degli altri esempi, non è in attesa che venga eseguita la funzione asincrona.
Affrontiamolo implementando un nostro sistema di callback. Prima di tutto, ci liberiamo di quel brutto outerScopeVar
che è completamente inutile in questo caso. Quindi aggiungiamo un parametro che accetta un argomento di funzione, il nostro callback. Al termine dell'operazione asincrona, chiamiamo questo callback passando il risultato. L'implementazione (si prega di leggere i commenti in ordine):
// 1. Call helloCatAsync passing a callback function,
// which will be called receiving the result from the async operation
helloCatAsync(function(result) {
// 5. Received the result from the async function,
// now do whatever you want with it:
alert(result);
});
// 2. The "callback" parameter is a reference to the function which
// was passed as argument from the helloCatAsync call
function helloCatAsync(callback) {
// 3. Start async operation:
setTimeout(function() {
// 4. Finished async operation,
// call the callback passing the result as argument
callback('Nya');
}, Math.random() * 2000);
}
Snippet di codice dell'esempio precedente:
// 1. Call helloCatAsync passing a callback function,
// which will be called receiving the result from the async operation
console.log("1. function called...")
helloCatAsync(function(result) {
// 5. Received the result from the async function,
// now do whatever you want with it:
console.log("5. result is: ", result);
});
// 2. The "callback" parameter is a reference to the function which
// was passed as argument from the helloCatAsync call
function helloCatAsync(callback) {
console.log("2. callback here is the function passed as argument above...")
// 3. Start async operation:
setTimeout(function() {
console.log("3. start async operation...")
console.log("4. finished async operation, calling the callback, passing the result...")
// 4. Finished async operation,
// call the callback passing the result as argument
callback('Nya');
}, Math.random() * 2000);
}
Molto spesso nei casi d'uso reali, l'API DOM e la maggior parte delle librerie forniscono già la funzionalità di callback (l' helloCatAsync
implementazione in questo esempio dimostrativo). Devi solo passare la funzione di callback e capire che verrà eseguita al di fuori del flusso sincrono e ristrutturare il tuo codice per adattarlo.
Noterai anche che a causa della natura asincrona, è impossibile return
riportare un valore da un flusso asincrono al flusso sincrono in cui è stato definito il callback, poiché i callback asincroni vengono eseguiti molto tempo dopo che il codice sincrono ha già terminato l'esecuzione.
Invece di return
inserire un valore da una richiamata asincrona, sarà necessario utilizzare il modello di richiamata o ... Promesse.
Promesse
Sebbene ci siano modi per tenere a bada l' inferno di richiamata con vanilla JS, le promesse stanno crescendo in popolarità e sono attualmente standardizzate in ES6 (vedere Promise - MDN ).
Le promesse (note anche come Futures) forniscono una lettura più lineare, e quindi piacevole, del codice asincrono, ma spiegare la loro intera funzionalità è fuori dallo scopo di questa domanda. Invece, lascio queste eccellenti risorse per gli interessati:
Altro materiale da leggere sull'asincronicità di JavaScript
- The Art of Node - Callbacks spiega molto bene il codice asincrono e le callback con esempi JS vanilla e anche il codice Node.js.
Nota: ho contrassegnato questa risposta come Community Wiki, quindi chiunque abbia almeno 100 reputazioni può modificarla e migliorarla! Sentiti libero di migliorare questa risposta o di inviare una risposta completamente nuova se lo desideri.
Voglio trasformare questa domanda in un argomento canonico per rispondere a problemi di asincronicità che non sono correlati ad Ajax (c'è Come restituire la risposta da una chiamata AJAX? Per quello), quindi questo argomento ha bisogno del tuo aiuto per essere il più buono e utile possibile !
La risposta di Fabrício è perfetta; ma volevo completare la sua risposta con qualcosa di meno tecnico, che si concentra su un'analogia per aiutare a spiegare il concetto di asincronicità .
Un'analogia ...
Ieri il lavoro che stavo facendo richiedeva alcune informazioni da un collega. L'ho chiamato; ecco come è andata la conversazione:
Io : Ciao Bob, ho bisogno di sapere come abbiamo trovato il bar la scorsa settimana. Jim vuole un rapporto al riguardo e tu sei l'unico a conoscerne i dettagli.
Bob : certo, ma mi ci vorranno circa 30 minuti?
Io : è fantastico Bob. Dammi un richiamo quando hai le informazioni!
A questo punto, ho riattaccato il telefono. Dal momento che avevo bisogno di informazioni da Bob per completare il mio rapporto, ho lasciato il rapporto e sono andato a prendere un caffè, poi ho recuperato qualche e-mail. 40 minuti dopo (Bob è lento), Bob ha richiamato e mi ha fornito le informazioni di cui avevo bisogno. A questo punto, ho ripreso il mio lavoro con la mia relazione, poiché avevo tutte le informazioni di cui avevo bisogno.
Immagina se invece la conversazione fosse andata così;
Io : Ciao Bob, ho bisogno di sapere come abbiamo trovato il bar la scorsa settimana. Jim vuole un rapporto al riguardo, e tu sei l'unico a conoscerne i dettagli.
Bob : certo, ma mi ci vorranno circa 30 minuti?
Io : è fantastico Bob. Aspetterò.
E mi sono seduto lì e ho aspettato. E ho aspettato. E ho aspettato. Per 40 minuti. Non fare altro che aspettare. Alla fine, Bob mi ha dato le informazioni, abbiamo riattaccato e ho completato il mio rapporto. Ma avevo perso 40 minuti di produttività.
Questo è un comportamento asincrono rispetto a quello sincrono
Questo è esattamente ciò che sta accadendo in tutti gli esempi nella nostra domanda. Il caricamento di un'immagine, il caricamento di un file dal disco e la richiesta di una pagina tramite AJAX sono tutte operazioni lente (nel contesto dell'informatica moderna).
Anziché attendere il completamento di queste operazioni lente, JavaScript consente di registrare una funzione di callback che verrà eseguita al termine dell'operazione lenta. Nel frattempo, tuttavia, JavaScript continuerà a eseguire altro codice. Il fatto che JavaScript esegua altro codice mentre attende il completamento dell'operazione lenta rende il comportamento asincrono . Se JavaScript avesse atteso il completamento dell'operazione prima di eseguire qualsiasi altro codice, si sarebbe trattato di un comportamento sincrono .
var outerScopeVar;
var img = document.createElement('img');
// Here we register the callback function.
img.onload = function() {
// Code within this function will be executed once the image has loaded.
outerScopeVar = this.width;
};
// But, while the image is loading, JavaScript continues executing, and
// processes the following lines of JavaScript.
img.src = 'lolcat.png';
alert(outerScopeVar);
Nel codice sopra, chiediamo di caricare JavaScript lolcat.png
, che è un'operazione sloooow . La funzione di callback verrà eseguita una volta completata questa lenta operazione, ma nel frattempo JavaScript continuerà a elaborare le successive righe di codice; cioè alert(outerScopeVar)
.
Questo è il motivo per cui vediamo la visualizzazione dell'avviso undefined
; poiché alert()
viene elaborato immediatamente, piuttosto che dopo che l'immagine è stata caricata.
Per correggere il nostro codice, tutto ciò che dobbiamo fare è spostare il alert(outerScopeVar)
codice nella funzione di callback. Di conseguenza, non abbiamo più bisogno della outerScopeVar
variabile dichiarata come variabile globale.
var img = document.createElement('img');
img.onload = function() {
var localScopeVar = this.width;
alert(localScopeVar);
};
img.src = 'lolcat.png';
Vedrai sempre che un callback è specificato come una funzione, perché è l'unico * modo in JavaScript per definire del codice, ma non eseguirlo fino a dopo.
Pertanto, in tutti i nostri esempi, function() { /* Do something */ }
è il callback; per correggere tutti gli esempi, tutto ciò che dobbiamo fare è spostare lì il codice che richiede la risposta dell'operazione!
* Tecnicamente puoi usare eval()
anche tu , ma eval()
è malvagio per questo scopo
Come faccio a mantenere il mio chiamante in attesa?
Al momento potresti avere un codice simile a questo;
function getWidthOfImage(src) {
var outerScopeVar;
var img = document.createElement('img');
img.onload = function() {
outerScopeVar = this.width;
};
img.src = src;
return outerScopeVar;
}
var width = getWidthOfImage('lolcat.png');
alert(width);
Tuttavia, ora sappiamo che ciò return outerScopeVar
accade immediatamente; prima che la onload
funzione di callback abbia aggiornato la variabile. Questo porta a getWidthOfImage()
tornare undefined
e ad undefined
essere allertato.
Per risolvere questo problema, dobbiamo consentire alla funzione che chiama getWidthOfImage()
di registrare un callback, quindi spostare l'avviso della larghezza in modo che sia all'interno di quel callback;
function getWidthOfImage(src, cb) {
var img = document.createElement('img');
img.onload = function() {
cb(this.width);
};
img.src = src;
}
getWidthOfImage('lolcat.png', function (width) {
alert(width);
});
... come prima, nota che siamo stati in grado di rimuovere le variabili globali (in questo caso width
).
Ecco una risposta più concisa per le persone che cercano un riferimento rapido e alcuni esempi che utilizzano promesse e async / await.
Inizia con l'approccio ingenuo (che non funziona) per una funzione che chiama un metodo asincrono (in questo caso setTimeout
) e restituisce un messaggio:
function getMessage() {
var outerScopeVar;
setTimeout(function() {
outerScopeVar = 'Hello asynchronous world!';
}, 0);
return outerScopeVar;
}
console.log(getMessage());
undefined
viene registrato in questo caso perché getMessage
ritorna prima che setTimeout
venga chiamata la richiamata e si aggiorna outerScopeVar
.
I due modi principali per risolverlo sono l'utilizzo di callback e promesse :
Richiami
La modifica qui è che getMessage
accetta un callback
parametro che verrà chiamato per restituire i risultati al codice chiamante una volta disponibile.
function getMessage(callback) {
setTimeout(function() {
callback('Hello asynchronous world!');
}, 0);
}
getMessage(function(message) {
console.log(message);
});
Le promesse forniscono un'alternativa più flessibile rispetto ai callback perché possono essere combinate naturalmente per coordinare più operazioni asincrone. A Promesse / A + implementazione standard viene fornito in modo nativo node.js (0.12+) e molti browser attuali, ma è implementata anche nelle biblioteche come la Bluebird e Q .
function getMessage() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Hello asynchronous world!');
}, 0);
});
}
getMessage().then(function(message) {
console.log(message);
});
jQuery Deferreds
jQuery fornisce funzionalità simili alle promesse con i suoi Deferred.
function getMessage() {
var deferred = $.Deferred();
setTimeout(function() {
deferred.resolve('Hello asynchronous world!');
}, 0);
return deferred.promise();
}
getMessage().done(function(message) {
console.log(message);
});
asincrono / attendono
Se il tuo ambiente JavaScript include il supporto per async
e await
(come Node.js 7.6+), puoi utilizzare le promesse in modo sincrono all'interno delle async
funzioni:
function getMessage () {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Hello asynchronous world!');
}, 0);
});
}
async function main() {
let message = await getMessage();
console.log(message);
}
main();
Per affermare l'ovvio, la tazza rappresenta outerScopeVar
.
Le funzioni asincrone sono come ...
Le altre risposte sono eccellenti e voglio solo fornire una risposta diretta a questo. Limitando solo alle chiamate asincrone jQuery
Tutte le chiamate ajax (incluso $.get
or $.post
or $.ajax
) sono asincrone.
Considerando il tuo esempio
var outerScopeVar; //line 1
$.post('loldog', function(response) { //line 2
outerScopeVar = response;
});
alert(outerScopeVar); //line 3
L'esecuzione del codice inizia dalla riga 1, dichiara la variabile e innesca e la chiamata asincrona sulla riga 2, (cioè la richiesta di post) e continua la sua esecuzione dalla riga 3, senza attendere che la richiesta di post completi la sua esecuzione.
Diciamo che la richiesta di post richiede 10 secondi per essere completata, il valore di outerScopeVar
verrà impostato solo dopo quei 10 secondi.
Da provare
var outerScopeVar; //line 1
$.post('loldog', function(response) { //line 2, takes 10 seconds to complete
outerScopeVar = response;
});
alert("Lets wait for some time here! Waiting is fun"); //line 3
alert(outerScopeVar); //line 4
Ora, quando lo esegui, riceverai un avviso sulla riga 3. Attendi un po 'di tempo finché non sei sicuro che la richiesta di post ha restituito un valore. Quindi, quando si fa clic su OK, nella casella di avviso, l'avviso successivo stamperà il valore previsto, perché lo si aspettava.
Nello scenario di vita reale, il codice diventa,
var outerScopeVar;
$.post('loldog', function(response) {
outerScopeVar = response;
alert(outerScopeVar);
});
Tutto il codice che dipende dalle chiamate asincrone, viene spostato all'interno del blocco asincrono, o in attesa delle chiamate asincrone.
In tutti questi scenari outerScopeVar
viene modificato o assegnato un valore in modo asincrono o che accade in un secondo momento (in attesa o in ascolto del verificarsi di qualche evento), per il quale l'esecuzione corrente non attenderà . Quindi tutti questi casi il flusso di esecuzione corrente risulta inouterScopeVar = undefined
Discutiamo ogni esempio (ho contrassegnato la parte che viene chiamata in modo asincrono o ritardata per il verificarsi di alcuni eventi):
1.
Qui registriamo un eventlistner che verrà eseguito su quel particolare evento. Qui viene caricata l'immagine. Quindi l'esecuzione corrente continua con le righe successive img.src = 'lolcat.png';
e alert(outerScopeVar);
nel frattempo l'evento potrebbe non verificarsi. cioè, la funzione img.onload
attende il caricamento dell'immagine di riferimento, in modo asincrono. Questo accadrà nel seguente esempio: l'evento potrebbe essere diverso.
2.
Qui l'evento timeout gioca il ruolo, che richiamerà il gestore dopo il tempo specificato. Eccolo 0
, ma ancora registra un evento asincrono che verrà aggiunto all'ultima posizione di Event Queue
for execution, il che rende il ritardo garantito.
3.
4.
Il nodo può essere considerato come un re della codifica asincrona.Qui la funzione contrassegnata è registrata come un gestore di callback che verrà eseguito dopo aver letto il file specificato.
5.
La promessa ovvia (qualcosa sarà fatto in futuro) è asincrona. vedi Quali sono le differenze tra Deferred, Promise e Future in JavaScript?
https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript