Por que minha variável é inalterada depois de modificá-la dentro de uma function? – Referência de código assíncrona

Dados os exemplos a seguir, por que o outerScopeVar é indefinido em todos os casos?

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

Por que a saída é undefined em todos esses exemplos? Não quero soluções alternativas, quero saber por que isso está acontecendo.


Nota: Esta é uma questão canônica para a assincronia do JavaScript . Sinta-se à vontade para melhorar essa questão e adicionar exemplos mais simplificados com os quais a comunidade pode se identificar.

Uma palavra resposta: assincronismo .

Prefácio

Este tópico foi iterado pelo menos um par de milhares de vezes, aqui, no Stack Overflow. Por isso, em primeiro lugar, gostaria de destacar alguns resources extremamente úteis:

  • @Felix Kling’s “Como retornar a resposta de uma chamada AJAX” . Veja sua excelente resposta explicando os streams síncronos e asynchronouss, bem como a seção “Reestruturar código”.
    @Benjamin Gruenbaum também fez um grande esforço explicando a assincronia no mesmo tópico.

  • A resposta de @Matt Esch para “Obter dados de fs.readFile” também explica muito bem a assincronicidade de uma maneira simples.


A resposta para a pergunta em questão

Vamos traçar o comportamento comum primeiro. Em todos os exemplos, o outerScopeVar é modificado dentro de uma function . Essa function claramente não é executada imediatamente, está sendo atribuída ou passada como um argumento. Isso é o que chamamos de retorno de chamada .

Agora a questão é, quando é que esse retorno de chamada é chamado?

Depende do caso. Vamos tentar rastrear algum comportamento comum novamente:

  • img.onload pode ser chamado em algum momento no futuro , quando (e se) a imagem for carregada com sucesso.
  • setTimeout pode ser chamado em algum momento no futuro , após o atraso ter expirado e o tempo limite não ter sido cancelado por clearTimeout . Nota: mesmo quando se usa 0 como atraso, todos os navegadores têm um limite de tempo limite mínimo (especificado para ser 4 ms na especificação HTML5).
  • O callback do jQuery $.post pode ser chamado em algum momento no futuro , quando (e se) o pedido do Ajax tiver sido completado com sucesso.
  • fs.readFile do Node.js pode ser chamado em algum momento no futuro , quando o arquivo foi lido com sucesso ou gerou um erro.

Em todos os casos, temos um retorno de chamada que pode ocorrer em algum momento no futuro . Este “em algum momento no futuro” é o que nos referimos como stream asynchronous .

Execução assíncrona é empurrada para fora do stream síncrono. Ou seja, o código asynchronous nunca será executado enquanto a pilha de códigos síncronos estiver sendo executada. Este é o significado do JavaScript ser single-threaded.

Mais especificamente, quando o mecanismo JS está ocioso – não executando uma pilha de (a) código síncrono – ele pesquisará events que possam ter acionado retornos de chamada asynchronouss (por exemplo, tempo expirado expirado, resposta de rede recebida) e executá-los um após o outro. Isso é considerado como loop de events .

Ou seja, o código asynchronous destacado nas formas vermelhas desenhadas à mão pode ser executado somente depois que todo o código síncrono restante em seus respectivos blocos de código tiver sido executado:

código assíncrono destacado

Em suma, as funções de retorno de chamada são criadas de forma síncrona, mas executadas de forma assíncrona. Você simplesmente não pode confiar na execução de uma function assíncrona até saber que ela foi executada e como fazer isso?

É simples, na verdade. A lógica que depende da execução da function assíncrona deve ser iniciada / chamada de dentro dessa function assíncrona. Por exemplo, mover os alert s e console.log s também dentro da function de retorno de chamada produziria o resultado esperado, porque o resultado está disponível nesse ponto.

Implementando sua própria lógica de retorno de chamada

Muitas vezes você precisa fazer mais coisas com o resultado de uma function assíncrona ou fazer coisas diferentes com o resultado, dependendo de onde a function assíncrona foi chamada. Vamos abordar um exemplo um pouco mais complexo:

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

Nota: Estou usando setTimeout com um atraso random como uma function assíncrona genérica, o mesmo exemplo se aplica a Ajax, readFile , onload e qualquer outro stream asynchronous.

Este exemplo claramente sofre o mesmo problema que os outros exemplos, ele não está esperando até que a function assíncrona seja executada.

Vamos enfrentá-lo implementando um sistema de retorno de chamada próprio. Em primeiro lugar, nos livramos daquele feio outerScopeVar que é completamente inútil neste caso. Em seguida, adicionamos um parâmetro que aceita um argumento de function, nosso callback. Quando a operação assíncrona termina, chamamos esse retorno de chamada passando o resultado. A implementação (por favor leia os comentários em ordem):

 // 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); } 

Geralmente, em casos de uso reais, a API do DOM e a maioria das bibliotecas já fornecem a funcionalidade de retorno de chamada (a implementação helloCatAsync neste exemplo demonstrativo). Você só precisa passar a function de retorno de chamada e entender que ela será executada fora do stream síncrono e reestruturar seu código para acomodar isso.

Você também perceberá que, devido à natureza assíncrona, é impossível return um valor de um stream asynchronous para o stream síncrono no qual o retorno de chamada foi definido, pois os retornos de chamada asynchronouss são executados muito depois de o código síncrono já ter concluído a execução.

Em vez de return um valor de um retorno de chamada asynchronous, você terá que fazer uso do padrão de retorno de chamada, ou … Promessas.

Promessas

Embora existam maneiras de manter o callback longe da baunilha JS, as promises estão crescendo em popularidade e atualmente estão sendo padronizadas no ES6 (veja Promise – MDN ).

As Promessas (também conhecidas como Futures) fornecem uma leitura mais linear e, portanto, mais agradável, do código asynchronous, mas explicar toda a sua funcionalidade está fora do escopo desta questão. Em vez disso, vou deixar esses excelentes resources para os interessados:

  • Promessas JavaScript – Rochas HTML5
  • Você está perdendo o ponto das promises

Mais material de leitura sobre a assincronia do JavaScript

  • O Art of Node – Callbacks explica o código asynchronous e callbacks muito bem com exemplos JS vanilla e código Node.js também.

Nota: Eu marquei esta resposta como Wiki da Comunidade, portanto qualquer pessoa com pelo menos 100 reputações pode editá-la e melhorá-la! Por favor, sinta-se livre para melhorar esta resposta, ou envie uma resposta completamente nova se você quiser também.

Eu quero transformar essa questão em um tópico canônico para responder a questões de assincronismo que não estão relacionadas ao Ajax (há Como retornar a resposta de uma chamada AJAX? Para isso), portanto, esse tópico precisa da sua ajuda para ser o mais bom e útil possível !

A resposta de Fabrício é pontual; mas eu queria complementar sua resposta com algo menos técnico, que focaliza uma analogia para ajudar a explicar o conceito de assincronia .


Uma analogia …

Ontem, o trabalho que estava fazendo exigiu algumas informações de um colega. Eu liguei para ele; aqui está como a conversa foi:

Eu : Oi Bob, eu preciso saber como saímos do bar na semana passada. Jim quer um relatório sobre isso, e você é o único que sabe os detalhes sobre isso.

Bob : Claro, mas vai demorar cerca de 30 minutos?

Eu : Isso é ótimo Bob. Me dê um anel de volta quando você tiver a informação!

Nesse momento, desliguei o telefone. Como eu precisava de informações de Bob para completar meu relatório, deixei o relatório e fui tomar um café, depois, eu peguei um e-mail. 40 minutos depois (Bob é lento), Bob ligou de volta e me deu a informação que eu precisava. Neste ponto, retomei meu trabalho com meu relatório, pois tinha todas as informações de que precisava.


Imagine se a conversa tivesse sido assim;

Eu : Oi Bob, eu preciso saber como saímos do bar na semana passada. Jim quer um relatório sobre isso, e você é o único que sabe os detalhes sobre isso.

Bob : Claro, mas vai demorar cerca de 30 minutos?

Eu : Isso é ótimo Bob. Eu vou esperar.

E eu sentei lá e esperei. E esperei. E esperei. Por 40 minutos. Não fazendo nada além de esperar. Eventualmente, Bob me deu as informações, desligamos e concluí meu relatório. Mas eu perdi 40 minutos de produtividade.


Isso é asynchronous vs. comportamento síncrono

Isto é exatamente o que está acontecendo em todos os exemplos em nossa questão. Carregar uma imagem, carregar um arquivo fora do disco e solicitar uma página via AJAX são operações lentas (no contexto da computação moderna).

Em vez de esperar pela conclusão dessas operações lentas, o JavaScript permite registrar uma function de retorno de chamada que será executada quando a operação lenta for concluída. Enquanto isso, no entanto, o JavaScript continuará executando outro código. O fato de JavaScript executar outro código enquanto aguarda a conclusão da operação lenta torna o comportamento asynchronous . Se o JavaScript esperasse a conclusão da operação antes de executar qualquer outro código, isso seria um comportamento síncrono .

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

No código acima, estamos pedindo JavaScript para carregar lolcat.png , que é uma operação sloooow . A function de retorno de chamada será executada uma vez que esta operação lenta tenha feito, mas enquanto isso, o JavaScript continuará processando as próximas linhas de código; isto é, alert(outerScopeVar) .

É por isso que vemos o alerta mostrando undefined ; já que o alert() é processado imediatamente, em vez de depois que a imagem foi carregada.

Para corrigir nosso código, tudo o que precisamos fazer é mover o código de alert(outerScopeVar) para a function de retorno de chamada. Como conseqüência disso, não precisamos mais da variável outerScopeVar declarada como uma variável global.

 var img = document.createElement('img'); img.onload = function() { var localScopeVar = this.width; alert(localScopeVar); }; img.src = 'lolcat.png'; 

Você sempre verá que um retorno de chamada é especificado como uma function, porque essa é a única maneira * em JavaScript para definir algum código, mas não executá-lo até mais tarde.

Portanto, em todos os nossos exemplos, a function() { /* Do something */ } é o retorno de chamada; para consertar todos os exemplos, tudo o que temos a fazer é mover o código que precisa da resposta da operação para lá!

* Tecnicamente você também pode usar eval() , mas eval() é maligno para essa finalidade


Como faço para manter meu interlocutor esperando?

Você pode atualmente ter algum código semelhante a este;

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

No entanto, sabemos agora que o return outerScopeVar acontece imediatamente; antes que a function de retorno de chamada onload tenha atualizado a variável. Isso leva a getWidthOfImage() retornando undefined e undefined sendo alertado.

Para corrigir isso, precisamos permitir que a function chamando getWidthOfImage() registre um retorno de chamada e, em seguida, mova o alerta da largura para dentro desse retorno de chamada;

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

… como antes, note que conseguimos remover as variables ​​globais (neste caso, width ).

Aqui está uma resposta mais concisa para as pessoas que estão procurando uma referência rápida, bem como alguns exemplos usando promises e async / aguardar.

Comece com a abordagem ingênua (que não funciona) para uma function que chama um método asynchronous (neste caso, setTimeout ) e retorna uma mensagem:

 function getMessage() { var outerScopeVar; setTimeout(function() { outerScopeVar = 'Hello asynchronous world!'; }, 0); return outerScopeVar; } console.log(getMessage()); 

undefined fica logado neste caso porque getMessage retorna antes que o callback setTimeout seja chamado e atualiza outerScopeVar .

As duas principais formas de resolvê-lo são o uso de callbacks e promises :

Retornos de chamada

A alteração aqui é que getMessage aceita um parâmetro de callback que será chamado para entregar os resultados de volta ao código de chamada, uma vez disponível.

 function getMessage(callback) { setTimeout(function() { callback('Hello asynchronous world!'); }, 0); } getMessage(function(message) { console.log(message); }); 

Promessas

As promises fornecem uma alternativa que é mais flexível do que as chamadas de retorno porque elas podem ser naturalmente combinadas para coordenar várias operações assíncronas. Uma implementação padrão Promises / A + é fornecida nativamente em node.js (0.12+) e muitos navegadores atuais, mas também é implementada em bibliotecas como 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

O jQuery fornece funcionalidade semelhante às promises com seus diferidos.

 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 o seu ambiente JavaScript include suporte para async e await (como o Node.js 7.6+), você poderá usar promises de forma síncrona em funções async :

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

Para declarar o óbvio, o copo representa outerScopeVar .

Funções assíncronas são como …

chamada assíncrona para café

As outras respostas são excelentes e eu só quero fornecer uma resposta direta a isso. Apenas limitando as chamadas assíncronas do jQuery

Todas as chamadas ajax (incluindo o $.get ou $.post ou $.ajax ) são assíncronas.

Considerando o seu exemplo

 var outerScopeVar; //line 1 $.post('loldog', function(response) { //line 2 outerScopeVar = response; }); alert(outerScopeVar); //line 3 

A execução do código inicia na linha 1, declara a variável e os acionadores e a chamada assíncrona na linha 2 (ou seja, a solicitação de postagem) e continua sua execução a partir da linha 3, sem esperar que a solicitação de postagem conclua sua execução.

Vamos dizer que a solicitação de postagem leva 10 segundos para ser concluída, o valor de outerScopeVar só será definido após esses 10 segundos.

Tentar,

 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 

Agora, quando você executar isso, você receberá um alerta na linha 3. Agora espere algum tempo até ter certeza de que a solicitação de postagem retornou algum valor. Então, quando você clicar em OK, na checkbox de alerta, o próximo alerta imprimirá o valor esperado, porque você esperou por ele.

No cenário da vida real, o código se torna

 var outerScopeVar; $.post('loldog', function(response) { outerScopeVar = response; alert(outerScopeVar); }); 

Todo o código que depende das chamadas assíncronas, é movido dentro do bloco asynchronous ou aguardando as chamadas assíncronas.

Em todos esses cenários, outerScopeVar é modificado ou atribuído um valor de forma assíncrona ou ocorrendo em um momento posterior (aguardando ou aguardando a ocorrência de algum evento), para o qual a execução atual não esperará . Portanto, todos os casos atuais resultam em outerScopeVar = undefined

Vamos discutir cada exemplo (marquei a parte que é chamada de forma assíncrona ou atrasada para que alguns events ocorram):

1

insira a descrição da imagem aqui

Aqui nós registramos um eventlistner que será executado naquele evento em particular. Aqui o carregamento da imagem. Então a execução atual continua com as próximas linhas img.src = 'lolcat.png'; e alert(outerScopeVar); Enquanto isso, o evento pode não ocorrer. ou seja, funtion img.onload espera que a imagem referida seja carregada de forma assíncrona. Isso acontecerá todo o seguinte exemplo – o evento pode ser diferente.

2

2

Aqui, o evento timeout desempenha a function, que chama o manipulador após o tempo especificado. Aqui é 0 , mas ainda assim registra um evento asynchronous que será adicionado à última posição da Event Queue para execução, o que torna o delay garantido.

3

insira a descrição da imagem aqui Desta vez retorno de chamada ajax.

4

insira a descrição da imagem aqui

Node pode ser considerado como um rei da codificação assíncrona.Aqui a function marcada é registrada como um manipulador de retorno de chamada que será executado depois de ler o arquivo especificado.

5

insira a descrição da imagem aqui

Promessa óbvia (algo será feito no futuro) é assíncrona. veja Quais são as diferenças entre Diferido, Promessa e Futuro em JavaScript?

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