Por que minha variável permanece inalterada após modificá-la dentro de uma função? - Referência de código assíncrona

738

Dados os exemplos a seguir, por que é outerScopeVarindefinido 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 é exibido undefinedem todos esses exemplos? Não quero soluções alternativas, quero saber por que isso está acontecendo.


Nota: Esta é uma pergunta canônica para a assincronicidade do JavaScript . Sinta-se livre para melhorar esta questão e adicionar exemplos mais simplificados com os quais a comunidade possa se identificar.

Fabrício Matté
fonte
@ Dukeling obrigado, tenho certeza de que havia comentado com esse link, mas aparentemente existem alguns comentários ausentes. Além disso, em relação à sua edição: acredito que ter "canônico" e "assincronicidade" no título ajuda na busca por essa pergunta para marcar outra questão como um tolo. E, é claro, também ajuda a encontrar essa pergunta do Google ao procurar explicações sobre assincronicidade.
Fabrício Matté 16/05
5
Pensando um pouco mais, "tópico de assincronicidade canônica" é um pouco pesado sobre o título, "referência de código assíncrona" é mais simples e objetiva. Eu também acredito que a maioria das pessoas procura por "assíncrono" em vez de "assincronicidade".
Fabrício Matté 20/05
2
Algumas pessoas inicializam sua variável antes da chamada da função. Que tal mudar o título que de alguma forma representa isso também? Como "Por que minha variável permanece inalterada depois de modificá-la dentro de uma função?" ?
Felix Kling 07/07
Em todos os exemplos de código mencionados acima, "alert (outerScopeVar);" executa NOW, enquanto a atribuição de valor a "outerScopeVar" acontece MAIS TARDE (de forma assíncrona).
refatorar

Respostas:

581

Resposta de uma palavra: assincronicidade .

Prefácio

Este tópico foi iterado pelo menos algumas milhares de vezes, aqui, no Stack Overflow. Por isso, primeiro, gostaria de destacar alguns recursos extremamente úteis:


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 função . Essa função claramente não é executada imediatamente, está sendo atribuída ou passada como argumento. É isso que chamamos de retorno de chamada .

Agora, a pergunta é: quando é que esse retorno de chamada é chamado?

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

  • img.onloadpode ser chamado em algum momento no futuro , quando (e se) a imagem tiver sido carregada com êxito.
  • setTimeoutpode ser chamado em algum momento no futuro , após o atraso expirar e o tempo limite não ter sido cancelado por clearTimeout. Nota: mesmo quando usado 0como atraso, todos os navegadores têm um limite mínimo de atraso de tempo limite (especificado para 4ms na especificação HTML5).
  • $.postO retorno de chamada do jQuery pode ser chamado em algum momento no futuro , quando (e se) a solicitação do Ajax for concluída com êxito.
  • Os Node.js fs.readFilepodem ser chamados em algum momento no futuro , quando o arquivo tiver sido lido com êxito ou ocorrer um erro.

Em todos os casos, temos um retorno de chamada que pode ser executado em algum momento no futuro . Este "em algum momento no futuro" é o que chamamos de fluxo assíncrono .

A execução assíncrona é enviada para fora do fluxo síncrono. Ou seja, o código assíncrono será nunca executado enquanto a pilha de códigos síncronos estiver em execução. Este é o significado do JavaScript ser de thread único.

Mais especificamente, quando o mecanismo JS estiver ocioso - não executando uma pilha de (a) código síncrono - ele pesquisará eventos que podem ter acionado retornos de chamada assíncronos (por exemplo, tempo limite expirado, resposta de rede recebida) e os executará um após o outro. Isto é considerado como loop de eventos .

Ou seja, o código assíncrono destacado nas formas vermelhas desenhadas à mão pode ser executado somente após a execução de todo o código síncrono restante em seus respectivos blocos de código:

código assíncrono destacado

Em resumo, 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 função assíncrona até saber que ela foi executada e como fazer isso?

É simples, realmente. A lógica que depende da execução da função assíncrona deve ser iniciada / chamada de dentro dessa função assíncrona. Por exemplo, movendo-se os alerts econsole.log é também dentro da função de retorno de saída seria o resultado esperado, porque o resultado está disponível naquele momento.

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

Geralmente, você precisa fazer mais coisas com o resultado de uma função assíncrona ou fazer coisas diferentes com o resultado, dependendo de onde a função 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: eu estou usando setTimeoutcom um atraso aleatório como uma função assíncrona genérico, o mesmo exemplo se aplica ao Ajax, readFile,onload e qualquer outro fluxo assíncrono.

Este exemplo sofre claramente do mesmo problema que os outros exemplos, não está aguardando até que a função assíncrona seja executada.

Vamos resolver isso implementando um sistema de retorno de chamada próprio. Primeiro, nos livramos daquele feio outerScopeVarque é completamente inútil neste caso. Em seguida, adicionamos um parâmetro que aceita um argumento de função, nosso retorno de chamada. Quando a operação assíncrona termina, chamamos esse retorno de chamada passando o resultado. A implementação (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);
}

Fragmento de código do exemplo acima:

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

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

Você também notará que, devido à natureza assíncrona, é impossível return um valor de um fluxo assíncrono para o fluxo síncrono em que o retorno de chamada foi definido, pois os retornos de chamada assíncronos são executados muito tempo após o código síncrono já ter terminado de executar.

Em vez de returninserir um valor de um retorno de chamada assíncrono, você precisará usar o padrão de retorno de chamada ou ... Promessas.

Promessas

Embora existam maneiras de manter o retorno de chamada muito distante do JS vanilla, as promessas estão crescendo em popularidade e atualmente estão sendo padronizadas no ES6 (consulte Promise - MDN ).

As promessas (também conhecidas como futuros) fornecem uma leitura mais linear e, portanto, agradável do código assíncrono, mas a explicação de toda a sua funcionalidade está fora do escopo desta questão. Em vez disso, deixarei esses excelentes recursos para os interessados:


Mais material de leitura sobre assincronicidade JavaScript

  • The Art of Node - Callbacks explica muito bem o código assíncrono e os retornos de chamada com exemplos JS vanilla e código Node.js.

Nota: Marquei esta resposta como Wiki da comunidade, portanto, qualquer pessoa com pelo menos 100 reputações pode editá-la e melhorá-la! Sinta-se à vontade para melhorar esta resposta ou envie uma resposta completamente nova, se desejar.

Quero transformar essa pergunta em um tópico canônico para responder a questões de assincronicidade que não estão relacionadas ao Ajax (há Como retornar a resposta de uma chamada AJAX? ), Portanto, este tópico precisa de sua ajuda para ser o melhor e mais útil possível !

Fabrício Matté
fonte
1
No seu último exemplo, existe uma razão específica para você usar funções anônimas ou funcionaria da mesma maneira usando funções nomeadas?
JDelage
1
Os exemplos de código são um pouco estranhos quando você declara a função depois de chamá-la. Funciona por causa da elevação, é claro, mas foi intencional?
Bergi 5/05
2
é um impasse? felix Kling está apontando para sua resposta e você está apontando para resposta felix
Mahi
1
Você precisa entender que o código do círculo vermelho é apenas assíncrono porque está sendo executado pelas funções javascript assíncronas do NATIVE. Esse é um recurso do seu mecanismo javascript - seja o Node.js ou um navegador. É assíncrono porque está sendo passado como um "retorno de chamada" para uma função que é essencialmente uma caixa preta (implementada em C etc.). Para o desenvolvedor infeliz, eles são assíncronos ... só porque. Se você quiser escrever sua própria função assíncrona, precisará hacká-la enviando-a para SetTimeout (myfunc, 0). Você deveria fazer isso? Outro debate ... provavelmente não.
Sean Anderson
@Fabricio Procurei as especificações que definem o grampo "> = 4ms", mas não consegui encontrá-lo - encontrei alguma menção de mecanismo semelhante (para fixar chamadas aninhadas) no MDN - developer.mozilla.org/pt-BR/docs / Web / API /… - alguém tem um link para a parte correta da especificação HTML.
Sebi
156

A resposta de Fabrício é imediata; mas eu queria complementar sua resposta com algo menos técnico, que se concentra em uma analogia para ajudar a explicar o conceito de assincronicidade .


Uma Analogia ...

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

-Me : Hi Bob, eu preciso saber como nós foo 'd o bar ' D na semana passada. Jim quer um relatório sobre isso, e você é o único que conhece os detalhes.

Bob : Claro, mas levará cerca de 30 minutos?

Eu : Isso é ótimo, Bob. Dê-me um anel de volta quando tiver as informações!

Nesse ponto, desliguei o telefone. Como eu precisava de informações de Bob para concluir o meu relatório, deixei o relatório e fui tomar um café, depois peguei um email. 40 minutos depois (Bob é lento), Bob ligou de volta e me deu as informações que eu precisava. Nesse ponto, retomei meu trabalho com meu relatório, pois possuía todas as informações necessárias.


Imagine se a conversa tivesse sido assim;

-Me : Hi Bob, eu preciso saber como nós foo 'd o bar ' D na semana passada. Jim quer um relatório e você é o único que conhece os detalhes.

Bob : Claro, mas levará cerca de 30 minutos?

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

E eu sentei lá e esperei. E esperou. E esperou. 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 tinha perdido 40 minutos de produtividade.


Este é um comportamento assíncrono vs. síncrono

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

Em vez de aguardar a conclusão dessas operações lentas, o JavaScript permite registrar uma função 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 o JavaScript executar outro código enquanto aguarda a conclusão da operação lenta torna o comportamento assíncrono . Se o JavaScript esperasse a conclusão da operação antes de executar qualquer outro código, isso teria sido 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 solicitando o carregamento do JavaScript lolcat.png, que é uma operação sloooow . A função de retorno de chamada será executada assim que essa operação lenta for concluída, mas, enquanto isso, o JavaScript continuará processando as próximas linhas de código; ie alert(outerScopeVar).

É por isso que vemos o alerta sendo exibido undefined; já que o alert()é processado imediatamente, e não depois que a imagem foi carregada.

Para corrigir nosso código, tudo o que precisamos fazer é mover o alert(outerScopeVar)código para a função de retorno de chamada. Como consequência disso, não precisamos mais da outerScopeVarvariável declarada como 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 função, porque essa é a única maneira * no JavaScript de definir algum código, mas não executá-lo até mais tarde.

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

* Tecnicamente você pode usar eval()também, mas eval()é mau para esse fim


Como faço para manter minha ligação em espera?

No momento, você pode 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, agora sabemos que isso return outerScopeVaracontece imediatamente; antes que a onloadfunção de retorno de chamada atualize a variável Isso leva ao getWidthOfImage()retorno undefinede ao undefinedalerta.

Para corrigir isso, precisamos permitir que a chamada de função 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, observe que conseguimos remover as variáveis ​​globais (neste caso width).

Matt
fonte
4
Mas como o alerta ou o envio para o console é útil se você deseja usar os resultados em um cálculo diferente ou armazená-lo em uma variável de objeto?
Ken Ingram
73

Aqui está uma resposta mais concisa para as pessoas que procuram uma referência rápida, bem como alguns exemplos usando promessas e assíncrono / espera.

Comece com a abordagem ingênua (que não funciona) para uma função que chama um método assíncrono (neste caso setTimeout) e retorna uma mensagem:

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

undefinedé registrado nesse caso porque getMessageretorna antes que o setTimeoutretorno de chamada seja chamado e seja atualizado outerScopeVar.

As duas principais maneiras de resolvê-lo estão usando retornos de chamada e promessas :

Retornos de chamada

A mudança aqui é que getMessageaceita um callbackparâmetro que será chamado para entregar os resultados de volta ao código de chamada assim que disponível.

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

Promessas

As promessas fornecem uma alternativa mais flexível que os retornos de chamada, porque eles podem ser combinados naturalmente para coordenar várias operações assíncronas. A Promises / A + implementação padrão é nativamente previsto no node.js (0.12+) e muitos navegadores atuais, mas também é implementado 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 promessas com seus adiados.

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

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

assíncrono / aguardar

Se o seu ambiente JavaScript incluir suporte para ( asynce awaitcomo o Node.js 7.6+), você poderá usar as promessas de forma síncrona nas asyncfunções:

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();
JohnnyHK
fonte
Sua amostra do Promises é basicamente o que eu tenho procurado nas últimas horas. Seu exemplo é bonito e explica promessas ao mesmo tempo. Por que isso não está em outro lugar, é espantoso.
Vincent P
Tudo bem, mas e se você precisar chamar getMessage () com parâmetros? Como você escreveria o acima nesse cenário?
Chiwda
2
@Chiwda É só colocar a rechamada último parâmetro: function getMessage(param1, param2, callback) {...}.
precisa saber é o seguinte
Estou testando sua async/awaitamostra, mas estou com problemas. Em vez de instanciar a new Promise, estou fazendo uma .Get()chamada e, portanto, não tenho acesso a nenhum resolve()método. Assim, o meu getMessage()está retornando a promessa e não o resultado. Você poderia editar sua resposta um pouco para mostrar uma sintaxe funcional para isso?
InteXX 8/08/19
@InteXX Não sei ao certo o que você quer dizer com fazer uma .Get()ligação. Provavelmente é melhor postar uma nova pergunta.
precisa saber é o seguinte
56

Para afirmar o óbvio, o copo representa outerScopeVar.

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

chamada assíncrona para café

Johannes Fahrenkrug
fonte
14
Ao passo que tentar fazer com que uma função assíncrona atue de forma síncrona seria beber o café em 1 segundo e colocá-lo no seu colo em 1 minuto.
Teepeemm
Se estivesse afirmando o óbvio, não acho que a pergunta teria sido feita, não?
broccoli2000
2
@ broccoli2000 por que eu não queria dizer que a pergunta era óbvia, mas que é óbvio que a taça representa no desenho :)
Johannes Fahrenkrug
14

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

Todas as chamadas ajax (incluindo o $.getou $.postou $.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 começa na linha 1, declara a variável e dispara e chamada assíncrona na linha 2 (ou seja, a solicitação de postagem) e continua sua execução da linha 3, sem aguardar que a solicitação de postagem conclua sua execução.

Digamos que a solicitação de postagem leva 10 segundos para ser concluída, o valor de outerScopeVarsomente 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, ao executar isso, você receberá um alerta na linha 3. Agora aguarde um pouco até ter certeza de que a solicitação de postagem retornou algum valor. Quando você clicar em OK, na caixa de alerta, o próximo alerta imprimirá o valor esperado, porque você o aguardou.

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 para dentro do bloco assíncrono ou aguardando as chamadas assíncronas.

Teja
fonte
or by waiting on the asynchronous callsComo é que alguém faz isso?
InteXX 8/08/19
@InteXX Usando um método de retorno de chamada
Teja
Você tem um exemplo rápido de sintaxe?
InteXX 9/09/19
11

Em todos esses cenários, outerScopeVaré modificado ou atribuído um valor de forma assíncrona ou acontecendo posteriormente (aguardando ou ouvindo a ocorrência de algum evento), pelo qual a execução atual não aguardará . Portanto, em todos esses casos, o fluxo de execução atual resulta emouterScopeVar = undefined

Vamos discutir cada exemplo (marquei a parte que é chamada de forma assíncrona ou atrasada para a ocorrência de alguns eventos):

1

insira a descrição da imagem aqui

Aqui registramos um eventlistner que será executado nesse evento específico. Aqui está o carregamento da imagem. Em seguida, a execução atual continua nas próximas linhas img.src = 'lolcat.png';e, alert(outerScopeVar);enquanto isso, o evento pode não ocorrer. isto é, img.onloadespera a função para carregar a imagem referida de forma assíncrona. Isso acontecerá em todos os exemplos a seguir - o evento pode ser diferente.

2)

2

Aqui, o evento de tempo limite desempenha a função, que chamará o manipulador após o tempo especificado. Aqui está 0, mas ainda assim registra um evento assíncrono que será adicionado à última posição da Event Queueexecução, o que torna o atraso garantido.

3)

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

4)

insira a descrição da imagem aqui

O nó pode ser considerado um rei da codificação assíncrona. Aqui a função marcada é registrada como um manipulador de retorno de chamada que será executado após a leitura do arquivo especificado.

5)

insira a descrição da imagem aqui

A promessa óbvia (algo será feito no futuro) é assíncrona. consulte Quais são as diferenças entre adiado, promissor e futuro em JavaScript?

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

Tom Sebastian
fonte