Resposta com 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.onload
pode ser chamado em algum momento no futuro , quando (e se) a imagem tiver sido carregada com êxito.
setTimeout
pode 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 ao usar0
como atraso, todos os navegadores têm um limite mínimo de atraso de tempo limite (especificado para 4ms na especificação HTML5).
$.post
O 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.readFile
podem 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 ocorrer 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 nunca será 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. Isso é 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:
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, mover os alert
s 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
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 setTimeout
com 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 outerScopeVar
que é 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 (a 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 return
inserir um valor a partir 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 também com o 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 !