Como retornar uma resposta de uma chamada assíncrona?

Eu tenho uma função foo que faz um pedido Ajax. Como devolver a resposta de foo ?

Eu tentei retornar um valor do retorno de chamada de success e também atribuir a resposta a uma variável local dentro da função e retorná-lo, mas nenhum desses métodos retorna uma resposta.

 function foo() { var result; $.ajax({ url: '...', success: function(response) { result = response; // return response; // <- I tried that one as well } }); return result; } var result = foo(); // It always ends up being `undefined`. 
4687
08 янв. Felix Kling set Jan 08 2013-01-08 20:06 '13 às 20:06 2013-01-08 20:06
@ 39 respostas
  • 1
  • 2

→ Para uma explicação mais geral do comportamento assíncrono em diferentes exemplos, consulte Por que minha variável não muda depois que eu a altero dentro de uma função? - link assíncrono para o código

→ Se você já entendeu o problema, vá para possíveis soluções abaixo.

Esse problema

A em Ajax significa assíncrono . Isso significa que enviar uma solicitação (ou melhor, receber uma resposta) é removido do fluxo normal de execução. No seu exemplo, $.ajax retorna imediatamente e a próxima instrução return result; , é executado antes mesmo que a função que você passou como callback de success seja chamada.

Aqui está uma analogia que, esperançosamente, esclarece a diferença entre um fluxo síncrono e assíncrono:

síncrono

Imagine que você está chamando um amigo e pedindo para ele encontrar algo para você. Embora possa levar algum tempo, você espera no telefone e olha para o espaço até que seu amigo lhe dê a resposta que você precisa.

A mesma coisa acontece quando você faz uma chamada de função que contém código "normal":

 function findItem() { var item; while(item_not_found) { // search } return item; } var item = findItem(); // Do something with item doSomethingElse(); 

Mesmo findItem demore muito tempo para executar findItem , qualquer código que se segue var item = findItem(); deve esperar até que a função retorne um resultado.

Assíncrono

Você liga para seu amigo novamente pelo mesmo motivo. Mas desta vez você diz a ele que está com pressa, e ele deve ligar de volta para o seu celular. Você desliga, sai de casa e faz tudo que planejou. Assim que seu amigo ligar de volta, você lidará com as informações que ele lhe deu.

Isto é exatamente o que acontece quando você faz uma requisição Ajax.

 findItem(function(item) { // Do something with item }); doSomethingElse(); 

Em vez de esperar por uma resposta, a execução continua imediatamente e a instrução é executada após uma chamada Ajax. Para finalmente receber uma resposta, você fornece uma função que será chamada depois de receber a resposta, um retorno de chamada (note algo, “chamar de volta”). Qualquer declaração após esta chamada é executada antes da chamada de retorno.


Solução (s)

Adote a natureza assíncrona do javascript! Embora algumas operações assíncronas forneçam contrapartes síncronas (como "Ajax"), elas geralmente não são recomendadas para uso, especialmente no contexto do navegador.

Por que isso é ruim, você pergunta?

O JavaScript é executado no encadeamento da interface do usuário do navegador, e qualquer processo demorado bloqueia a interface do usuário, o que faz com que ele não responda. Além disso, há um limite de tempo de execução superior para o JavaScript, e o navegador perguntará ao usuário se continuará a execução ou não.

Tudo isso é realmente uma experiência ruim para o usuário. O usuário não poderá dizer se tudo está funcionando corretamente ou não. Além disso, o efeito será pior para usuários com conexão lenta.

Em seguida, analisamos três soluções diferentes que são construídas umas sobre as outras:

  • Promessas com async/await await (ES2017 +, disponível em navegadores mais antigos se você usar um transportador ou um regenerador)
  • Callbacks (popular no site)
  • Promessas com then() (ES2015 +, disponível em navegadores mais antigos se você usar uma das muitas bibliotecas prometidas)

Todos os três estão disponíveis nos navegadores atuais e no nó 7+.


ES2017 +: promete com espera async/await await

A versão ECMAScript lançada em 2017 introduziu suporte a sintaxe para funções assíncronas. Com async e await você pode escrever de forma assíncrona em "estilo síncrono". O código ainda é assíncrono, mas mais fácil de ler / entender.

async/await baseado em promessas: a função async sempre retorna uma promessa. await "desembrulha" a promessa e o resultado no significado da promessa foi resolvido ou dá um erro se a promessa foi rejeitada.

Importante: Você só pode usar o await dentro de uma função async . No momento, no nível mais alto, o await ainda não é suportado, portanto, talvez seja necessário fazer com que o IIFE assíncrono inicie o contexto async .

Você pode ler mais sobre async e await no MDN.

Aqui está um exemplo que é baseado 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); })(); 

As versões atuais do navegador e do host suportam async/await . Você também pode manter ambientes antigos convertendo seu código em ES5 usando um regenerador (ou ferramentas usando um regenerador, como o Babel ).


Deixe funções aceitar retornos de chamada.

Um retorno de chamada é simplesmente uma função passada para outra função. Essa outra função pode chamar uma função passada sempre que estiver pronta. No contexto de um processo assíncrono, um retorno de chamada será chamado sempre que um processo assíncrono for executado. Normalmente, o resultado é enviado para o retorno de chamada.

No exemplo da questão, você pode forçar o foo a aceitar um retorno de chamada e usá-lo como um retorno de chamada bem- success . 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 para a função:

 function myCallback(result) { // Code that depends on 'result' } foo(myCallback); 

foo si é definido da seguinte forma:

 function foo(callback) { $.ajax({ // ... success: callback }); } 

callback se referirá à função que passamos para foo quando o chamamos, e simplesmente o passamos para o success . Ou seja assim que a solicitação do Ajax for bem-sucedida, $.ajax invocará o callback e $.ajax resposta para o retorno de chamada (que pode ser referenciado usando o result , pois é assim que definimos o retorno de chamada).

Você também pode processar a resposta antes de encaminhá-la para o retorno de chamada:

 function foo(callback) { $.ajax({ // ... success: function(response) { // For example, filter the response callback(filtered_response); } }); } 

Escrever código usando retornos de chamada é mais fácil do que parece. Afinal, o JavaScript no navegador é altamente dependente de eventos (eventos DOM). Obter uma resposta do Ajax nada mais é do que um evento.
Dificuldades podem surgir quando você tem que trabalhar com código de terceiros, mas a maioria dos problemas pode ser resolvida simplesmente através do fluxo do aplicativo.


ES2015 +: promessas com então ()

A Promise API é um novo recurso do ECMAScript 6 (ES2015), mas já possui um bom suporte ao navegador . Há também muitas bibliotecas que implementam as Promessas de API padrão e fornecem métodos adicionais para simplificar o uso e a composição de funções assíncronas (por exemplo, bluebird ).

Promessas são contêineres para valores futuros. Quando uma promessa recebe um valor (é permitido) ou quando é cancelada (rejeitada), ele notifica todos os seus "ouvintes" que desejam acessar esse valor.

A vantagem sobre as chamadas de retorno simples é que elas permitem separar seu código e é mais fácil compô-las.

Aqui está um exemplo simples de usar promessas:

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

Com relação à nossa chamada Ajax, poderíamos usar as seguintes promessas:

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

Uma descrição de todos os benefícios que eles prometem oferecer está além do escopo desta resposta, mas se você estiver escrevendo um novo código, você deve considerá-los seriamente. Eles fornecem excelente abstração e separação do seu código.

Mais informações sobre promessas: HTML5 - rochas - promessas de JavaScript

Nota: objetos pendentes jQuery

Objetos adiados são uma implementação personalizada de promessas no jQuery (antes da padronização da API Promise). Eles quase se comportam como promessas, mas expõem uma API ligeiramente diferente.

Todo jQuery Ajax método já retorna um "objeto pendente" (na verdade uma promessa de um objeto pendente), que você pode simplesmente retornar de sua função:

 function ajax() { return $.ajax(...); } ajax().done(function(result) { // Code depending on result }).fail(function() { // An error occurred }); 

Nota: a promessa acabou

Lembre-se de que promessas e objetos adiados são apenas contêineres para valor futuro, e não o próprio valor. 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 não compreende os problemas assíncronos acima. Em particular, $.ajax() não congela o código enquanto verifica a página '/ password' no seu servidor - envia uma requisição ao servidor e, enquanto espera, retorna imediatamente o objeto jQuery Ajax Adiado, não a resposta do servidor. Isso significa que a if sempre receberá esse objeto adiado, processará como true e agirá como se o usuário estivesse logado. Não é bom

Mas corrija facilmente:

 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 eu disse, algumas (!) Operações assíncronas têm contrapartes síncronas. Eu não defendo o uso deles, mas, por questão de integridade, veja como você deve realizar uma chamada síncrona:

Não jQuery

Se você estiver usando diretamente o objeto XMLHTTPRequest , passe false como o terceiro argumento .open .

Jquery

Se você usar o jQuery , poderá definir o parâmetro async como false . Por favor, note que esta opção está obsoleta desde o jQuery 1.8. Então você ainda pode usar o callback de success ou o acesso à propriedade responseText 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., você deve alterá-lo para $.ajax (já que você só pode passar parâmetros de configuração para $.ajax ).

Cuidado Não é possível fazer uma solicitação JSONP síncrona. O JSONP é sempre de natureza assíncrona (outra razão para não considerar essa opção).

5011
08 янв. A resposta é dada por Felix Kling Jan 08 2013-01-08 20:06 '13 às 20:06 2013-01-08 20:06

Se você não usa o jQuery no seu código, esta resposta é para você.

Seu código deve ser algo assim:

 function foo() { var httpRequest = new XMLHttpRequest(); httpRequest.open('GET', "/echo/json"); httpRequest.send(); return httpRequest.responseText; } var result = foo(); // always ends up being 'undefined' 

Felix Kling fez um ótimo trabalho escrevendo a resposta para as pessoas que usam o jQuery para AJAX, e decidi oferecer uma alternativa para pessoas que não o fazem.

( Observe que, para aqueles que usam a nova fetch API, Angular ou promessas, adicionei outra resposta abaixo )


O que você encontra

Este é um breve resumo da "Explicação do problema" de outra resposta, se você não tiver certeza, depois de ler isto, leia isto.

Um em AJAX significa assíncrono . Isso significa que enviar uma solicitação (ou melhor, receber uma resposta) é removido do fluxo normal de execução. No seu exemplo, .send retornado imediatamente e a próxima instrução return result; é executado antes mesmo que a função que você passou como callback de success seja chamada.

Isso significa que, quando você retorna, o ouvinte especificado ainda não foi atendido, o que significa que o valor retornado não foi determinado.

Analogia simples

 function getFive(){ var a; setTimeout(function(){ a=5; },10); return a; } 

(Feeddle)

O valor de retorno de a é undefined , uma vez que a parte a=5 ainda não foi executada. O AJAX faz isso: você retorna o valor antes que o servidor possa dizer ao seu navegador qual é o valor.

Uma solução possível para esse problema é reutilizar o código, informando seu programa sobre o que fazer quando o cálculo for concluído.

 function onComplete(a){ // When the code completes, do this alert(a); } function getFive(whenDone){ var a; setTimeout(function(){ a=5; whenDone(a); },10); } 

Isso é chamado CPS . Basicamente, getFive ação que precisa ser executada quando ela é concluída, informamos ao nosso código como reagir quando o evento termina (por exemplo, nossa chamada AJAX ou, neste caso, um tempo limite).

Use:

 getFive(onComplete); 

Qual deve alertar "5" na tela. (Violino)

Soluções possíveis

border=0

Existem basicamente duas maneiras de resolver este problema:

  • Faça uma chamada AJAX síncrona (chame-o de SJAX).
  • Reestruture seu código para funcionar corretamente com retornos de chamada.

1. AJAX Synchronous - não faça isso !!

Quanto ao AJAX síncrono, não faça isso! A resposta de Felix dá alguns argumentos fortes para porque esta é uma má ideia. Para resumir, ele congelará o navegador do usuário até que o servidor retorne uma resposta e crie uma interface de usuário muito ruim. Aqui está outro resumo do MDN sobre o motivo:

XMLHttpRequest suporta comunicação síncrona e assíncrona. Em geral, no entanto, as solicitações assíncronas devem ser preferíveis às solicitações síncronas por motivos de desempenho.

Em suma, os pedidos síncronos bloqueiam a execução do código ... isso pode causar sérios problemas ...

Se você precisar fazer isso, pode passar o sinalizador: veja como:

 var request = new XMLHttpRequest(); request.open('GET', 'yourURL', false); // `false` makes the request synchronous request.send(null); if (request.status === 200) {// That HTTP for 'ok' console.log(request.responseText); } 

2. Código de Reestruturação

Deixe sua função aceitar o retorno de chamada. No exemplo, o código foo pode aceitar um retorno de chamada. Vamos dizer ao nosso código como reagir quando o foo terminar.

Então:

 var result = foo(); // code that depends on `result` goes here 

torna-se:

 foo(function(result) { // code that depends on `result` }); 

Aqui nós passamos uma função anônima, mas podemos passar um link para uma função existente, assim:

 function myHandler(result) { // code that depends on `result` } foo(myHandler); 

Para obter mais informações sobre como esse tipo de retorno de chamada é executado, verifique a resposta do Felix.

Agora vamos definir o próprio foo para agir de acordo

 function foo(callback) { var httpRequest = new XMLHttpRequest(); httpRequest.onload = function(){ // when the request is loaded callback(httpRequest.responseText);// we're calling our method }; httpRequest.open('GET', "/echo/json"); httpRequest.send(); } 

(violino)

Agora nossa função foo aceita uma ação que inicia quando o AJAX é concluído com sucesso, podemos continuar verificando se o status de resposta 200 está respondendo e agindo de acordo (criar um manipulador de erros, etc.). Uma solução eficaz para o nosso problema.

Se você ainda tiver problemas para entender isso, leia o Guia de Introdução do AJAX no MDN.

953
30 мая '13 в 2:30 2013-05-30 02:30 Resposta dada por Benjamin Gruenbaum 30 de maio de 2013 às 2:30 2013-05-30 02:30

XMLHttpRequest 2 (Primeiro leia as respostas de Benjamin Grünbaum e Felix Kling )

Se você não estiver usando o jQuery e quiser obter um XMLHttpRequest 2 curto e agradável que funcione em navegadores modernos, assim como em navegadores de dispositivos móveis, sugiro usá-lo da seguinte maneira:

 function ajax(a, b, c){ // URL, callback, just a placeholder c = new XMLHttpRequest; c.open('GET', a); c.onload = b; c.send() } 

Como você pode ver:

  1. É mais curto que todas as outras funções listadas.
  2. O retorno de chamada é estabelecido diretamente (portanto, nenhum fechamento desnecessário desnecessário).
  3. Há outras situações que não me lembro que tornam o XMLHttpRequest 1 irritante.

Há duas maneiras de obter uma resposta para essa chamada Ajax (três usando o nome da variável XMLHttpRequest):

O mais simples:

 this.response 

Ou, se por algum motivo você bind() callback com uma classe:

 e.target.response 

Exemplo:

 function callback(e){ console.log(this.response); } ajax('URL', callback); 

Ou (as melhores funções anônimas acima são sempre um problema):

 ajax('URL', function(e){console.log(this.response)}); 

Não há nada mais fácil.

Agora, algumas pessoas provavelmente dirão que é melhor usar onreadystatechange ou até mesmo o nome da variável XMLHttpRequest. Isso está errado.

Explore os recursos avançados do XMLHttpRequest

Todos os navegadores modernos são suportados. E eu posso confirmar que eu uso essa abordagem porque existe XMLHttpRequest 2. Eu nunca tive nenhum problema em todos os navegadores que eu uso.

onreadystatechange é útil somente se você deseja obter os cabeçalhos no estado 2.

Usar o nome da variável XMLHttpRequest é outro grande erro, porque você precisa chamar de volta dentro de closures onload / oreadystatechange, caso contrário, você o perdeu.


Agora, se você quiser algo mais complexo usando post e FormData, pode facilmente estender essa função:

 function x(a, b, e, d, c){ // URL, callback, method, formdata or {key:val},placeholder c = new XMLHttpRequest; c.open(e||'get', a); c.onload = b; c.send(d||null) } 

Mais uma vez ... esta é uma função muito curta, mas recebe e publica.

Exemplos de uso:

 x(url, callback); // By default it get so no need to set x(url, callback, 'post', {'key': 'val'}); // No need to set post data 

Ou passe o elemento de formulário completo ( document.getElementsByTagName('form')[0] ):

 var fd = new FormData(form); x(url, callback, 'post', fd); 

Ou defina alguns valores personalizados:

 var fd = new FormData(); fd.append('key', 'val') x(url, callback, 'post', fd); 

Como você pode ver, eu não implementei sincronização ... isso é ruim.

Dito isto ... por que não fazê-lo de uma maneira simples?


Como mencionado no comentário, o uso de error synchronous viola completamente o significado da resposta. O que é uma boa maneira de usar o Ajax corretamente?

Manipulador de erro

 function x(a, b, e, d, c){ // URL, callback, method, formdata or {key:val}, placeholder c = new XMLHttpRequest; c.open(e||'get', a); c.onload = b; c.onerror = error; c.send(d||null) } function error(e){ console.log('--Error--', this.type); console.log('this: ', this); console.log('Event: ', e) } function displayAjax(e){ console.log(e, this); } x('WRONGURL', displayAjax); 

No script acima, você tem um manipulador de erros que é definido estaticamente, portanto, não compromete a função. O manipulador de erros pode ser usado para outras funções.

Mas, para realmente obter o erro, a única maneira é escrever a URL errada, e nesse caso cada navegador dá um erro.