Como faço para retornar a resposta de uma chamada assíncrona?

5511

Eu tenho uma função fooque faz uma solicitação Ajax. Como posso devolver a resposta foo?

Tentei retornar o valor do successretorno de chamada, além de atribuir a resposta a uma variável local dentro da função e retornar essa, mas nenhuma dessas maneiras realmente retorna a 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`.
Felix Kling
fonte

Respostas:

5703

→ 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, $.ajaxretorna imediatamente e a próxima instrução return result;,, é executada antes da função que você passou como successretorno 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 findItemdemore 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 comasync/await (ES2017 +, disponível em navegadores mais antigos se você usar um transpiler ou regenerador)
  • Retornos de chamada (populares no nó)
  • Promessas comthen() (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 asynce 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/awaitse baseia em promessas: uma asyncfunçã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 awaitdentro de uma asyncfunção. No momento, o nível superior awaitainda não é suportado; portanto, você pode precisar criar um IIFE assíncrono ( expressão de função chamada imediatamente ) para iniciar um asynccontexto.

Você pode ler mais sobre asynce awaitno 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 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 fooaceitar um retorno de chamada e usá-lo como successretorno 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
    });
}

callbackirá se referir à função para a qual passamos fooquando a chamamos e simplesmente a passamos para success. Ou seja, uma vez que a solicitação do Ajax for bem-sucedida, $.ajaxchamará callbacke 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 ifinstrução sempre obterá esse objeto adiado, trate-o como truee 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 XMLHTTPRequestobjeto diretamente , passe falsecomo terceiro argumento para .open.

jQuery

Se você usa o jQuery , pode definir a asyncopção para false. Observe que esta opção está obsoleta desde o jQuery 1.8. Você ainda pode usar um successretorno de chamada ou acessar a responseTextpropriedade do objeto jqXHR :

function foo() {
    var jqXHR = $.ajax({
        //...
        async: false
    });
    return jqXHR.responseText;
}

Se você usar qualquer outro método jQuery Ajax, como $.get, $.getJSONetc., 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).

Felix Kling
fonte
74
@ Pommy: Se você quiser usar o jQuery, precisará incluí-lo. Consulte docs.jquery.com/Tutorials:Getting_Started_with_jQuery .
precisa saber é o seguinte
11
No Solution 1, sub jQuery, eu não conseguia entender esta linha: If you use any other jQuery AJAX method, such as $.get, $.getJSON, etc., you have them to $.ajax.(Sim, eu percebo que meu nick é um pouco irônico, neste caso)
cssyphus
32
@ gibberish: Mmmh, eu não sei como isso pode ser esclarecido. Você vê como fooé chamado e uma função é passada para ele ( foo(function(result) {....});)? resulté usado dentro desta função e é a resposta da solicitação do Ajax. Para se referir a essa função, o primeiro parâmetro de foo é chamado callbacke atribuído em successvez de uma função anônima. Então, $.ajaxligará callbackquando a solicitação for bem-sucedida. Eu tentei explicar um pouco mais.
Felix Kling 06/02
43
O Chat para esta pergunta está inoperante, por isso não sei onde propor alterações descritas, mas proponho: 1) Altere a parte síncrona para uma discussão simples de por que é ruim, sem um exemplo de código de como fazê-lo. 2) Remova / mescle os exemplos de retorno de chamada para mostrar apenas a abordagem adiada mais flexível, que acho que também pode ser um pouco mais fácil de seguir para quem está aprendendo Javascript.
Chris Moschini
14
@ Jessica: Eu acho que você entendeu mal essa parte da resposta. Você não pode usar $.getJSONse quiser que a solicitação do Ajax seja síncrona. No entanto, você não deve querer que a solicitação seja síncrona, para que isso não se aplique. Você deve usar retornos de chamada ou promessas para lidar com a resposta, conforme explicado anteriormente na resposta.
Felix Kling
1071

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

Seu código deve ser algo parecido com isto:

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 bom trabalho ao escrever uma resposta para as pessoas que usam o jQuery para AJAX. Decidi fornecer uma alternativa para as pessoas que não o são.

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


O que você está enfrentando

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

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, .sendretorna imediatamente e a próxima instrução return result;,, é executada antes da função que você passou como successretorno de chamada ser chamada.

Isso significa que, quando você está retornando, o ouvinte que você definiu ainda não foi executado, o que significa que o valor que você está retornando não foi definido.

Aqui está uma analogia simples

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

(Violino)

O valor aretornado é undefinedporque a a=5peça ainda não foi executada. O AJAX age assim: você retorna o valor antes que o servidor tenha a chance de informar ao seu navegador qual é esse valor.

Uma solução possível para esse problema é codificar de forma reativa , informando ao seu programa 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 de CPS . Basicamente, estamos passando getFiveuma ação para executar quando ela é concluída, informando ao nosso código como reagir quando um evento é concluído (como nossa chamada AJAX ou, neste caso, o tempo limite).

O uso seria:

getFive(onComplete);

O que deve alertar "5" para a tela. (Violino) .

Soluções possíveis

Existem basicamente duas maneiras de resolver isso:

  1. Torne a chamada AJAX síncrona (vamos chamá-la de SJAX).
  2. Reestruture seu código para funcionar corretamente com retornos de chamada.

1. AJAX síncrono - não faça isso !!

Quanto ao AJAX síncrono, não faça isso! A resposta de Felix levanta alguns argumentos convincentes sobre por que é uma má ideia. Para resumir, congelará o navegador do usuário até que o servidor retorne a resposta e crie uma experiência muito ruim para o usuário. Aqui está outro pequeno resumo do MDN sobre o porquê:

XMLHttpRequest suporta comunicações síncronas e assíncronas. Em geral, no entanto, solicitações assíncronas devem ser preferidas a solicitações síncronas por motivos de desempenho.

Em suma, solicitações síncronas bloqueiam a execução do código ... ... isso pode causar problemas sérios ...

Se você precisar fazer isso, pode passar uma bandeira: Aqui está como:

var request = new XMLHttpRequest();
request.open('GET', 'yourURL', false);  // `false` makes the request synchronous
request.send(null);

if (request.status === 200) {// That's HTTP for 'ok'
  console.log(request.responseText);
}

2. Reestruturar o código

Deixe sua função aceitar um retorno de chamada. No exemplo de código, você foopode aceitar um retorno de chamada. Nós estaremos dizendo ao nosso código como reagir quando fooconcluir.

Assim:

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

Torna-se:

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

Aqui passamos uma função anônima, mas podemos facilmente passar uma referência a uma função existente, fazendo com que pareça:

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

Para mais detalhes sobre como esse tipo de design de retorno de chamada é feito, consulte a resposta de 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 fizemos nossa função foo aceitar uma ação a ser executada quando o AJAX for concluído com êxito. Podemos estender isso ainda mais, verificando se o status da resposta não é 200 e agindo de acordo (crie um manipulador de falhas e outros). Resolvendo efetivamente nosso problema.

Se você ainda está com dificuldades para entender isso, leia o guia de introdução do AJAX na MDN.

Benjamin Gruenbaum
fonte
20
"solicitações síncronas bloqueiam a execução do código e podem vazar memória e eventos" como uma solicitação síncrona pode vazar memória?
Matthew G
10
@ MatthewG Adicionei uma recompensa nessa questão , vou ver o que posso pescar. Estou removendo a citação da resposta nesse meio tempo.
Benjamin Gruenbaum
17
Apenas para referência, o XHR 2 nos permite usar o onloadmanipulador, que só dispara quando readyStateé 4. Obviamente, não é suportado no IE8. (iirc, pode precisar de confirmação.) #
Florian Margaine
9
Sua explicação de como passar uma função anônima como retorno de chamada é válida, mas enganosa. O exemplo var bar = foo (); está pedindo que uma variável seja definida, enquanto o seu foo sugerido (functim () {}); não define barra
Robbie Averill
396

XMLHttpRequest 2 (leia primeiro as respostas de Benjamin Gruenbaum e Felix Kling )

Se você não usa o jQuery e deseja um XMLHttpRequest 2 curto e agradável que funcione nos navegadores modernos e também nos navegadores móveis, sugiro usá-lo desta 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 é definido diretamente (portanto, nenhum fechamento extra desnecessário).
  3. Ele usa o novo onload (para que você não precise verificar o status de readystate &&)
  4. Existem outras situações em que não me lembro que tornam o XMLHttpRequest 1 irritante.

Há duas maneiras de obter a resposta dessa chamada do Ajax (três usando o nome de var XMLHttpRequest):

O mais simples:

this.response

Ou se, por algum motivo, você bind()retornar a chamada para uma classe:

e.target.response

Exemplo:

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

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

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

Nada mais fácil.

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

Confira os recursos avançados do XMLHttpRequest

Ele suporta todos os * navegadores modernos. E posso confirmar que estou usando essa abordagem, já que o XMLHttpRequest 2 existe. Nunca tive nenhum tipo de problema em todos os navegadores que uso.

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

Usar o XMLHttpRequestnome da variável é outro grande erro, pois você precisa executar o retorno de chamada dentro dos fechamentos onload / oreadystatechange, caso contrário você o perdeu.


Agora, se você quiser algo mais complexo usando post e FormData, poderá estender facilmente esta 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 ... é uma função muito curta, mas recebe e publica.

Exemplos de uso:

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

Ou passe um 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 a sincronização ... é uma coisa ruim.

Dito isto ... por que não fazer isso da maneira mais fácil?


Como mencionado no comentário, o uso do erro && synchronous quebra completamente o ponto da resposta. Qual é uma maneira curta e agradável de usar o Ajax da maneira correta?

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ê possui um manipulador de erros definido estaticamente para que não comprometa a função. O manipulador de erros também pode ser usado para outras funções.

Mas realmente obter um erro, a única maneira é escrever um URL errado; nesse caso, todo navegador gera um erro.

Os manipuladores de erro talvez sejam úteis se você definir cabeçalhos personalizados, definir o responseType como buffer da matriz de blob ou qualquer outra coisa ...

Mesmo se você passar 'POSTAPAPAP' como método, isso não causará erro.

Mesmo se você passar 'fdggdgilfdghfldj' como formdata, isso não causará um erro.

No primeiro caso, o erro está dentro do displayAjax()under this.statusTextas Method not Allowed.

No segundo caso, simplesmente funciona. Você deve verificar no lado do servidor se passou os dados corretos da postagem.

o domínio cruzado não permitido gera erro automaticamente.

Na resposta a erro, não há códigos de erro.

Existe apenas o this.typeque está definido como erro.

Por que adicionar um manipulador de erros se você não tem controle total sobre erros? A maioria dos erros é retornada dentro disso na função de retorno de chamada displayAjax().

Portanto: não há necessidade de verificação de erros se você conseguir copiar e colar o URL corretamente. ;)

PS: Como o primeiro teste, eu escrevi x ('x', displayAjax) ... e ele obteve uma resposta total ... ??? Então, verifiquei a pasta em que o HTML está localizado e havia um arquivo chamado 'x.xml'. Portanto, mesmo se você esquecer a extensão do seu arquivo, XMLHttpRequest 2 ENCONTRA-O . EU LOL


Ler um arquivo síncrono

Não faça isso.

Se você deseja bloquear o navegador por um tempo, carregue um bom .txtarquivo grande e síncrono.

function omg(a, c){ // URL
  c = new XMLHttpRequest;
  c.open('GET', a, true);
  c.send();
  return c; // Or c.response
}

Agora você pode fazer

 var res = omg('thisIsGonnaBlockThePage.txt');

Não há outra maneira de fazer isso de maneira não assíncrona. (Sim, com loop setTimeout ... mas sério?)

Outro ponto é ... se você trabalha com APIs ou apenas os arquivos da sua própria lista ou o que quer que use sempre funções diferentes para cada solicitação ...

Somente se você tiver uma página na qual carregue sempre o mesmo XML / JSON ou o que precisar, precisará de apenas uma função. Nesse caso, modifique um pouco a função Ajax e substitua b por sua função especial.


As funções acima são para uso básico.

Se você deseja estender a função ...

Sim você pode.

Estou usando muitas APIs e uma das primeiras funções que integro a todas as páginas HTML é a primeira função Ajax nesta resposta, apenas com GET ...

Mas você pode fazer muitas coisas com o XMLHttpRequest 2:

Criei um gerenciador de downloads (usando intervalos de ambos os lados com currículo, leitor de arquivos, sistema de arquivos), vários conversores de redimensionadores de imagens usando canvas, preenchendo bancos de dados SQL da web com base64images e muito mais ... Mas nesses casos, você deve criar uma função apenas para isso finalidade ... às vezes você precisa de um blob, buffers de matriz, pode definir cabeçalhos, substituir o tipo de mim e há muito mais ...

Mas a questão aqui é como retornar uma resposta do Ajax ... (adicionei uma maneira fácil.)

cocco
fonte
15
Embora essa resposta seja boa (e todos gostamos de XHR2 e publicar dados de arquivo e dados de várias partes é totalmente incrível) - isso mostra um açúcar sintático para postar XHR com JavaScript - você pode colocar isso em um post do blog (eu gostaria) ou até mesmo em uma biblioteca (não tenho certeza sobre o nome x, ajaxou xhrpode ser mais agradável :)). Não vejo como ele trata o retorno da resposta de uma chamada AJAX. (alguém ainda pode fazer var res = x("url")e não entender por que não funciona;)). Em uma nota lateral - seria legal se você retornasse cdo método para que os usuários possam se conectar erroretc.
Benjamin Gruenbaum
25
2.ajax is meant to be async.. so NO var res=x('url')..Esse é o ponto inteiro desta questão e respostas :)
Benjamin Gruenbaum
3
por que existe um parâmetro 'c' nas funções, se na primeira linha você está substituindo qualquer valor que tenha? estou esquecendo de algo?
Brian H.
2
Você pode usar parâmetros como um espaço reservado para evitar escrever várias vezes "var"
Cocco
11
@cocco Então você escreveu um código enganoso e ilegível em uma resposta SO , a fim de salvar algumas teclas digitadas? Por favor, não faça isso.
stone
316

Se você estiver usando promessas, esta resposta é para você.

Isso significa AngularJS, jQuery (com adiado), substituição nativa do XHR (busca), EmberJS, salvamento do BackboneJS ou qualquer biblioteca de nós que retorne promessas.

Seu código deve ser algo parecido com isto:

function foo() {
    var data;
    // or $.get(...).then, or request(...).then, or query(...).then
    fetch("/echo/json").then(function(response){
        data = response.json();
    });
    return data;
}

var result = foo(); // result is always undefined no matter what.

Felix Kling fez um bom trabalho ao escrever uma resposta para as pessoas que usam jQuery com retornos de chamada para AJAX. Eu tenho uma resposta para XHR nativo. Esta resposta é para uso genérico de promessas no front-end ou no back-end.


A questão central

O modelo de simultaneidade JavaScript no navegador e no servidor com NodeJS / io.js é assíncrono e reativo .

Sempre que você chama um método que retorna uma promessa, os thenmanipuladores são sempre executados de forma assíncrona - ou seja, após o código abaixo deles que não está em um.then manipulador.

Isso significa que, quando você está retornando, datao thenmanipulador que você definiu ainda não foi executado. Por sua vez, isso significa que o valor que você está retornando não foi definido com o valor correto no tempo.

Aqui está uma analogia simples para o problema:

    function getFive(){
        var data;
        setTimeout(function(){ // set a timer for one second in the future
           data = 5; // after a second, do this
        }, 1000);
        return data;
    }
    document.body.innerHTML = getFive(); // `undefined` here and not 5

O valor de dataé undefinedporque a data = 5peça ainda não foi executada. Provavelmente será executado em um segundo, mas a essa altura é irrelevante para o valor retornado.

Como a operação ainda não aconteceu (AJAX, chamada do servidor, IO, timer), você está retornando o valor antes que a solicitação tenha a chance de informar ao seu código qual é esse valor.

Uma solução possível para esse problema é codificar de forma reativa , informando ao seu programa o que fazer quando o cálculo for concluído. As promessas ativam isso ativamente por serem temporais (sensíveis ao tempo) por natureza.

Resumo rápido das promessas

Uma promessa é um valor ao longo do tempo . As promessas têm estado, elas começam como pendentes sem valor e podem se estabelecer em:

  • cumprido, o que significa que o cálculo foi concluído com êxito.
  • rejeitado, o que significa que a computação falhou.

Uma promessa só pode mudar de estado uma vez, após o que sempre permanecerá no mesmo estado para sempre. Você pode anexar thenmanipuladores a promessas para extrair seu valor e manipular erros. thenmanipuladores permitem encadear chamadas. As promessas são criadas usando APIs que as retornam . Por exemplo, a substituição AJAX mais moderna fetchou o jQuery$.get retorno prometem.

Quando exigimos .thenuma promessa e devolvemos algo dela - obtemos uma promessa para o valor processado . Se retornarmos outra promessa, teremos coisas incríveis, mas vamos segurar nossos cavalos.

Com promessas

Vamos ver como podemos resolver o problema acima com promessas. Primeiro, vamos demonstrar nossa compreensão dos estados de promessa acima, usando o construtor Promise para criar uma função de atraso:

function delay(ms){ // takes amount of milliseconds
    // returns a new promise
    return new Promise(function(resolve, reject){
        setTimeout(function(){ // when the time is up
            resolve(); // change the promise to the fulfilled state
        }, ms);
    });
}

Agora, depois que convertemos setTimeout para usar promessas, podemos usar thenpara fazer valer:

function delay(ms){ // takes amount of milliseconds
  // returns a new promise
  return new Promise(function(resolve, reject){
    setTimeout(function(){ // when the time is up
      resolve(); // change the promise to the fulfilled state
    }, ms);
  });
}

function getFive(){
  // we're RETURNING the promise, remember, a promise is a wrapper over our value
  return delay(100).then(function(){ // when the promise is ready
      return 5; // return the value 5, promises are all about return values
  })
}
// we _have_ to wrap it like this in the call site, we can't access the plain value
getFive().then(function(five){ 
   document.body.innerHTML = five;
});

Basicamente, em vez de retornar um valor que não podemos fazer por causa do modelo de concorrência - estamos retornando um invólucro para um valor que podemos desembrulhar com then. É como uma caixa com a qual você pode abrir then.

Aplicando isso

Isso significa o mesmo para sua chamada de API original, você pode:

function foo() {
    // RETURN the promise
    return fetch("/echo/json").then(function(response){
        return response.json(); // process it inside the `then`
    });
}

foo().then(function(response){
    // access the value inside the `then`
})

Então, isso funciona tão bem. Aprendemos que não podemos retornar valores de chamadas já assíncronas, mas podemos usar promessas e encadeá-las para executar o processamento. Agora sabemos como retornar a resposta de uma chamada assíncrona.

ES2015 (ES6)

O ES6 apresenta geradores que são funções que podem retornar no meio e retomar o ponto em que estavam. Isso geralmente é útil para sequências, por exemplo:

function* foo(){ // notice the star, this is ES6 so new browsers/node/io only
    yield 1;
    yield 2;
    while(true) yield 3;
}

É uma função que retorna um iterador sobre a sequência 1,2,3,3,3,3,....que pode ser iterada. Embora isso seja interessante por si só e abra espaço para muitas possibilidades, há um caso interessante em particular.

Se a sequência que estamos produzindo é uma sequência de ações, e não números - podemos pausar a função sempre que uma ação é produzida e aguardá-la antes de retomar a função. Então, em vez de uma sequência de números, precisamos de uma sequência de futuros valores - ou seja: promessas.

Esse truque um tanto complicado, mas muito poderoso, permite escrever código assíncrono de maneira síncrona. Existem vários "corredores" que fazem isso por você, escrevendo um com poucas linhas de código, mas está além do escopo desta resposta. Vou usar o Bluebird's Promise.coroutineaqui, mas existem outros wrappers como coor Q.async.

var foo = coroutine(function*(){
    var data = yield fetch("/echo/json"); // notice the yield
    // code here only executes _after_ the request is done
    return data.json(); // data is defined
});

Esse método retorna uma promessa em si, que podemos consumir de outras corotinas. Por exemplo:

var main = coroutine(function*(){
   var bar = yield foo(); // wait our earlier coroutine, it returns a promise
   // server call done here, code below executes when done
   var baz = yield fetch("/api/users/"+bar.userid); // depends on foo's result
   console.log(baz); // runs after both requests done
});
main();

ES2016 (ES7)

No ES7, isso é padronizado ainda mais, existem várias propostas no momento, mas em todas elas você pode awaitprometer. Isso é apenas "açúcar" (melhor sintaxe) para a proposta ES6 acima, adicionando as palavras async- awaitchave e . Fazendo o exemplo acima:

async function foo(){
    var data = await fetch("/echo/json"); // notice the await
    // code here only executes _after_ the request is done
    return data.json(); // data is defined
}

Ele ainda retorna uma promessa da mesma forma :)

Benjamin Gruenbaum
fonte
Essa deve ser a resposta aceita. +1 para async / await (embora não deveríamos return await data.json();?)
Lewis Donovan
247

Você está usando o Ajax incorretamente. A idéia é não fazer com que ele retorne nada, mas entregue os dados a algo chamado função de retorno de chamada, que lida com os dados.

Isso é:

function handleData( responseData ) {

    // Do what you want with the data
    console.log(responseData);
}

$.ajax({
    url: "hi.php",
    ...
    success: function ( data, status, XHR ) {
        handleData(data);
    }
});

Retornar qualquer coisa no manipulador de envio não fará nada. Em vez disso, você deve entregar os dados ou fazer o que quiser diretamente dentro da função de sucesso.

Nic
fonte
13
Essa resposta é completamente semântica ... seu método de sucesso é apenas um retorno de chamada em um retorno de chamada. Você poderia ter success: handleDatae funcionaria.
Jacques
5
E se você quiser retornar o "responseData" fora de "handleData" ... :) ... como você fará isso ...? ... causar um simples retorno vai devolvê-lo para a chamada de retorno "sucesso" do ajax ... e não fora "handleData" ...
pesho Hristov
@Jacques & @pesho hristov Você perdeu esse ponto. O manipulador de envio não é o successmétodo, é o escopo circundante de $.ajax.
Travnik
Travnik @ Eu não senti falta disso. Se você tomou o conteúdo de handleData e colocá-lo no método de sucesso que agiriam exatamente o mesmo ...
Jacquesジャック
234

A solução mais simples é criar uma função JavaScript e chamá-la para o successretorno de chamada do Ajax .

function callServerAsync(){
    $.ajax({
        url: '...',
        success: function(response) {

            successCallback(response);
        }
    });
}

function successCallback(responseObj){
    // Do something like read the response and show data
    alert(JSON.stringify(responseObj)); // Only applicable to JSON response
}

function foo(callback) {

    $.ajax({
        url: '...',
        success: function(response) {
           return callback(null, response);
        }
    });
}

var result = foo(function(err, result){
          if (!err)
           console.log(result);    
}); 
Hemant Bavle
fonte
3
Não sei quem votou negativo. Mas este é um trabalho em torno do qual funcionou, de fato, usei essa abordagem para criar um aplicativo inteiro. O jquery.ajax não retorna dados, então é melhor usar a abordagem acima. Se estiver errado, explique e sugira uma maneira melhor de fazê-lo.
Hemant Bavle 28/03
11
Desculpe, esqueci de deixar um comentário (geralmente faço!). Eu voto negativo. Votos negativos não indicam correção ou falta de fato, eles indicam utilidade no contexto ou falta de. Não acho sua resposta útil, dada a de Felix, que já explica isso apenas com muito mais detalhes. Em uma nota lateral, por que você especificaria a resposta se for JSON?
Benjamin Gruenbaum
5
ok .. @Benjamin eu usei stringify, para converter um objeto JSON em string. E obrigado por esclarecer seu ponto de vista. Lembre-se de postar respostas mais elaboradas.
Hemant Bavle
E se você quiser retornar o "responseObj" fora de "successCallback" ... :) ... como você fará isso ...? ... causar um simples retorno vai devolvê-lo para a chamada de retorno "sucesso" do ajax ... e não fora "successCallback" ...
pesho Hristov
221

Vou responder com uma história em quadrinhos horrível, desenhada à mão. A segunda imagem é a razão pela qual resultestá undefinedno seu exemplo de código.

insira a descrição da imagem aqui

Johannes Fahrenkrug
fonte
32
Uma imagem vale mais que mil palavras . Pessoa A - Peça os detalhes da pessoa B para consertar seu carro. Pessoa B - Faz o Ajax Call e aguarda a resposta do servidor para obter detalhes de fixação do carro, quando a resposta é recebida, a função Ajax Success chama a Pessoa Função B e passa a resposta como argumento, a Pessoa A recebe a resposta.
shaijut 31/10/16
10
Seria ótimo se você adicionasse linhas de código a cada imagem para ilustrar os conceitos.
Hassan Baig
1
Enquanto isso, o cara com o carro está preso na berma da estrada. Ele exige que o carro seja consertado antes de continuar. Ele agora está sozinho na beira da estrada, esperando ... Ele preferia estar ao telefone esperando por mudanças de status, mas o mecânico não faria isso ... O mecânico disse que precisa seguir seu trabalho e não pode simplesmente desligue o telefone. O mecânico prometeu que ligaria de volta o mais rápido possível. Após cerca de 4 horas, o cara desiste e liga para o Uber. - Exemplo de tempo limite.
barrypicker
@barrypicker :-D Brilhante!
Johannes Fahrenkrug
159

Angular1

Para pessoas que estão usando o AngularJS , podem lidar com essa situação usando Promises.

Aqui diz:

As promessas podem ser usadas para desnaturar funções assíncronas e permitem que se encadeie várias funções.

Você pode encontrar uma boa explicação aqui também.

Exemplo encontrado nos documentos mencionados abaixo.

  promiseB = promiseA.then(
    function onSuccess(result) {
      return result + 1;
    }
    ,function onError(err) {
      //Handle error
    }
  );

 // promiseB will be resolved immediately after promiseA is resolved 
 // and its value will be the result of promiseA incremented by 1.

Angular2 e mais tarde

Veja Angular2o exemplo a seguir, mas é recomendável usar Observablescom Angular2.

 search(term: string) {
     return this.http
  .get(`https://api.spotify.com/v1/search?q=${term}&type=artist`)
  .map((response) => response.json())
  .toPromise();

}

Você pode consumir dessa maneira,

search() {
    this.searchService.search(this.searchField.value)
      .then((result) => {
    this.result = result.artists.items;
  })
  .catch((error) => console.error(error));
}

Veja o post original aqui. Porém, o Typescript não suporta o es6 Promises nativo ; se você quiser usá-lo, pode ser necessário um plugin para isso.

Além disso, aqui estão as especificações de promessas definidas aqui.

Maleen Abewardana
fonte
15
Isso não explica como as promessas resolveriam esse problema.
Benjamin Gruenbaum
4
Os métodos jQuery e fetch também retornam promessas. Eu sugeriria revisar sua resposta. Embora o jQuery não seja o mesmo (então existe, mas catch não é).
precisa
153

A maioria das respostas aqui fornece sugestões úteis para quando você tem uma única operação assíncrona, mas às vezes isso ocorre quando você precisa executar uma operação assíncrona para cada entrada em uma matriz ou outra estrutura semelhante a uma lista. A tentação é fazer isso:

// WRONG
var results = [];
theArray.forEach(function(entry) {
    doSomethingAsync(entry, function(result) {
        results.push(result);
    });
});
console.log(results); // E.g., using them, returning them, etc.

Exemplo:

O motivo para isso não funcionar é que os retornos de chamada doSomethingAsyncainda não foram executados no momento em que você está tentando usar os resultados.

Portanto, se você possui uma matriz (ou algum tipo de lista) e deseja executar operações assíncronas para cada entrada, você tem duas opções: Execute as operações em paralelo (sobreposição) ou em série (uma após a outra em sequência).

Paralelo

Você pode iniciar todos eles e acompanhar quantas chamadas de retorno espera e, em seguida, usar os resultados quando receber tantas chamadas de retorno:

var results = [];
var expecting = theArray.length;
theArray.forEach(function(entry, index) {
    doSomethingAsync(entry, function(result) {
        results[index] = result;
        if (--expecting === 0) {
            // Done!
            console.log("Results:", results); // E.g., using the results
        }
    });
});

Exemplo:

(Nós poderíamos acabar com expectinge apenas usarresults.length === theArray.length , mas isso nos deixa abertos à possibilidade de theArraymudar enquanto as chamadas são excelentes ...)

Observe como usamos a indexpartir deforEach para salvar o resultado na resultsmesma posição da entrada a que se refere, mesmo que os resultados cheguem fora de ordem (já que as chamadas assíncronas não são necessariamente concluídas na ordem em que foram iniciadas).

Mas e se você precisar retornar esses resultados de uma função? Como as outras respostas apontaram, você não pode; você precisa que sua função aceite e chame um retorno de chamada (ou retorne uma promessa ). Aqui está uma versão de retorno de chamada:

function doSomethingWith(theArray, callback) {
    var results = [];
    var expecting = theArray.length;
    theArray.forEach(function(entry, index) {
        doSomethingAsync(entry, function(result) {
            results[index] = result;
            if (--expecting === 0) {
                // Done!
                callback(results);
            }
        });
    });
}
doSomethingWith(theArray, function(results) {
    console.log("Results:", results);
});

Exemplo:

Ou aqui está uma versão retornando um Promise:

function doSomethingWith(theArray) {
    return new Promise(function(resolve) {
        var results = [];
        var expecting = theArray.length;
        theArray.forEach(function(entry, index) {
            doSomethingAsync(entry, function(result) {
                results[index] = result;
                if (--expecting === 0) {
                    // Done!
                    resolve(results);
                }
            });
        });
    });
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

Claro se doSomethingAsync nos passassem erros, usaríamos rejectpara rejeitar a promessa quando recebíamos um erro.)

Exemplo:

(Ou, alternativamente, você pode fazer um invólucro para doSomethingAsync que retorne uma promessa e fazer o seguinte ...)

Se doSomethingAsynclhe der uma promessa , você pode usar Promise.all:

function doSomethingWith(theArray) {
    return Promise.all(theArray.map(function(entry) {
        return doSomethingAsync(entry);
    }));
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

Se você sabe que doSomethingAsyncisso ignorará um segundo e um terceiro argumento, você pode apenas passá-lo diretamente para map( mapchama seu retorno de chamada com três argumentos, mas a maioria das pessoas usa apenas o primeiro na maior parte do tempo):

function doSomethingWith(theArray) {
    return Promise.all(theArray.map(doSomethingAsync));
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

Exemplo:

Observe que Promise.allresolve sua promessa com uma variedade de resultados de todas as promessas que você faz quando todas são resolvidas ou rejeita sua promessa quando a primeira das promessas que você rejeita.

Series

Suponha que você não queira que as operações sejam paralelas? Se você deseja executá-los um após o outro, precisará aguardar a conclusão de cada operação antes de iniciar a próxima. Aqui está um exemplo de uma função que faz isso e chama um retorno de chamada com o resultado:

function doSomethingWith(theArray, callback) {
    var results = [];
    doOne(0);
    function doOne(index) {
        if (index < theArray.length) {
            doSomethingAsync(theArray[index], function(result) {
                results.push(result);
                doOne(index + 1);
            });
        } else {
            // Done!
            callback(results);
        }
    }
}
doSomethingWith(theArray, function(results) {
    console.log("Results:", results);
});

(Como estamos fazendo o trabalho em série, podemos apenas usá-lo, results.push(result)pois sabemos que não obteremos resultados fora de ordem. No exemplo acima, poderíamos ter usado results[index] = result;, mas em alguns dos exemplos a seguir não temos um índice usar.)

Exemplo:

(Ou, novamente, crie um invólucro para doSomethingAsyncque você prometa e faça o abaixo ...)

Se doSomethingAsynclhe der uma promessa, se você puder usar a sintaxe do ES2017 + (talvez com um transpilador como o Babel ), poderá usar uma asyncfunção com for-ofe await:

async function doSomethingWith(theArray) {
    const results = [];
    for (const entry of theArray) {
        results.push(await doSomethingAsync(entry));
    }
    return results;
}
doSomethingWith(theArray).then(results => {
    console.log("Results:", results);
});

Exemplo:

Se você ainda não pode usar a sintaxe do ES2017 +, pode usar uma variação no padrão "Redução de promessas" (isso é mais complexo que a redução de promessas usual porque não estamos passando o resultado de um para o outro, mas sim reunindo seus resultados em uma matriz):

function doSomethingWith(theArray) {
    return theArray.reduce(function(p, entry) {
        return p.then(function(results) {
            return doSomethingAsync(entry).then(function(result) {
                results.push(result);
                return results;
            });
        });
    }, Promise.resolve([]));
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

Exemplo:

... o que é menos complicado com as funções de seta do ES2015 + :

function doSomethingWith(theArray) {
    return theArray.reduce((p, entry) => p.then(results => doSomethingAsync(entry).then(result => {
        results.push(result);
        return results;
    })), Promise.resolve([]));
}
doSomethingWith(theArray).then(results => {
    console.log("Results:", results);
});

Exemplo:

TJ Crowder
fonte
1
Você poderia explicar como a if (--expecting === 0)parte do código funciona, por favor? A versão de retorno da sua solução está funcionando muito bem para mim, mas não entendo como, com essa declaração, você está verificando o número de respostas concluídas. Aprecio que é apenas falta de conhecimento da minha parte. Existe uma maneira alternativa de verificar esse cheque?
Sarah
@ Sarah: expectingcomeça com o valor de array.length, que é quantas solicitações vamos fazer. Sabemos que o retorno de chamada não será chamado até que todas essas solicitações sejam iniciadas. No retorno de chamada, if (--expecting === 0)faz o seguinte: 1. Decrementos expecting(recebemos uma resposta, portanto esperamos uma resposta a menos) e se o valor após o decréscimo for 0 (não esperamos mais respostas), estamos feito!
TJ Crowder
1
@PatrickRoberts - Thanks !! Sim, erro de copiar e colar, esse segundo argumento foi completamente ignorado nesse exemplo (que é a única razão pela qual não falhou, pois, como você apontou, resultsnão existia). :-) Corrigido.
TJ Crowder
111

Veja este exemplo:

var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope,$http) {

    var getJoke = function(){
        return $http.get('http://api.icndb.com/jokes/random').then(function(res){
            return res.data.value;  
        });
    }

    getJoke().then(function(res) {
        console.log(res.joke);
    });
});

Como você pode ver, getJokeestá retornando uma promessa resolvida (é resolvida ao retornar res.data.value). Portanto, você espera até que a solicitação $ http.get seja concluída e o console.log (res.joke) seja executado (como um fluxo assíncrono normal).

Este é o plnkr:

http://embed.plnkr.co/XlNR7HpCaIhJxskMJfSg/

Caminho ES6 (assíncrono - aguardar)

(function(){
  async function getJoke(){
    let response = await fetch('http://api.icndb.com/jokes/random');
    let data = await response.json();
    return data.value;
  }

  getJoke().then((joke) => {
    console.log(joke);
  });
})();
Francisco Carmona
fonte
107

Este é um dos lugares em que o conceito de vinculação ou armazenamento de dados usado em muitas novas estruturas JavaScript funcionará muito bem para você ...

Portanto, se você estiver usando Angular, React ou qualquer outra estrutura que execute duas formas de vinculação de dados ou conceito de armazenamento, esse problema será simplesmente corrigido para você. Assim, em palavras simples, seu resultado será undefinedo primeiro estágio, portanto, você deve obter result = undefinedantes de receber o dados, assim que você obtiver o resultado, ele será atualizado e atribuído ao novo valor que resposta da sua chamada Ajax ...

Mas como você pode fazer isso em javascript puro ou jQuery, por exemplo, como você fez nesta pergunta?

Você pode usar um retorno de chamada , promessa e observável recentemente para lidar com isso, por exemplo, em promessas que temos algumas funções como success()ou then()que serão executadas quando seus dados estiverem prontos para você, o mesmo que com a função de retorno de chamada ou assinatura observável .

Por exemplo, no seu caso em que você está usando jQuery , você pode fazer algo assim:

$(document).ready(function(){
    function foo() {
        $.ajax({url: "api/data", success: function(data){
            fooDone(data); //after we have data, we pass it to fooDone
        }});
    };

    function fooDone(data) {
        console.log(data); //fooDone has the data and console.log it
    };

    foo(); //call happens here
});

Para obter mais informações, estude sobre promessas e observáveis, que são maneiras mais recentes de fazer isso de forma assíncrona.

Alireza
fonte
Isso é bom no escopo global, mas em algum contexto módulo que você provavelmente vai querer garantir contexto certo para o retorno de chamada, por exemplo$.ajax({url: "api/data", success: fooDone.bind(this)});
steve.sims
8
Este é na verdade incorrecta como Reagir é a ligação de dados unidirecionais
Matthew Brent
@MatthewBrent você não está errado, mas não é certo também, Reagir adereços são objeto e se mudou, eles mudam todo o aplicativo, mas não é uma forma que Reagir desenvolvedor recomendo usá-lo ...
Alireza
98

É um problema muito comum que enfrentamos enquanto lutamos com os 'mistérios' do JavaScript. Deixe-me tentar desmistificar esse mistério hoje.

Vamos começar com uma simples função JavaScript:

function foo(){
// do something 
 return 'wohoo';
}

let bar = foo(); // bar is 'wohoo' here

É uma simples chamada de função síncrona (onde cada linha de código é 'finalizada com seu trabalho' antes da próxima em sequência), e o resultado é o mesmo que o esperado.

Agora, vamos adicionar um pouco de distorção, introduzindo pouco atraso em nossa função, para que todas as linhas de código não sejam 'finalizadas' em sequência. Assim, ele emulará o comportamento assíncrono da função:

function foo(){
 setTimeout( ()=>{
   return 'wohoo';
  }, 1000 )
}

let bar = foo() // bar is undefined here

Então lá está, esse atraso acabou com a funcionalidade que esperávamos! Mas o que exatamente aconteceu? Bem, é realmente muito lógico se você olhar para o código. a função foo(), na execução, não retorna nada (o valor retornado é undefined), mas inicia um timer, que executa uma função após 1s para retornar 'wohoo'. Mas como você pode ver, o valor atribuído à barra é o material retornado imediatamente de foo (), que não é nada, é apenas justo undefined.

Então, como lidamos com esse problema?

Vamos pedir à nossa função uma PROMESSA . Promessa é realmente sobre o que isso significa: significa que a função garante que você forneça qualquer saída que obtiver no futuro. então vamos vê-lo em ação para o nosso pequeno problema acima:

function foo(){
   return new Promise( (resolve, reject) => { // I want foo() to PROMISE me something
    setTimeout ( function(){ 
      // promise is RESOLVED , when execution reaches this line of code
       resolve('wohoo')// After 1 second, RESOLVE the promise with value 'wohoo'
    }, 1000 )
  })
}

let bar ; 
foo().then( res => {
 bar = res;
 console.log(bar) // will print 'wohoo'
});

Assim, o resumo é - para lidar com funções assíncronas, como chamadas baseadas em ajax, etc., você pode usar uma promessa ao resolvevalor (que pretende retornar). Assim, em resumo, você resolve o valor em vez de retornar , em funções assíncronas.

UPDATE (Promessas com assíncrono / espera)

Além de then/catchtrabalhar com promessas, existe mais uma abordagem. A idéia é reconhecer uma função assíncrona e esperar as promessas serem resolvidas antes de passar para a próxima linha de código. Ainda é apenas um promisessegredo, mas com uma abordagem sintática diferente. Para tornar as coisas mais claras, você pode encontrar uma comparação abaixo:

então / catch versão:

function saveUsers(){
     getUsers()
      .then(users => {
         saveSomewhere(users);
      })
      .catch(err => {
          console.error(err);
       })
 }

versão assíncrona / aguardada:

  async function saveUsers(){
     try{
        let users = await getUsers()
        saveSomewhere(users);
     }
     catch(err){
        console.error(err);
     }
  }
Anish K.
fonte
essa ainda é considerada a melhor maneira de retornar um valor de uma promessa ou assíncrona / aguardada?
Edwardsmarkf
3
@edwardsmarkf Pessoalmente, não acho que exista uma maneira melhor. Uso promessas com then / catch, async / wait e geradores de partes assíncronas do meu código. Depende em grande parte do contexto de uso.
Anish K. #
96

Outra abordagem para retornar um valor de uma função assíncrona é passar um objeto que armazenará o resultado da função assíncrona.

Aqui está um exemplo do mesmo:

var async = require("async");

// This wires up result back to the caller
var result = {};
var asyncTasks = [];
asyncTasks.push(function(_callback){
    // some asynchronous operation
    $.ajax({
        url: '...',
        success: function(response) {
            result.response = response;
            _callback();
        }
    });
});

async.parallel(asyncTasks, function(){
    // result is available after performing asynchronous operation
    console.log(result)
    console.log('Done');
});

Estou usando o result objeto para armazenar o valor durante a operação assíncrona. Isso permite que o resultado esteja disponível mesmo após o trabalho assíncrono.

Eu uso muito essa abordagem. Eu estaria interessado em saber como essa abordagem funciona quando está envolvido o retorno do resultado através de módulos consecutivos.

jsbisht
fonte
9
Não há nada de especial em usar um objeto aqui. Também funcionaria se você atribuísse a resposta diretamente a ele result. Funciona porque você está lendo a variável após a conclusão da função assíncrona.
Felix Kling
85

Embora promessas e retornos de chamada funcionem bem em muitas situações, é difícil expressar algo como:

if (!name) {
  name = async1();
}
async2(name);

Você acabaria passando async1; verifique se nameestá indefinido ou não e chame o retorno de chamada adequadamente.

async1(name, callback) {
  if (name)
    callback(name)
  else {
    doSomething(callback)
  }
}

async1(name, async2)

Embora seja bom em pequenos exemplos, fica irritante quando você tem muitos casos semelhantes e tratamento de erros envolvidos.

Fibers ajuda a resolver o problema.

var Fiber = require('fibers')

function async1(container) {
  var current = Fiber.current
  var result
  doSomething(function(name) {
    result = name
    fiber.run()
  })
  Fiber.yield()
  return result
}

Fiber(function() {
  var name
  if (!name) {
    name = async1()
  }
  async2(name)
  // Make any number of async calls from here
}

Você pode conferir o projeto aqui .

rohithpr
fonte
1
@recurf - Não é o meu projeto. Você pode tentar usar o rastreador de problemas.
rohithpr
1
isso é semelhante às funções do gerador? developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/… *
Emanegux 7/17
1
Isso ainda é relevante?
Aluan Haddad
Você pode fazer uso async-awaitse estiver usando algumas das versões mais recentes do nó. Se alguém está preso com versões mais antigas, ele pode usar esse método.
rohithpr
83

O exemplo a seguir que escrevi mostra como

  • Manipular chamadas HTTP assíncronas;
  • Aguarde a resposta de cada chamada da API;
  • Use o padrão Promise ;
  • Use o padrão Promise.all para ingressar em várias chamadas HTTP;

Este exemplo de trabalho é independente. Ele definirá um objeto de solicitação simples que usa o XMLHttpRequestobjeto de janela para fazer chamadas. Ele definirá uma função simples para aguardar a conclusão de várias promessas.

Contexto. O exemplo é consultar o ponto de extremidade da API da Web do Spotify para procurar playlistobjetos para um determinado conjunto de cadeias de consulta:

[
 "search?type=playlist&q=%22doom%20metal%22",
 "search?type=playlist&q=Adele"
]

Para cada item, uma nova promessa acionará um bloco - ExecutionBlock, analise o resultado, programe um novo conjunto de promessas com base na matriz de resultados, que é uma lista de userobjetos do Spotify e execute a nova chamada HTTP de ExecutionProfileBlockforma assíncrona.

Você pode ver uma estrutura Promise aninhada, que permite gerar várias chamadas HTTP aninhadas completamente assíncronas e juntar os resultados de cada subconjunto de chamadas Promise.all.

NOTA As searchAPIs recentes do Spotify exigirão que um token de acesso seja especificado nos cabeçalhos da solicitação:

-H "Authorization: Bearer {your access token}" 

Portanto, para executar o exemplo a seguir, você precisa colocar seu token de acesso nos cabeçalhos da solicitação:

var spotifyAccessToken = "YourSpotifyAccessToken";
var console = {
    log: function(s) {
        document.getElementById("console").innerHTML += s + "<br/>"
    }
}

// Simple XMLHttpRequest
// based on https://davidwalsh.name/xmlhttprequest
SimpleRequest = {
    call: function(what, response) {
        var request;
        if (window.XMLHttpRequest) { // Mozilla, Safari, ...
            request = new XMLHttpRequest();
        } else if (window.ActiveXObject) { // Internet Explorer
            try {
                request = new ActiveXObject('Msxml2.XMLHTTP');
            }
            catch (e) {
                try {
                  request = new ActiveXObject('Microsoft.XMLHTTP');
                } catch (e) {}
            }
        }

        // State changes
        request.onreadystatechange = function() {
            if (request.readyState === 4) { // Done
                if (request.status === 200) { // Complete
                    response(request.responseText)
                }
                else
                    response();
            }
        }
        request.open('GET', what, true);
        request.setRequestHeader("Authorization", "Bearer " + spotifyAccessToken);
        request.send(null);
    }
}

//PromiseAll
var promiseAll = function(items, block, done, fail) {
    var self = this;
    var promises = [],
                   index = 0;
    items.forEach(function(item) {
        promises.push(function(item, i) {
            return new Promise(function(resolve, reject) {
                if (block) {
                    block.apply(this, [item, index, resolve, reject]);
                }
            });
        }(item, ++index))
    });
    Promise.all(promises).then(function AcceptHandler(results) {
        if (done) done(results);
    }, function ErrorHandler(error) {
        if (fail) fail(error);
    });
}; //promiseAll

// LP: deferred execution block
var ExecutionBlock = function(item, index, resolve, reject) {
    var url = "https://api.spotify.com/v1/"
    url += item;
    console.log( url )
    SimpleRequest.call(url, function(result) {
        if (result) {

            var profileUrls = JSON.parse(result).playlists.items.map(function(item, index) {
                return item.owner.href;
            })
            resolve(profileUrls);
        }
        else {
            reject(new Error("call error"));
        }
    })
}

arr = [
    "search?type=playlist&q=%22doom%20metal%22",
    "search?type=playlist&q=Adele"
]

promiseAll(arr, function(item, index, resolve, reject) {
    console.log("Making request [" + index + "]")
    ExecutionBlock(item, index, resolve, reject);
}, function(results) { // Aggregated results

    console.log("All profiles received " + results.length);
    //console.log(JSON.stringify(results[0], null, 2));

    ///// promiseall again

    var ExecutionProfileBlock = function(item, index, resolve, reject) {
        SimpleRequest.call(item, function(result) {
            if (result) {
                var obj = JSON.parse(result);
                resolve({
                    name: obj.display_name,
                    followers: obj.followers.total,
                    url: obj.href
                });
            } //result
        })
    } //ExecutionProfileBlock

    promiseAll(results[0], function(item, index, resolve, reject) {
        //console.log("Making request [" + index + "] " + item)
        ExecutionProfileBlock(item, index, resolve, reject);
    }, function(results) { // aggregated results
        console.log("All response received " + results.length);
        console.log(JSON.stringify(results, null, 2));
    }

    , function(error) { // Error
        console.log(error);
    })

    /////

  },
  function(error) { // Error
      console.log(error);
  });
<div id="console" />

Eu discuti extensivamente esta solução aqui .

Loretoparisi
fonte
80

Resposta curta: você precisa implementar um retorno de chamada como este:

function callback(response) {
    // Here you can do what ever you want with the response object.
    console.log(response);
}

$.ajax({
    url: "...",
    success: callback
});
Pablo Matias Gomez
fonte
78

Resposta de 2017: agora você pode fazer exatamente o que deseja em todos os navegadores e nós atuais

Isso é bem simples:

  • Devolver uma promessa
  • Use o 'aguardar' , que instruirá o JavaScript a aguardar a promessa de ser resolvido em um valor (como a resposta HTTP)
  • Adicione a palavra-chave 'async' à função pai

Aqui está uma versão funcional do seu código:

(async function(){

var response = await superagent.get('...')
console.log(response)

})()

aguardar é suportado em todos os navegadores atuais e no nó 8

mikemaccana
fonte
7
Infelizmente, isso funciona apenas com funções que retornam promessas - por exemplo, não funciona com a API Node.js., que usa retornos de chamada. E eu não recomendaria usá-lo sem o Babel, porque nem todo mundo usa "navegadores atuais".
Michał Perłakowski
2
O nó 8 do MichałPerłakowski inclui nodejs.org/api/util.html#util_util_promisify_original, que pode ser usado para fazer promessas de retorno da API do node.js. Se você tem tempo e dinheiro para oferecer suporte a navegadores não atuais, obviamente depende da sua situação.
Mikemaccana
O IE 11 ainda é um navegador atual em 2018, infelizmente e não é compatível #await/async
Juan Mendes #
O IE11 não é um navegador atual. Foi lançado há 5 anos, tem uma participação de mercado mundial de 2,5%, de acordo com a caniuse, e, a menos que alguém esteja dobrando seu orçamento para ignorar toda a tecnologia atual, não vale o tempo da maioria das pessoas.
Mikemaccana
76

Js é um único encadeamento.

O navegador pode ser dividido em três partes:

1) Loop de Eventos

2) API da Web

3) Fila de Eventos

O loop de eventos é executado para sempre, ou seja, é um tipo de loop infinito. A fila de eventos é onde todas as suas funções são pressionadas em algum evento (exemplo: clique); este é um por um realizado na fila e colocado no loop de eventos que executa essa função e se prepara automaticamente para o próximo após o primeiro ser executado. Isso significa que a execução de uma função não inicia até que a função antes da fila seja executada no loop de eventos.

Agora, vamos pensar que pressionamos duas funções em uma fila, uma para obter dados do servidor e outra utiliza esses dados. A função serverRequest entra no loop de eventos e faz uma chamada para o servidor, pois nunca sabemos quanto tempo levará para obter dados do servidor, portanto, espera-se que esse processo leve tempo e ocupamos nosso loop de eventos, pendurando nossa página, é aí que a Web A API entra em função, assume essa função do loop de eventos e lida com o servidor, liberando o loop de eventos, para que possamos executar a próxima função da fila. A próxima função na fila é utiliseData () que entra em loop, mas por causa de nenhum dado disponível, ele vai o desperdício e a execução da próxima função continuam até o final da fila (isso é chamado de chamada assíncrona, ou seja, podemos fazer outra coisa até obtermos dados)

Vamos supor que nossa função serverRequest () tenha uma declaração de retorno em um código, quando recuperarmos os dados da API da Web do servidor, eles serão colocados na fila no final da fila. Como é empurrado no final da fila, não podemos utilizar seus dados, pois não há nenhuma função em nossa fila para utilizá-los. Portanto, não é possível retornar algo do Async Call.

Portanto, a solução para isso é retorno ou promessa .

Uma imagem de uma das respostas aqui, explica corretamente o uso de retorno de chamada ... Damos nossa função (função que utiliza dados retornados do servidor) para funcionar como servidor de chamada.

Ligue de volta

 function doAjax(callbackFunc, method, url) {
  var xmlHttpReq = new XMLHttpRequest();
  xmlHttpReq.open(method, url);
  xmlHttpReq.onreadystatechange = function() {

      if (xmlHttpReq.readyState == 4 && xmlHttpReq.status == 200) {
        callbackFunc(xmlHttpReq.responseText);
      }


  }
  xmlHttpReq.send(null);

}

No meu código é chamado como

function loadMyJson(categoryValue){
  if(categoryValue==="veg")
  doAjax(print,"GET","http://localhost:3004/vegetables");
  else if(categoryValue==="fruits")
  doAjax(print,"GET","http://localhost:3004/fruits");
  else 
  console.log("Data not found");
}

Retorno de chamada Javscript.info

Aniket Jha
fonte
68

Você pode usar esta biblioteca personalizada (gravada usando o Promise) para fazer uma chamada remota.

function $http(apiConfig) {
    return new Promise(function (resolve, reject) {
        var client = new XMLHttpRequest();
        client.open(apiConfig.method, apiConfig.url);
        client.send();
        client.onload = function () {
            if (this.status >= 200 && this.status < 300) {
                // Performs the function "resolve" when this.status is equal to 2xx.
                // Your logic here.
                resolve(this.response);
            }
            else {
                // Performs the function "reject" when this.status is different than 2xx.
                reject(this.statusText);
            }
        };
        client.onerror = function () {
            reject(this.statusText);
        };
    });
}

Exemplo de uso simples:

$http({
    method: 'get',
    url: 'google.com'
}).then(function(response) {
    console.log(response);
}, function(error) {
    console.log(error)
});
Vinoth Rajendran
fonte
67

Outra solução é executar o código via executor seqüencial nsynjs .

Se a função subjacente for prometida

O nsynjs avaliará todas as promessas sequencialmente e colocará o resultado da promessa na datapropriedade:

function synchronousCode() {

    var getURL = function(url) {
        return window.fetch(url).data.text().data;
    };
    
    var url = 'https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js';
    console.log('received bytes:',getURL(url).length);
    
};

nsynjs.run(synchronousCode,{},function(){
    console.log('synchronousCode done');
});
<script src="https://rawgit.com/amaksr/nsynjs/master/nsynjs.js"></script>

Se a função subjacente não for prometida

Etapa 1. Função Wrap com retorno de chamada no wrapper compatível com nsynjs (se houver uma versão promissificada, você pode pular esta etapa):

var ajaxGet = function (ctx,url) {
    var res = {};
    var ex;
    $.ajax(url)
    .done(function (data) {
        res.data = data;
    })
    .fail(function(e) {
        ex = e;
    })
    .always(function() {
        ctx.resume(ex);
    });
    return res;
};
ajaxGet.nsynjsHasCallback = true;

Etapa 2. Coloque a lógica síncrona em função:

function process() {
    console.log('got data:', ajaxGet(nsynjsCtx, "data/file1.json").data);
}

Etapa 3. Execute a função de maneira síncrona via nsynjs:

nsynjs.run(process,this,function () {
    console.log("synchronous function finished");
});

O Nsynjs avaliará todos os operadores e expressões passo a passo, interrompendo a execução, caso o resultado de alguma função lenta não esteja pronto.

Mais exemplos aqui: https://github.com/amaksr/nsynjs/tree/master/examples

amaksr
fonte
2
Isto é interessante. Eu gosto de como ele permite codificar chamadas assíncronas da maneira que você faria em outros idiomas. Mas tecnicamente não é JavaScript real?
J Morris
41

O ECMAScript 6 possui 'geradores' que permitem programar facilmente em um estilo assíncrono.

function* myGenerator() {
    const callback = yield;
    let [response] = yield $.ajax("https://stackoverflow.com", {complete: callback});
    console.log("response is:", response);

    // examples of other things you can do
    yield setTimeout(callback, 1000);
    console.log("it delayed for 1000ms");
    while (response.statusText === "error") {
        [response] = yield* anotherGenerator();
    }
}

Para executar o código acima, faça o seguinte:

const gen = myGenerator(); // Create generator
gen.next(); // Start it
gen.next((...args) => gen.next([...args])); // Set its callback function

Se você precisar direcionar navegadores que não suportam o ES6, execute o código por meio do Babel ou do compilador de fechamento para gerar o ECMAScript 5.

O retorno de chamada ...argsé agrupado em uma matriz e destruído quando você os lê, para que o padrão possa lidar com retornos de chamada que possuem vários argumentos. Por exemplo, com o nó fs :

const [err, data] = yield fs.readFile(filePath, "utf-8", callback);
James
fonte
39

Aqui estão algumas abordagens para trabalhar com solicitações assíncronas:

  1. Objeto Promessa do Navegador
  2. P - Uma biblioteca de promessas para JavaScript
  3. A + Promises.js
  4. jQuery adiado
  5. API XMLHttpRequest
  6. Usando o conceito de retorno de chamada - Como implementação na primeira resposta

Exemplo: Implementação adiada do jQuery para trabalhar com várias solicitações

var App = App || {};

App = {
    getDataFromServer: function(){

      var self = this,
                 deferred = $.Deferred(),
                 requests = [];

      requests.push($.getJSON('request/ajax/url/1'));
      requests.push($.getJSON('request/ajax/url/2'));

      $.when.apply(jQuery, requests).done(function(xhrResponse) {
        return deferred.resolve(xhrResponse.result);
      });
      return deferred;
    },

    init: function(){

        this.getDataFromServer().done(_.bind(function(resp1, resp2) {

           // Do the operations which you wanted to do when you
           // get a response from Ajax, for example, log response.
        }, this));
    }
};
App.init();

Mohan Dere
fonte
38

Nós nos encontramos em um universo que parece progredir ao longo de uma dimensão que chamamos de "tempo". Nós realmente não entendemos o que é o tempo, mas desenvolvemos abstrações e vocabulários que nos permitem raciocinar e falar sobre isso: "passado", "presente", "futuro", "antes", "depois".

Os sistemas de computador que construímos - cada vez mais - têm o tempo como uma dimensão importante. Certas coisas estão configuradas para acontecer no futuro. Então outras coisas precisam acontecer depois que essas primeiras coisas acabarem. Essa é a noção básica chamada "assincronicidade". Em nosso mundo cada vez mais em rede, o caso mais comum de assincronicidade está aguardando que algum sistema remoto responda a alguma solicitação.

Considere um exemplo. Você chama o leiteiro e pede um pouco de leite. Quando chegar, você quer colocá-lo no seu café. Você não pode colocar o leite no café agora, porque ainda não está aqui. Você precisa esperar que chegue antes de colocá-lo no café. Em outras palavras, o seguinte não funcionará:

var milk = order_milk();
put_in_coffee(milk);

Como o JS não tem como saber que ele precisa esperar para order_milkconcluir antes de executar put_in_coffee. Em outras palavras, ele não sabe que order_milké assíncrono - é algo que não resultará em leite até algum tempo futuro. JS e outras linguagens declarativas executam uma instrução após a outra sem esperar.

A abordagem clássica da JS para esse problema, aproveitando o fato de que a JS suporta funções como objetos de primeira classe que podem ser passados, é passar uma função como parâmetro para a solicitação assíncrona, que será chamada depois que for concluída sua tarefa em algum momento no futuro. Essa é a abordagem de "retorno de chamada". Se parece com isso:

order_milk(put_in_coffee);

order_milkcomeça, pede o leite e, quando e somente quando chega, invoca put_in_coffee.

O problema com essa abordagem de retorno de chamada é que polui a semântica normal de uma função que informa seu resultado return; em vez disso, as funções não devem relatar seus resultados chamando um retorno de chamada fornecido como parâmetro. Além disso, essa abordagem pode rapidamente tornar-se difícil de lidar quando se lida com sequências mais longas de eventos. Por exemplo, digamos que quero esperar que o leite seja colocado no café e só então execute uma terceira etapa, ou seja, beber o café. Acabo precisando escrever algo como isto:

order_milk(function(milk) { put_in_coffee(milk, drink_coffee); }

onde estou passando put_in_coffeeo leite para colocá-lo e também a ação ( drink_coffee) a ser executada depois que o leite for colocado. Esse código fica difícil de escrever, ler e depurar.

Nesse caso, poderíamos reescrever o código na pergunta como:

var answer;
$.ajax('/foo.json') . done(function(response) {
  callback(response.data);
});

function callback(data) {
  console.log(data);
}

Digite promessas

Essa foi a motivação para a noção de "promessa", que é um tipo particular de valor que representa um resultado futuro ou assíncrono de algum tipo. Pode representar algo que já aconteceu, ou que acontecerá no futuro, ou que talvez nunca aconteça. As promessas têm um único método, nomeado then, ao qual você passa uma ação a ser executada quando o resultado que a promessa representa tiver sido realizado.

No caso de nosso leite e café, planejamos order_milkretornar uma promessa para o leite que chega e, em seguida, especificar put_in_coffeecomo uma thenação, da seguinte maneira:

order_milk() . then(put_in_coffee)

Uma vantagem disso é que podemos agrupá-las para criar sequências de ocorrências futuras ("encadeamento"):

order_milk() . then(put_in_coffee) . then(drink_coffee)

Vamos aplicar promessas ao seu problema específico. Vamos envolver nossa lógica de solicitação dentro de uma função, que retorna uma promessa:

function get_data() {
  return $.ajax('/foo.json');
}

Na verdade, tudo o que fizemos foi adicionado returna à chamada de $.ajax. Isso funciona porque o jQuery $.ajaxjá retorna um tipo de promessa. (Na prática, sem entrar em detalhes, preferimos encerrar esta chamada para retornar uma promessa real ou usar alguma alternativa a $.ajaxisso.) Agora, se quisermos carregar o arquivo e esperar que ele termine e então faça alguma coisa, podemos simplesmente dizer

get_data() . then(do_something)

por exemplo,

get_data() . 
  then(function(data) { console.log(data); });

Ao usar promessas, acabamos transferindo muitas funções para then, portanto, geralmente é útil usar as funções de seta mais compactas no estilo ES6:

get_data() . 
  then(data => console.log(data));

A asyncpalavra-chave

Mas ainda há algo vagamente insatisfatório em ter que escrever código de uma maneira, se síncrona, e de uma maneira bem diferente, se for assíncrona. Para síncrona, escrevemos

a();
b();

mas se afor assíncrono, com promessas temos que escrever

a() . then(b);

Acima, dissemos: "O JS não tem como saber que precisa aguardar a conclusão da primeira chamada antes de executar a segunda". Não seria bom se foi alguma maneira de dizer JS isso? Acontece que existe - a awaitpalavra - chave, usada dentro de um tipo especial de função chamada função "assíncrona". Esse recurso faz parte da próxima versão do ES, mas já está disponível em transpilers como Babel, com as predefinições corretas. Isso nos permite simplesmente escrever

async function morning_routine() {
  var milk   = await order_milk();
  var coffee = await put_in_coffee(milk);
  await drink(coffee);
}

No seu caso, você seria capaz de escrever algo como

async function foo() {
  data = await get_data();
  console.log(data);
}
user663031
fonte
37

Resposta curta : Seu foo()método retorna imediatamente, enquanto a $ajax()chamada é executada de forma assíncrona após o retorno da função . O problema é como ou onde armazenar os resultados recuperados pela chamada assíncrona quando ela retornar.

Várias soluções foram fornecidas neste segmento. Talvez a maneira mais fácil seja passar um objeto para o foo()método e armazenar os resultados em um membro desse objeto após a conclusão da chamada assíncrona.

function foo(result) {
    $.ajax({
        url: '...',
        success: function(response) {
            result.response = response;   // Store the async result
        }
    });
}

var result = { response: null };   // Object to hold the async result
foo(result);                       // Returns before the async completes

Observe que a chamada para foo()ainda não retornará nada útil. No entanto, o resultado da chamada assíncrona agora será armazenado result.response.

David R Tribble
fonte
14
Enquanto isso funciona, não é realmente melhor do que atribuir a uma variável global.
Felix Kling
36

Use uma callback()função dentro do foo()sucesso. Tente desta maneira. É simples e fácil de entender.  

var lat = "";
var lon = "";
function callback(data) {
    lat = data.lat;
    lon = data.lon;
}
function getLoc() {
    var url = "http://ip-api.com/json"
    $.getJSON(url, function(data) {
        callback(data);
    });
}

getLoc();
Mahfuzur Rahman
fonte
29

A questão era:

Como faço para retornar a resposta de uma chamada assíncrona?

que PODE ser interpretado como:

Como fazer com que o código assíncrono pareça síncrono ?

A solução será evitar retornos de chamada e usar uma combinação de promessas e assíncrono / espera .

Eu gostaria de dar um exemplo para uma solicitação do Ajax.

(Embora possa ser escrito em Javascript, prefiro escrevê-lo em Python e compilá-lo em Javascript usando Transcrypt . Será bastante claro.)

Permite primeiro ativar o uso de JQuery, para ter $disponível como S:

__pragma__ ('alias', 'S', '$')

Defina uma função que retorne uma promessa , neste caso, uma chamada Ajax:

def read(url: str):
    deferred = S.Deferred()
    S.ajax({'type': "POST", 'url': url, 'data': { },
        'success': lambda d: deferred.resolve(d),
        'error': lambda e: deferred.reject(e)
    })
    return deferred.promise()

Use o código assíncrono como se fosse síncrono :

async def readALot():
    try:
        result1 = await read("url_1")
        result2 = await read("url_2")
    except Exception:
        console.warn("Reading a lot failed")
Pieter Jan Bonestroo
fonte
29

Usando Promessa

A resposta mais perfeita para esta pergunta está usando Promise.

function ajax(method, url, params) {
  return new Promise(function(resolve, reject) {
    var xhr = new XMLHttpRequest();
    xhr.onload = function() {
      resolve(this.responseText);
    };
    xhr.onerror = reject;
    xhr.open(method, url);
    xhr.send(params);
  });
}

Uso

ajax("GET", "/test", "acrive=1").then(function(result) {
    // Code depending on result
})
.catch(function() {
    // An error occurred
});

Mas espere...!

Há um problema com o uso de promessas!

Por que devemos usar nossa própria promessa personalizada?

Eu estava usando esta solução por um tempo até descobrir que havia um erro nos navegadores antigos:

Uncaught ReferenceError: Promise is not defined

Então, decidi implementar minha própria classe Promise para o ES3 abaixo dos compiladores js, se não estiver definido. Basta adicionar esse código antes do código principal e usar o Promise com segurança!

if(typeof Promise === "undefined"){
    function _classCallCheck(instance, Constructor) {
        if (!(instance instanceof Constructor)) { 
            throw new TypeError("Cannot call a class as a function"); 
        }
    }
    var Promise = function () {
        function Promise(main) {
            var _this = this;
            _classCallCheck(this, Promise);
            this.value = undefined;
            this.callbacks = [];
            var resolve = function resolve(resolveValue) {
                _this.value = resolveValue;
                _this.triggerCallbacks();
            };
            var reject = function reject(rejectValue) {
                _this.value = rejectValue;
                _this.triggerCallbacks();
            };
            main(resolve, reject);
        }
        Promise.prototype.then = function then(cb) {
            var _this2 = this;
            var next = new Promise(function (resolve) {
                _this2.callbacks.push(function (x) {
                    return resolve(cb(x));
                });
            });
            return next;
        };
        Promise.prototype.catch = function catch_(cb) {
            var _this2 = this;
            var next = new Promise(function (reject) {
                _this2.callbacks.push(function (x) {
                    return reject(cb(x));
                });
            });
            return next;
        };
        Promise.prototype.triggerCallbacks = function triggerCallbacks() {
            var _this3 = this;
            this.callbacks.forEach(function (cb) {
                cb(_this3.value);
            });
        };
        return Promise;
    }();
}
Amir Fo
fonte
28

É claro que existem muitas abordagens, como solicitação síncrona, promessa, mas, pela minha experiência, acho que você deve usar a abordagem de retorno de chamada. É natural o comportamento assíncrono do Javascript. Portanto, seu snippet de código pode ser reescrito um pouco diferente:

function foo() {
    var result;

    $.ajax({
        url: '...',
        success: function(response) {
            myCallback(response);
        }
    });

    return result;
}

function myCallback(response) {
    // Does something.
}
Khoa Bui
fonte
5
Não há nada inerentemente assíncrono em retornos de chamada ou JavaScript.
Aluan Haddad
19

Em vez de lançar código para você, existem 2 conceitos que são essenciais para entender como o JS lida com retornos de chamada e assincronicidade. (Isso é mesmo uma palavra?)

O Modelo de Loop e Concorrência de Eventos

Você precisa estar ciente de três coisas; A fila; o loop de eventos e a pilha

Em termos amplos e simplistas, o loop de eventos é como o gerente de projetos, está constantemente ouvindo todas as funções que desejam executar e se comunicar entre a fila e a pilha.

while (queue.waitForMessage()) {
   queue.processNextMessage();
}

Depois de receber uma mensagem para executar algo, ela é adicionada à fila. A fila é a lista de itens que estão aguardando para serem executados (como sua solicitação AJAX). imagine assim:

 1. call foo.com/api/bar using foobarFunc
 2. Go perform an infinite loop
 ... and so on

Quando uma dessas mensagens é executada, ela aparece a mensagem da fila e cria uma pilha, a pilha é tudo que o JS precisa executar para executar a instrução na mensagem. Então, no nosso exemplo, está sendo dito para ligarfoobarFunc

function foobarFunc (var) {
  console.log(anotherFunction(var));
}

Portanto, qualquer coisa que foobarFunc precise executar (no nosso caso anotherFunction) será empurrada para a pilha. executado e esquecido - o loop de eventos passará para a próxima coisa na fila (ou ouvirá mensagens)

A principal coisa aqui é a ordem de execução. Isso é

QUANDO algo vai correr

Quando você faz uma chamada usando AJAX para uma parte externa ou executa qualquer código assíncrono (um setTimeout, por exemplo), o Javascript depende de uma resposta antes de prosseguir.

A grande questão é quando vai receber a resposta? A resposta é que não sabemos - então o loop de eventos está aguardando a mensagem dizer "ei, corra-me". Se o JS apenas esperasse por essa mensagem de forma síncrona, seu aplicativo congelaria e seria péssimo. Assim, o JS continua executando o próximo item na fila enquanto aguarda a inclusão da mensagem na fila.

É por isso que, com funcionalidade assíncrona, usamos itens chamados retornos de chamada . É como uma promessa literalmente. Como prometo retornar algo em algum momento, o jQuery usa retornos de chamada específicos chamados deffered.done deffered.faile deffered.always(entre outros). Você pode vê-los todos aqui

Portanto, o que você precisa fazer é passar uma função que promete executar em algum momento com os dados que são passados ​​para ela.

Como um retorno de chamada não é executado imediatamente, mas posteriormente é importante passar a referência para a função, não executada. tão

function foo(bla) {
  console.log(bla)
}

então na maioria das vezes (mas nem sempre) você foonão passafoo()

Espero que isso faça algum sentido. Quando você encontra coisas como essa que parecem confusas - eu recomendo a leitura completa da documentação para pelo menos entender isso. Isso fará de você um desenvolvedor muito melhor.

Matthew Brent
fonte
18

Usando o ES2017, você deve ter isso como a declaração de função

async function foo() {
    var response = await $.ajax({url: '...'})
    return response;
}

E executando assim.

(async function() {
    try {
        var result = await foo()
        console.log(result)
    } catch (e) {}
})()

Ou a sintaxe da promessa

foo().then(response => {
    console.log(response)

}).catch(error => {
    console.log(error)

})
Fernando Carvajal
fonte
essa segunda função poderia ser reutilizável?
Zum Dummi 17/02/19
Como você usa os resultados se oncolse, log é chamado? Tudo não vai para o console nesse ponto?
Ken Ingram