→ Para obter uma explicação mais geral do comportamento assíncrono com exemplos diferentes, consulte Por que minha variável é inalterada depois que a modifico dentro de uma função? - Referência de código assíncrona
→ Se você já entendeu o problema, pule para as possíveis soluções abaixo.
O problema
O Um em Ajax significa assíncrona . Isso significa que o envio da solicitação (ou melhor, o recebimento da resposta) é retirado do fluxo de execução normal. No seu exemplo, $.ajax
retorna imediatamente e a próxima instrução return result;
,, é executada antes da função que você passou como success
retorno de chamada ser chamada.
Aqui está uma analogia que, esperançosamente, torna mais clara a diferença entre fluxo síncrono e assíncrono:
Síncrono
Imagine que você telefona para um amigo e pede que ele procure algo para você. Embora possa demorar um pouco, você espera no telefone e fica olhando para o espaço, até que seu amigo lhe dê a resposta que você precisava.
O mesmo está acontecendo quando você faz uma chamada de função contendo código "normal":
function findItem() {
var item;
while(item_not_found) {
// search
}
return item;
}
var item = findItem();
// Do something with item
doSomethingElse();
Mesmo que findItem
demore muito tempo para executar, qualquer código que vem depois var item = findItem();
precisa esperar até que a função retorne o resultado.
Assíncrono
Você chama seu amigo novamente pelo mesmo motivo. Mas desta vez você diz a ele que está com pressa e ele deve ligar de volta no seu celular. Você desliga, sai de casa e faz o que planeja fazer. Quando seu amigo ligar de volta, você estará lidando com as informações que ele lhe deu.
É exatamente o que está acontecendo quando você faz uma solicitação do Ajax.
findItem(function(item) {
// Do something with item
});
doSomethingElse();
Em vez de aguardar a resposta, a execução continua imediatamente e a instrução após a execução da chamada do Ajax. Para obter a resposta eventualmente, você fornece uma função a ser chamada assim que a resposta é recebida, um retorno de chamada (observe algo? Ligar de volta ?). Qualquer declaração que vem depois dessa chamada é executada antes da chamada de retorno.
Solução (s)
Adote a natureza assíncrona do JavaScript! Embora certas operações assíncronas forneçam contrapartes síncronas (o mesmo acontece com "Ajax"), geralmente é desencorajado usá-las, especialmente em um contexto de navegador.
Por que isso é ruim, você pergunta?
O JavaScript é executado no segmento da interface do usuário do navegador e qualquer processo de longa duração bloqueará a interface do usuário, deixando de responder. Além disso, há um limite superior no tempo de execução do JavaScript e o navegador perguntará ao usuário se continua ou não a execução.
Tudo isso é uma péssima experiência do usuário. O usuário não poderá saber se tudo está funcionando bem ou não. Além disso, o efeito será pior para usuários com uma conexão lenta.
A seguir, veremos três soluções diferentes, todas construídas umas sobre as outras:
- Promessas com
async/await
(ES2017 +, disponível em navegadores mais antigos se você usar um transpiler ou regenerador)
- Retornos de chamada (populares no nó)
- Promessas com
then()
(ES2015 +, disponível em navegadores mais antigos se você usar uma das muitas bibliotecas de promessas)
Todos os três estão disponíveis nos navegadores atuais e no nó 7+.
ES2017 +: Promessas com async/await
A versão do ECMAScript lançada em 2017 apresentou suporte no nível de sintaxe para funções assíncronas. Com a ajuda de async
e await
, você pode escrever assíncrono em um "estilo síncrono". O código ainda é assíncrono, mas é mais fácil de ler / entender.
async/await
se baseia em promessas: uma async
função sempre retorna uma promessa. await
"desembrulha" uma promessa e resulta no valor com o qual a promessa foi resolvida ou gera um erro se a promessa foi rejeitada.
Importante: Você só pode usar await
dentro de uma async
função. No momento, o nível superior await
ainda não é suportado; portanto, você pode precisar criar um IIFE assíncrono ( expressão de função chamada imediatamente ) para iniciar um async
contexto.
Você pode ler mais sobre async
e await
no MDN.
Aqui está um exemplo que se baseia no atraso acima:
// Using 'superagent' which will return a promise.
var superagent = require('superagent')
// This is isn't declared as `async` because it already returns a promise
function delay() {
// `delay` returns a promise
return new Promise(function(resolve, reject) {
// Only `delay` is able to resolve or reject the promise
setTimeout(function() {
resolve(42); // After 3 seconds, resolve the promise with value 42
}, 3000);
});
}
async function getAllBooks() {
try {
// GET a list of book IDs of the current user
var bookIDs = await superagent.get('/user/books');
// wait for 3 seconds (just for the sake of this example)
await delay();
// GET information about each book
return await superagent.get('/books/ids='+JSON.stringify(bookIDs));
} catch(error) {
// If any of the awaited promises was rejected, this catch block
// would catch the rejection reason
return null;
}
}
// Start an IIFE to use `await` at the top level
(async function(){
let books = await getAllBooks();
console.log(books);
})();
Suporte para versões atuais do navegador e do nóasync/await
. Você também pode oferecer suporte a ambientes mais antigos, transformando seu código no ES5 com a ajuda do regenerador (ou ferramentas que usam o regenerador, como Babel ).
Permitir que as funções aceitem retornos de chamada
Um retorno de chamada é simplesmente uma função passada para outra função. Essa outra função pode chamar a função passada sempre que estiver pronta. No contexto de um processo assíncrono, o retorno de chamada será chamado sempre que o processo assíncrono for concluído. Normalmente, o resultado é passado para o retorno de chamada.
No exemplo da pergunta, você pode foo
aceitar um retorno de chamada e usá-lo como success
retorno de chamada. Então, é isso
var result = foo();
// Code that depends on 'result'
torna-se
foo(function(result) {
// Code that depends on 'result'
});
Aqui nós definimos a função "inline", mas você pode passar qualquer referência de função:
function myCallback(result) {
// Code that depends on 'result'
}
foo(myCallback);
foo
em si é definido da seguinte forma:
function foo(callback) {
$.ajax({
// ...
success: callback
});
}
callback
irá se referir à função para a qual passamos foo
quando a chamamos e simplesmente a passamos para success
. Ou seja, uma vez que a solicitação do Ajax for bem-sucedida, $.ajax
chamará callback
e passará a resposta para o retorno de chamada (que pode ser referido result
, pois é assim que definimos o retorno de chamada).
Você também pode processar a resposta antes de passá-la para o retorno de chamada:
function foo(callback) {
$.ajax({
// ...
success: function(response) {
// For example, filter the response
callback(filtered_response);
}
});
}
É mais fácil escrever código usando retornos de chamada do que parece. Afinal, o JavaScript no navegador é fortemente orientado a eventos (eventos DOM). Receber a resposta do Ajax nada mais é do que um evento.
Dificuldades podem surgir quando você precisa trabalhar com código de terceiros, mas a maioria dos problemas pode ser resolvida apenas pensando no fluxo do aplicativo.
ES2015 +: Promessas com then ()
A API Promise é um novo recurso do ECMAScript 6 (ES2015), mas já possui um bom suporte ao navegador . Existem também muitas bibliotecas que implementam a API Promises padrão e fornecem métodos adicionais para facilitar o uso e a composição de funções assíncronas (por exemplo, bluebird ).
Promessas são recipientes para valores futuros . Quando a promessa recebe o valor (é resolvido ) ou quando é cancelada ( rejeitada ), notifica todos os seus "ouvintes" que desejam acessar esse valor.
A vantagem sobre os retornos de chamada simples é que eles permitem desacoplar seu código e são mais fáceis de compor.
Aqui está um exemplo simples de usar uma promessa:
function delay() {
// `delay` returns a promise
return new Promise(function(resolve, reject) {
// Only `delay` is able to resolve or reject the promise
setTimeout(function() {
resolve(42); // After 3 seconds, resolve the promise with value 42
}, 3000);
});
}
delay()
.then(function(v) { // `delay` returns a promise
console.log(v); // Log the value once it is resolved
})
.catch(function(v) {
// Or do something else if it is rejected
// (it would not happen in this example, since `reject` is not called).
});
Aplicados à nossa chamada do Ajax, poderíamos usar promessas como esta:
function ajax(url) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.onload = function() {
resolve(this.responseText);
};
xhr.onerror = reject;
xhr.open('GET', url);
xhr.send();
});
}
ajax("/echo/json")
.then(function(result) {
// Code depending on result
})
.catch(function() {
// An error occurred
});
A descrição de todas as vantagens que a promessa oferece está além do escopo desta resposta, mas se você escrever um novo código, considere-o seriamente. Eles fornecem uma ótima abstração e separação do seu código.
Mais informações sobre promessas: rochas HTML5 - Promessas JavaScript
Nota lateral: objetos adiados do jQuery
Objetos adiados são a implementação personalizada de promessas do jQuery (antes da padronização da API da promessa). Eles se comportam quase como promessas, mas expõem uma API ligeiramente diferente.
Todo método Ajax do jQuery já retorna um "objeto adiado" (na verdade, a promessa de um objeto adiado) que você pode retornar da sua função:
function ajax() {
return $.ajax(...);
}
ajax().done(function(result) {
// Code depending on result
}).fail(function() {
// An error occurred
});
Nota lateral: dicas da promessa
Lembre-se de que promessas e objetos adiados são apenas contêineres para um valor futuro, eles não são o valor em si. Por exemplo, suponha que você tenha o seguinte:
function checkPassword() {
return $.ajax({
url: '/password',
data: {
username: $('#username').val(),
password: $('#password').val()
},
type: 'POST',
dataType: 'json'
});
}
if (checkPassword()) {
// Tell the user they're logged in
}
Este código entende mal os problemas de assincronia acima. Especificamente, $.ajax()
não congela o código enquanto verifica a página '/ senha' no servidor - envia uma solicitação ao servidor e, enquanto espera, retorna imediatamente um objeto adiado jQuery Ajax Deferred, não a resposta do servidor. Isso significa que a if
instrução sempre obterá esse objeto adiado, trate-o como true
e continue como se o usuário estivesse logado. Nada bom.
Mas a correção é fácil:
checkPassword()
.done(function(r) {
if (r) {
// Tell the user they're logged in
} else {
// Tell the user their password was bad
}
})
.fail(function(x) {
// Tell the user something bad happened
});
Não recomendado: chamadas síncronas "Ajax"
Como mencionei, algumas operações assíncronas (!) Possuem contrapartes síncronas. Eu não defendo o uso deles, mas, por uma questão de integridade, eis como você faria uma chamada síncrona:
Sem jQuery
Se você usar um XMLHTTPRequest
objeto diretamente , passe false
como terceiro argumento para .open
.
jQuery
Se você usa o jQuery , pode definir a async
opção para false
. Observe que esta opção está obsoleta desde o jQuery 1.8. Você ainda pode usar um success
retorno de chamada ou acessar a responseText
propriedade do objeto jqXHR :
function foo() {
var jqXHR = $.ajax({
//...
async: false
});
return jqXHR.responseText;
}
Se você usar qualquer outro método jQuery Ajax, como $.get
, $.getJSON
etc., precisará alterá-lo para $.ajax
(já que você só pode passar parâmetros de configuração para $.ajax
).
Atenção! Não é possível fazer uma solicitação JSONP síncrona . O JSONP por sua própria natureza é sempre assíncrono (mais um motivo para nem considerar essa opção).