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

May 15 2014

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 aparece undefinedem todos esses exemplos? Não quero soluções alternativas, quero saber por que isso está acontecendo.


Observação: esta é uma questão canônica para assincronicidade de JavaScript . Sinta-se à vontade para melhorar esta questão e adicionar exemplos mais simplificados com os quais a comunidade pode se identificar.

Respostas

605 FabrícioMatté May 15 2014 at 06:55

Resposta de uma palavra: assincronicidade .

Prefácio

Este tópico foi iterado pelo menos alguns milhares de vezes, aqui, no Stack Overflow. Portanto, em primeiro lugar, gostaria de apontar alguns recursos extremamente úteis:


A resposta para a pergunta em questão

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

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

Depende do caso. Vamos tentar rastrear alguns comportamentos comuns novamente:

  • img.onloadpode ser chamado em algum momento no futuro , quando (e se) a imagem for carregada com sucesso.
  • setTimeoutpode ser chamado em algum momento no futuro , após o atraso ter expirado e o tempo limite não tiver sido cancelado por clearTimeout. Nota: mesmo quando usando 0como atraso, todos os navegadores têm um limite mínimo de atraso de tempo limite (especificado como 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 Ajax for concluída com êxito.
  • O Node.js fs.readFilepode 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 ser executado em algum momento no futuro . Esse "algum dia no futuro" é o que chamamos de fluxo assíncrono .

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

Mais especificamente, quando o mecanismo JS está 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. Isso é considerado como loop de evento .

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

Resumindo, 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?

É realmente simples. A lógica que depende da execução da função assíncrona deve ser iniciada / chamada de dentro desta função assíncrona. Por exemplo, mover os alerts e console.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

Freqüentemente, 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 analisar um exemplo um pouco mais complexo:

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

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

Observação: estou usando setTimeoutcom um atraso aleatório como uma função assíncrona genérica, o mesmo exemplo se aplica a Ajax readFile, onloade qualquer outro fluxo assíncrono.

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

Vamos abordar isso implementando nosso próprio sistema de retorno de chamada. 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);
}

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

Na maioria das vezes, em casos de uso reais, a API DOM e a maioria das bibliotecas já fornecem a funcionalidade de retorno de chamada (a helloCatAsyncimplementaçã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 returnum valor de um fluxo assíncrono retornar ao fluxo síncrono onde o retorno de chamada foi definido, já que os retornos de chamada assíncronos são executados muito depois que o código síncrono já terminou de ser executado.

Em vez de returncarregar um valor de um retorno de chamada assíncrono, você terá que fazer uso do padrão de retorno de chamada ou ... Promessas.

Promessas

Embora existam maneiras de evitar o inferno de callbacks com o vanilla JS, as promessas estão crescendo em popularidade e estão sendo padronizadas no ES6 (consulte Promise - MDN ).

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


Mais material de leitura sobre assincronicidade JavaScript


Observação: marquei esta resposta como Community Wiki, portanto, qualquer pessoa com pelo menos 100 reputações pode editá-la e melhorá-la! Sinta-se à vontade para melhorar esta resposta ou enviar uma resposta completamente nova, se desejar.

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

158 Matt May 29 2014 at 16:09

A resposta de Fabrício foi certeira; 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 fazia exigia algumas informações de um colega. Liguei para ele; Veja como foi a conversa:

Eu : Oi Bob, preciso saber como enganamos o bar na semana passada. Jim quer um relatório sobre isso, e você é o único que conhece os detalhes a respeito.

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

Eu : Isso é ótimo Bob. Me ligue de volta quando tiver a informação!

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


Imagine se a conversa tivesse sido assim;

Eu : Oi Bob, preciso saber como enganamos o bar na semana passada. Jim quer um relatório sobre isso, e você é o único que conhece os detalhes sobre isso.

Bob : Claro, mas vou demorar 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. Por fim, Bob me deu as informações, desligamos e concluí meu relatório. Mas eu perdi 40 minutos de produtividade.


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

Isso é exatamente o 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 esperar 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á a executar outro código. O fato de que o JavaScript executa outro código enquanto espera a conclusão da operação lenta torna o comportamento assíncrono . Se o JavaScript tivesse esperado pela 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, pedimos que o JavaScript carregue lolcat.png, o que é uma operação lenta . A função de retorno de chamada será executada assim que esta 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; uma vez que alert()é processado imediatamente, em vez de depois que a imagem foi carregada.

Para consertar 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 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 função, porque essa é a única * maneira em JavaScript de definir algum código, mas não executá-lo até mais tarde.

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

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


Como faço para manter meu chamador esperando?

Você pode ter atualmente 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 estar 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).

75 JohnnyHK Jan 21 2015 at 06:42

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

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 neste caso porque getMessageretorna antes que o setTimeoutretorno de chamada seja chamado e seja atualizado outerScopeVar.

As duas maneiras principais de resolver isso são retornos de chamada e promessas :

Callbacks

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 do que os retornos de chamada porque 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

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 seu ambiente JavaScript inclui suporte para asynce await(como Node.js 7.6+), você pode usar promessas de forma síncrona dentro das 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();
58 JohannesFahrenkrug Dec 08 2015 at 23:48

Para afirmar o óbvio, a xícara representa outerScopeVar.

As funções assíncronas são como ...

14 Teja Feb 26 2016 at 10:59

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

Todas as chamadas ajax (incluindo o $.getou $.postou $.ajax) são assíncronas.

Considerando 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 uma chamada assíncrona na linha 2 (ou seja, a pós-solicitação) e continua sua execução a partir da linha 3, sem esperar que a pós-solicitação conclua sua execução.

Digamos que a solicitação de postagem leve 10 segundos para ser concluída, o valor de outerScopeVarsó 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ê obteria um alerta na linha 3. Agora, espere algum tempo até ter certeza de que a solicitação de postagem retornou algum valor. Então, ao 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 dentro do bloco assíncrono, ou aguardando as chamadas assíncronas.

11 TomSebastian Oct 27 2015 at 13:35

Em todos esses cenários outerScopeVaré modificado ou atribuído um valor de forma assíncrona ou acontecendo em um momento posterior (esperando ou ouvindo algum evento ocorrer), para o qual a execução atual não esperará . Então, todos esses casos o fluxo de execução atual resulta emouterScopeVar = undefined

Vamos discutir cada um dos exemplos (marquei a parte que é chamada de forma assíncrona ou atrasada para que alguns eventos ocorram):

1

Aqui, registramos um listador de eventos que será executado naquele evento em particular. Aqui carregamos a imagem. Então, a execução atual continua com as próximas linhas img.src = 'lolcat.png';e, alert(outerScopeVar);entretanto, o evento pode não ocorrer. ou seja, função img.onloadesperar que a imagem em questão carregue de forma assíncrona. Isso acontecerá no exemplo a seguir - o evento pode ser diferente.

2

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

3 -

Desta vez, callback de Ajax.

4

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

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

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