Perché la mia variabile è inalterata dopo averla modificata all'interno di una funzione? - Riferimento asincrono al codice


669

Dati i seguenti esempi, perché outerScopeVarnon è definito 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é esce undefinedin tutti questi esempi? Non voglio soluzioni alternative, voglio sapere perché questo 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 community può identificarsi.



@Dukeling grazie, sono abbastanza sicuro di aver commentato quel link ma apparentemente ci sono alcuni commenti mancanti. Inoltre, per quanto riguarda la tua modifica: credo che avere "canonico" e "asincronicità" nel titolo sia utile quando cerchi questa domanda per contrassegnare un'altra domanda come un duplicato. E, naturalmente, aiuta anche a trovare questa domanda da Google quando si cercano spiegazioni di asincronicità.
Fabrício Matté,

3
Mettendo un po 'più di pensiero, "argomento canonico di asincronicità" è un po' pesante sul titolo, "riferimento al codice asincrono" è più semplice e più obiettivo. Credo anche che la maggior parte delle persone cerchi "asincrono" invece di "asincronicità".
Fabrício Matté,

1
Alcune persone inizializzano la propria variabile prima della chiamata della funzione. Che ne dici di cambiare il titolo che in qualche modo rappresenta anche quello? Come "Perché la mia variabile è inalterata dopo averla modificata all'interno di una funzione?" ?
Felix Kling

In tutti gli esempi di codice che hai menzionato sopra, "alert (outerScopeVar);" viene eseguito ADESSO, mentre l'assegnazione del valore a "outerScopeVar" avviene in seguito (in modo asincrono).
refactor

Risposte:


542

Una sola risposta: asincronicità .

prefazioni

Questo argomento è stato ripetuto almeno un paio di migliaia di volte, qui, in Stack Overflow. Quindi, prima di tutto vorrei sottolineare alcune risorse estremamente utili:


La risposta alla domanda attuale

Tracciamo prima il comportamento comune. In tutti gli esempi, outerScopeVarviene modificato all'interno di una funzione . Quella funzione chiaramente non viene eseguita immediatamente, viene assegnata o passata come argomento. Questo è ciò che chiamiamo callback .

Ora la domanda è: quando viene chiamato quel callback?

Dipende dal caso. Proviamo di nuovo a tracciare alcuni comportamenti comuni:

  • img.onloadpotrebbe essere chiamato in futuro , quando (e se) l'immagine è stata caricata correttamente.
  • setTimeoutpotrebbe essere chiamato in futuro , dopo che il ritardo è scaduto e il timeout non è stato annullato da clearTimeout. Nota: anche quando si utilizza 0come ritardo, tutti i browser hanno un limite di ritardo di timeout minimo (specificato come 4ms nelle specifiche HTML5).
  • $.postIl callback di jQuery potrebbe essere chiamato in futuro , quando (e se) la richiesta Ajax è stata completata correttamente.
  • Node.js fs.readFilepotrebbe essere chiamato in futuro , quando il file è stato letto correttamente o generato un errore.

In tutti i casi, abbiamo un callback che potrebbe essere eseguito in futuro . Questo "in futuro" è ciò che chiamiamo flusso asincrono .

L'esecuzione asincrona viene espulsa dal flusso sincrono. Cioè, il codice asincrono non verrà mai eseguito mentre lo stack di codice sincrono è in esecuzione. 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) ed eseguirli 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 l'esecuzione di tutto il codice sincrono rimanente nei rispettivi blocchi di codice:

codice asincrono evidenziato

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 fino a quando non sai che è stata eseguita, e come farlo?

È semplice, davvero. La logica che dipende dall'esecuzione della funzione asincrona deve essere avviata / chiamata dall'interno di questa funzione asincrona. Ad esempio, spostare anche la alerts e la console.logs all'interno della funzione di richiamata produrrebbe il risultato atteso, poiché il risultato è disponibile in quel punto.

Implementazione della propria logica di richiamata

Spesso è necessario fare più cose con il risultato da una funzione asincrona o fare cose diverse con il risultato a seconda di dove è stata chiamata la funzione asincrona. Facciamo un esempio un po 'più complesso:

var outerScopeVar;
helloCatAsync();
alert(outerScopeVar);

function helloCatAsync() {
    setTimeout(function() {
        outerScopeVar = 'Nya';
    }, Math.random() * 2000);
}

Nota: sto usando setTimeoutcon un ritardo casuale in funzione asincrona generica, lo stesso esempio vale per l'Ajax, readFile, onloade qualsiasi altro flusso asincrono.

Questo esempio soffre chiaramente dello stesso problema degli altri esempi, non è in attesa dell'esecuzione della funzione asincrona.

Affrontiamolo implementando un nostro sistema di callback. Prima di tutto, ci liberiamo di quel brutto outerScopeVarche è 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 che passa il risultato. L'implementazione (leggi 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 in casi d'uso reali, l'API DOM e la maggior parte delle librerie forniscono già la funzionalità di callback (l' helloCatAsyncimplementazione in questo esempio dimostrativo). Devi solo passare la funzione di callback e capire che verrà eseguita dal flusso sincrono e ristrutturare il tuo codice per adattarlo.

Noterai anche che a causa della natura asincrona, è impossibile ottenere returnun 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 returninserire un valore da un callback asincrono, dovrai usare il modello di callback, o ... Promesse.

promesse

Sebbene ci siano modi per tenere a bada l' inferno del callback con JS vaniglia, le promesse stanno crescendo in popolarità e sono attualmente standardizzate in ES6 (vedi Promise - MDN ).

Le promesse (alias Futures) forniscono una lettura più lineare e quindi piacevole del codice asincrono, ma spiegare la loro intera funzionalità non rientra nell'ambito di questa domanda. Lascerò invece queste eccellenti risorse agli interessati:


Altro materiale di lettura sull'asincronicità di JavaScript


Nota: ho contrassegnato questa risposta come Community Wiki, quindi chiunque abbia almeno 100 reputazioni può modificarlo e migliorarlo! 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à non correlati ad Ajax (c'è Come restituire la risposta da una chiamata AJAX? Per questo), quindi questo argomento ha bisogno del tuo aiuto per essere il più buono e utile possibile !


1
Nel tuo ultimo esempio, c'è un motivo specifico per cui usi funzioni anonime o funzionerebbe allo stesso modo usando funzioni nominate?
JDelage,

1
Gli esempi di codice sono un po 'strani dato che stai dichiarando la funzione dopo averla chiamata. Funziona a causa del sollevamento ovviamente, ma era intenzionale?
Bergi,

2
è un punto morto? felix kling sta indicando la tua risposta e tu stai indicando la risposta di felix
Mahi

1
Devi capire che il codice del cerchio rosso è asincrono solo perché viene eseguito dalle funzioni javascript asincrone NATIVE. Questa è una funzionalità del tuo motore javascript, che si tratti di Node.js o di un browser. È asincrono perché viene passato come "callback" a una funzione che è essenzialmente una scatola nera (implementata in C ecc.). Per lo sfortunato sviluppatore sono asincroni ... solo perché. Se vuoi scrivere la tua funzione asincrona devi hackerarla inviandola a SetTimeout (myfunc, 0). Dovresti farlo? Un altro dibattito .... probabilmente no.
Sean Anderson,

@Fabricio Ho cercato le specifiche che definiscono il "> = 4ms clamp", ma non sono riuscito a trovarlo - ho trovato qualche menzione di un meccanismo simile (per bloccare le chiamate nidificate) su MDN - developer.mozilla.org/en-US/docs / Web / API / ... - qualcuno ha un link alla parte giusta delle specifiche HTML.
Sebi

147

La risposta di Fabrício è perfetta; ma volevo integrare 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 svolgendo ha richiesto alcune informazioni da un collega. L'ho chiamato; ecco come è andata la conversazione:

Io : Ciao Bob, ho bisogno di sapere come siamo andati al bar la scorsa settimana. Jim vuole un rapporto su di esso, e tu sei l'unico a conoscerne i dettagli.

Bob : Certo, ma mi ci vorranno circa 30 minuti?

Io : È grandioso Bob. Dammi un anello quando hai le informazioni!

A questo punto, ho riagganciato il telefono. Dato che avevo bisogno di informazioni da Bob per completare il mio rapporto, ho lasciato il rapporto e invece sono andato a prendere un caffè, poi ho ricevuto qualche email. 40 minuti dopo (Bob è lento), Bob ha richiamato e mi ha dato le informazioni di cui avevo bisogno. A questo punto, ho ripreso il mio lavoro con il mio rapporto, poiché avevo tutte le informazioni di cui avevo bisogno.


Immagina se la conversazione fosse andata invece così;

Io : Ciao Bob, ho bisogno di sapere come siamo andati al bar la scorsa settimana. Jim vuole che ci sia un rapporto, e tu sei l'unico a conoscerne i dettagli.

Bob : Certo, ma mi ci vorranno circa 30 minuti?

Io : È grandioso 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 vs. sincrono

Questo è esattamente ciò che sta accadendo in tutti gli esempi della nostra domanda. Il caricamento di un'immagine, il caricamento di un file dal disco e la richiesta di una pagina tramite AJAX sono operazioni lente (nel contesto dell'informatica moderna).

Anziché attendere il completamento di queste operazioni lente, JavaScript consente di registrare una funzione di richiamata che verrà eseguita al termine dell'operazione lenta. Nel frattempo, tuttavia, JavaScript continuerà a eseguire altro codice. Il fatto che JavaScript esegua altro codice durante l'attesa del 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 il caricamento di JavaScript lolcat.png, che è un'operazione sloooow . La funzione di callback verrà eseguita una volta eseguita questa operazione lenta, ma nel frattempo JavaScript continuerà a elaborare le successive righe di codice; vale a dire alert(outerScopeVar).

Questo è il motivo per cui vediamo l'avviso che mostra undefined; poiché alert()viene elaborato immediatamente, anziché dopo che l'immagine è stata caricata.

Per correggere il nostro codice, tutto ciò che dobbiamo fare è spostare il alert(outerScopeVar)codice nella funzione di richiamata. Di conseguenza, non abbiamo più bisogno della outerScopeVarvariabile 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é questo è l'unico modo * in JavaScript per definire un 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 anche usare eval(), ma eval()è male per questo scopo


Come faccio ad aspettare il mio interlocutore?

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 outerScopeVaraccade immediatamente; prima che la onloadfunzione di callback abbia aggiornato la variabile. Questo porta al getWidthOfImage()ritorno undefinede undefinedall'allerta.

Per risolvere questo problema, dobbiamo consentire alla funzione che chiama getWidthOfImage()di registrare un callback, quindi spostare l'avviso della larghezza in modo tale da rientrare in 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).


Ma in che modo è utile avvisare o inviare alla console se si desidera utilizzare i risultati in un calcolo diverso o archiviarlo in una variabile oggetto?
Ken Ingram il

68

Ecco una risposta più concisa per le persone che cercano un riferimento rapido, nonché alcuni esempi che utilizzano promesse e asincrono / attendono.

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());

undefinedviene registrato in questo caso perché getMessageritorna prima che setTimeoutvenga richiamata la richiamata e si aggiorni outerScopeVar.

I due modi principali per risolverlo sono i callback e le promesse :

callback

La modifica qui è che getMessageaccetta un callbackparametro 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);
});

promesse

Le promesse forniscono un'alternativa più flessibile dei callback perché possono essere naturalmente combinati 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 offre funzionalità simili alle promesse con i suoi Differiti.

function getMessage() {
  var deferred = $.Deferred();
  setTimeout(function() {
    deferred.resolve('Hello asynchronous world!');
  }, 0);
  return deferred.promise();
}

getMessage().done(function(message) {
  console.log(message);  
});

async / await

Se il tuo ambiente JavaScript include il supporto per asynce await(come Node.js 7.6+), puoi utilizzare le promesse in modo sincrono all'interno delle asyncfunzioni:

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();

Il tuo campione su Promises è fondamentalmente quello che stavo cercando, nelle ultime ore. Il tuo esempio è bellissimo e spiega Promesse allo stesso tempo. Perché questo non è altrove è da capogiro.
Vincent P,

Va tutto bene, ma cosa succede se è necessario chiamare getMessage () con i parametri? Come scriveresti quanto sopra in quello scenario?
Chiwda,

2
@Chiwda Basta mettere il parametro di callback ultima: function getMessage(param1, param2, callback) {...}.
JohnnyHK,

Sto provando il tuo async/awaitcampione, ma sto riscontrando problemi. Invece di new Promisecreare un'istanza a , sto effettuando una .Get()chiamata e quindi non ho accesso a nessun resolve()metodo. Quindi il mio getMessage()sta restituendo la Promessa e non il risultato. Potresti modificare un po 'la tua risposta per mostrare una sintassi funzionante per questo?
InteXX

@InteXX Non sono sicuro di cosa intendi per effettuare una .Get()chiamata. Probabilmente è meglio pubblicare una nuova domanda.
JohnnyHK,

52

Per affermare l'ovvio, la coppa rappresenta outerScopeVar.

Le funzioni asincrone sono come ...

chiamata asincrona per il caffè


13
Considerando che provare a far funzionare una funzione asincrona in modo sincrono, si proverebbe a bere il caffè a 1 secondo e averlo versato in grembo a 1 minuto.
Teepeemm,

Se affermasse l'ovvio, non credo che la domanda sarebbe stata posta, no?
broccoli2000,

2
@ broccoli2000 Con ciò non intendevo dire che la domanda fosse ovvia, ma che sia ovvio cosa rappresenti la coppa nel disegno :)
Johannes Fahrenkrug,

13

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 il $.geto $.posto $.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 attiva la chiamata asincrona sulla riga 2 (ovvero la richiesta post) e continua la sua esecuzione dalla riga 3, senza attendere che la richiesta post completi la sua esecuzione.

Supponiamo che il completamento della richiesta di post richieda 10 secondi, il valore di outerScopeVarverrà impostato solo dopo quei 10 secondi.

Per 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 esegui questo, riceverai un avviso sulla linea 3. Ora attendi qualche istante finché non sei sicuro che la richiesta di post abbia restituito un valore. Quindi, quando si fa clic su OK, nella casella di avviso, l'avviso successivo stampa il valore previsto, poiché è stato atteso.

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.


or by waiting on the asynchronous callsCome si fa a farlo?
InteXX

@InteXX Utilizzando un metodo di callback
Teja

Hai un esempio di sintassi veloce?
InteXX

10

In tutti questi scenari outerScopeVarviene modificato o assegnato un valore in modo asincrono o si verifica in un momento successivo (in attesa o in attesa che si verifichi un evento), per il quale l'esecuzione corrente non attenderà . Pertanto, tutti questi casi il flusso di esecuzione corrente si traduce inouterScopeVar = undefined

Discutiamo ogni esempio (ho segnato la parte che viene chiamata in modo asincrono o ritardata affinché si verifichino alcuni eventi):

1.

inserisci qui la descrizione dell'immagine

Qui registriamo un listener di eventi che verrà eseguito su quel particolare evento. Qui il caricamento dell'immagine. Quindi l'esecuzione corrente continua con le righe successive img.src = 'lolcat.png';e alert(outerScopeVar);nel frattempo l'evento potrebbe non verificarsi. vale a dire, la funzione img.onloadattende il caricamento dell'immagine di riferimento, in modo asincrono. Questo accadrà a tutti gli esempi seguenti: l'evento potrebbe essere diverso.

2.

2

Qui l'evento di timeout svolge il ruolo, che invocherà il gestore dopo il tempo specificato. Eccolo 0, ma registra comunque un evento asincrono che verrà aggiunto all'ultima posizione di Event Queueper l'esecuzione, il che rende il ritardo garantito.

3.

inserisci qui la descrizione dell'immagine Questa volta ajax callback.

4.

inserisci qui la descrizione dell'immagine

Il nodo può essere considerato un re della codifica asincrona. Qui la funzione contrassegnata viene registrata come gestore di callback che verrà eseguito dopo aver letto il file specificato.

5.

inserisci qui la descrizione dell'immagine

L'ovvia promessa (qualcosa verrà fatto in futuro) è asincrona. vedi Quali sono le differenze tra differito, promessa e futuro in JavaScript?

https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.