Chamar uma função Javascript assíncrona de forma síncrona

222

Primeiro, este é um caso muito específico de fazê-lo da maneira errada com o objetivo de adaptar uma chamada assíncrona em uma base de código muito síncrona com milhares de linhas e o tempo atualmente não oferece a capacidade de fazer as alterações " certo. " Dói todas as fibras do meu ser, mas a realidade e os ideais geralmente não se misturam. Eu sei que isso é péssimo.

OK, fora do caminho, como faço para que eu possa:

function doSomething() {

  var data;

  function callBack(d) {
    data = d;
  }

  myAsynchronousCall(param1, callBack);

  // block here and return data when the callback is finished
  return data;
}

Os exemplos (ou a falta deles) usam bibliotecas e / ou compiladores, os quais não são viáveis ​​para esta solução. Eu preciso de um exemplo concreto de como fazê-lo bloquear (por exemplo, NÃO deixe a função doSomething até que o retorno de chamada seja chamado) SEM congelar a interface do usuário. Se tal coisa é possível em JS.

Robert C. Barth
fonte
16
Simplesmente não é possível fazer um navegador bloquear e esperar. Eles simplesmente não fazem isso.
Pointy
2
javascript dosent ter mecanismos de bloqueio na maioria dos navegadores ... você vai querer criar um callback que é chamada quando os acabamentos chamada assíncrona para retornar os dados
Nadir Muzaffar
8
Você está pedindo uma maneira de informar ao navegador "Eu sei que acabei de lhe dizer para executar a função anterior de forma assíncrona, mas eu realmente não quis dizer isso!". Por que você esperaria que isso fosse possível?
Wayne
2
Obrigado Dan pela edição. Eu não estava sendo estritamente rude, mas sua redação é melhor.
Robert C. Barth
2
@ RobertC.Barth Agora também é possível com JavaScript. As funções de espera assíncrona ainda não foram ratificadas no padrão, mas estão planejadas para o ES2017. Veja minha resposta abaixo para mais detalhes.
John

Respostas:

135

"não me diga como devo fazê-lo" do jeito certo "ou o que seja"

ESTÁ BEM. mas você realmente deve fazê-lo da maneira certa ... ou o que quer

"Eu preciso de um exemplo concreto de como fazê-lo bloquear ... SEM congelar a interface do usuário. Se isso é possível em JS."

Não, é impossível bloquear o JavaScript em execução sem bloquear a interface do usuário.

Dada a falta de informações, é difícil oferecer uma solução, mas uma opção pode ser a função de chamada fazer alguma pesquisa para verificar uma variável global e definir o retorno de chamada datacomo global.

function doSomething() {

      // callback sets the received data to a global var
  function callBack(d) {
      window.data = d;
  }
      // start the async
  myAsynchronousCall(param1, callBack);

}

  // start the function
doSomething();

  // make sure the global is clear
window.data = null

  // start polling at an interval until the data is found at the global
var intvl = setInterval(function() {
    if (window.data) { 
        clearInterval(intvl);
        console.log(data);
    }
}, 100);

Tudo isso pressupõe que você pode modificar doSomething(). Não sei se isso está nos cartões.

Se puder ser modificado, não sei por que você não passaria apenas um retorno de chamada doSomething()para ser chamado pelo outro retorno de chamada, mas é melhor parar antes que eu tenha problemas. ;)


Oh, que diabos. Você deu um exemplo que sugere que isso pode ser feito corretamente, então eu vou mostrar essa solução ...

function doSomething( func ) {

  function callBack(d) {
    func( d );
  }

  myAsynchronousCall(param1, callBack);

}

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

Como seu exemplo inclui um retorno de chamada que é passado para a chamada assíncrona, o caminho certo seria passar uma função doSomething()a ser invocada a partir do retorno de chamada.

Claro que, se essa é a única coisa que o retorno de chamada está fazendo, você passaria funcdiretamente ...

myAsynchronousCall(param1, func);
user1106925
fonte
22
Sim, eu sei como fazê-lo corretamente, preciso saber como / se isso pode ser feito incorretamente pelo motivo específico indicado. O ponto crucial é que eu não quero deixar doSomething () até que myAsynchronousCall conclua a chamada para a função de retorno de chamada. Bleh, isso não pode ser feito, como eu suspeitava, eu só precisava da sabedoria coletiva dos Internets para me apoiar. Obrigado. :-)
Robert C. Barth
2
@ RobertC.Barth: Sim, infelizmente suas suspeitas estavam corretas.
Sou eu ou apenas a versão "feita corretamente" funciona? A questão incluída uma chamada de retorno, diante do qual não deve algo que aguarda a chamada assíncrona ao fim, que esta primeira parte desta resposta não cobre ...
ravemir
@ravemir: A resposta afirma que não é possível fazer o que ele quer. Essa é a parte importante a entender. Em outras palavras, você não pode fazer uma chamada assíncrona e retornar um valor sem bloquear a interface do usuário. Portanto, a primeira solução é um truque feio usando uma variável global e sondagem para ver se essa variável foi modificada. A segunda versão é a maneira correta.
1
@ Leonardo: É a função misteriosa que está sendo chamada na pergunta. Basicamente, representa qualquer coisa que executa o código de forma assíncrona e produz um resultado que precisa ser recebido. Portanto, pode ser como uma solicitação AJAX. Você passa a callbackfunção para a myAsynchronousCallfunção, que faz coisas assíncronas e chama o retorno de chamada quando concluída. Aqui está uma demonstração.
60

As funções assíncronas , um recurso do ES2017 , fazem o código assíncrono parecer sincronizado usando promessas (uma forma específica de código assíncrono) e a awaitpalavra - chave. Observe também nos exemplos de código abaixo da palavra-chave asyncna frente da functionpalavra - chave que significa uma função assíncrona / aguardada. A awaitpalavra-chave não funcionará sem estar em uma função pré-fixada com a asyncpalavra - chave. Como atualmente não há nenhuma exceção a isso, significa que nenhum nível superior de espera funcionará (nível superior aguarda, o que significa uma espera fora de qualquer função). Embora exista uma proposta de nível superiorawait .

O ES2017 foi ratificado (ou seja, finalizado) como padrão para JavaScript em 27 de junho de 2017. O Async aguardar já pode funcionar no seu navegador, mas, se não, você ainda pode usar a funcionalidade usando um transpilador javascript, como babel ou traceur . O Chrome 55 oferece suporte total às funções assíncronas. Portanto, se você tiver um navegador mais novo, poderá experimentar o código abaixo.

Consulte a tabela de compatibilidade es2017 da kangax para obter compatibilidade com o navegador.

Aqui está um exemplo de função de espera assíncrona chamada, doAsyncque realiza três pausas de um segundo e imprime a diferença horária após cada pausa a partir da hora de início:

function timeoutPromise (time) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(Date.now());
    }, time)
  })
}

function doSomethingAsync () {
  return timeoutPromise(1000);
}

async function doAsync () {
  var start = Date.now(), time;
  console.log(0);
  time = await doSomethingAsync();
  console.log(time - start);
  time = await doSomethingAsync();
  console.log(time - start);
  time = await doSomethingAsync();
  console.log(time - start);
}

doAsync();

Quando a palavra-chave wait for colocada antes de um valor prometido (nesse caso, o valor prometido é o valor retornado pela função doSomethingAsync), a palavra-chave wait aguardará a execução da chamada da função, mas não pausará nenhuma outra função e continuará executando outro código até que a promessa seja resolvida. Depois que a promessa for resolvida, ela desembrulhará o valor da promessa e você poderá pensar na expressão de espera e promessa como sendo substituída agora por esse valor desembrulhado.

Portanto, uma vez que wait apenas pausa, espera e, em seguida, desembrulha um valor antes de executar o restante da linha, você pode usá-lo para loops e chamadas de funções internas, como no exemplo abaixo, que coleta diferenças de tempo esperadas em uma matriz e imprime a matriz.

function timeoutPromise (time) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(Date.now());
    }, time)
  })
}

function doSomethingAsync () {
  return timeoutPromise(1000);
}

// this calls each promise returning function one after the other
async function doAsync () {
  var response = [];
  var start = Date.now();
  // each index is a promise returning function
  var promiseFuncs= [doSomethingAsync, doSomethingAsync, doSomethingAsync];
  for(var i = 0; i < promiseFuncs.length; ++i) {
    var promiseFunc = promiseFuncs[i];
    response.push(await promiseFunc() - start);
    console.log(response);
  }
  // do something with response which is an array of values that were from resolved promises.
  return response
}

doAsync().then(function (response) {
  console.log(response)
})

A própria função assíncrona retorna uma promessa para que você possa usá-la como encadeamento, como eu faço acima ou dentro de outra função de espera assíncrona.

A função acima aguardaria cada resposta antes de enviar outra solicitação, se você quiser enviar as solicitações simultaneamente, pode usar Promise.all .

// no change
function timeoutPromise (time) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(Date.now());
    }, time)
  })
}

// no change
function doSomethingAsync () {
  return timeoutPromise(1000);
}

// this function calls the async promise returning functions all at around the same time
async function doAsync () {
  var start = Date.now();
  // we are now using promise all to await all promises to settle
  var responses = await Promise.all([doSomethingAsync(), doSomethingAsync(), doSomethingAsync()]);
  return responses.map(x=>x-start);
}

// no change
doAsync().then(function (response) {
  console.log(response)
})

Se a promessa possivelmente rejeitar, você pode envolvê-la em uma tentativa de captura ou pular a tentativa de captura e deixar o erro se propagar para a chamada de captura de funções assíncronas / aguardadas. Você deve ter cuidado para não deixar erros de promessa sem tratamento, especialmente no Node.js. Abaixo estão alguns exemplos que mostram como os erros funcionam.

function timeoutReject (time) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      reject(new Error("OOPS well you got an error at TIMESTAMP: " + Date.now()));
    }, time)
  })
}

function doErrorAsync () {
  return timeoutReject(1000);
}

var log = (...args)=>console.log(...args);
var logErr = (...args)=>console.error(...args);

async function unpropogatedError () {
  // promise is not awaited or returned so it does not propogate the error
  doErrorAsync();
  return "finished unpropogatedError successfully";
}

unpropogatedError().then(log).catch(logErr)

async function handledError () {
  var start = Date.now();
  try {
    console.log((await doErrorAsync()) - start);
    console.log("past error");
  } catch (e) {
    console.log("in catch we handled the error");
  }
  
  return "finished handledError successfully";
}

handledError().then(log).catch(logErr)

// example of how error propogates to chained catch method
async function propogatedError () {
  var start = Date.now();
  var time = await doErrorAsync() - start;
  console.log(time - start);
  return "finished propogatedError successfully";
}

// this is what prints propogatedError's error.
propogatedError().then(log).catch(logErr)

Se você for aqui, poderá ver as propostas finalizadas para as próximas versões do ECMAScript.

Uma alternativa a isso que pode ser usada apenas com o ES2015 (ES6) é usar uma função especial que envolve uma função de gerador. As funções do gerador têm uma palavra-chave yield que pode ser usada para replicar a palavra-chave wait com uma função circundante. A palavra-chave yield e a função de gerador são muito mais genéricas e podem fazer muito mais coisas do que a função de espera assíncrona. Se você quer um invólucro função de gerador que pode ser usado para assíncrono replicar esperam que eu gostaria check-out co.js . A propósito, as funções co, assim como as funções de espera assíncrona, retornam uma promessa. Honestamente, neste ponto, a compatibilidade do navegador é praticamente a mesma para as funções de gerador e funções assíncronas; portanto, se você deseja apenas a funcionalidade de espera assíncrona, deve usar as funções assíncronas sem co.js.

Atualmente, o suporte ao navegador é muito bom para as funções Async (a partir de 2017) em todos os principais navegadores atuais (Chrome, Safari e Edge), exceto no IE.

John
fonte
2
Eu gosto desta resposta
ycomp 10/07
1
quão longe nós viemos :)
Derek
3
Essa é uma ótima resposta, mas para o problema dos pôsteres originais, acho que tudo o que faz é elevar o problema um nível acima. Digamos que ele transforma algo em uma função assíncrona com uma espera dentro. Agora, essa função retorna uma promessa e é assíncrona; portanto, ele terá que lidar com o mesmo problema novamente, independentemente do que chamar essa função.
Dpwrussell 20/05/19
1
@ dpwrussell isso é verdade, há uma fluência de funções assíncronas e promessas na base de código. A melhor maneira de resolver as promessas de tudo é apenas escrever retornos de chamada síncronos. Não há como retornar um valor assíncrono de forma síncrona, a menos que você faça algo extremamente estranho e controverso como esse twitter.com/sebmarkbage/status/941214259505119232, que eu não faço recomendo. Adicionarei uma edição ao final da pergunta para responder mais completamente à pergunta como foi solicitada e não apenas responder ao título.
João
É uma ótima resposta +1 e tudo, mas, como está, não vejo como isso é menos complicado do que usar retornos de chamada.
Altimus Prime 25/09/19
47

Dê uma olhada no JQuery Promises:

http://api.jquery.com/promise/

http://api.jquery.com/jQuery.when/

http://api.jquery.com/deferred.promise/

Refatore o código:

    var dfd = novo jQuery.Deferred ();


    função callBack (data) {
       dfd.notify (dados);
    }

    // faça a chamada assíncrona.
    myAsynchronousCall (param1, callBack);

    função doSomething (data) {
     // faz coisas com dados ...
    }

    $ .when (dfd) .then (doSomething);


Matt Taylor
fonte
3
+1 para esta resposta, está correto. no entanto, eu atualizaria a linha com dfd.notify(data)paradfd.resolve(data)
Jason
7
É este o caso do código que dá a ilusão de ser síncrono, sem realmente NÃO ser assíncrono?
saurshaz
2
promessas são IMO, apenas retornos de chamada bem organizados :) se você precisar de uma chamada assíncrona, digamos alguma inicialização de objeto, as promessas fazem uma pequena diferença.
Webduvet 8/09/14
10
Promessas não são sincronizadas.
Vans S
6

Há uma boa solução alternativa em http://taskjs.org/

Ele usa geradores que são novos no javascript. Portanto, atualmente não é implementado pela maioria dos navegadores. Eu testei no firefox, e para mim é uma boa maneira de quebrar a função assíncrona.

Aqui está o código de exemplo do projeto GitHub

var { Deferred } = task;

spawn(function() {
    out.innerHTML = "reading...\n";
    try {
        var d = yield read("read.html");
        alert(d.responseText.length);
    } catch (e) {
        e.stack.split(/\n/).forEach(function(line) { console.log(line) });
        console.log("");
        out.innerHTML = "error: " + e;
    }

});

function read(url, method) {
    method = method || "GET";
    var xhr = new XMLHttpRequest();
    var deferred = new Deferred();
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) {
            if (xhr.status >= 400) {
                var e = new Error(xhr.statusText);
                e.status = xhr.status;
                deferred.reject(e);
            } else {
                deferred.resolve({
                    responseText: xhr.responseText
                });
            }
        }
    };
    xhr.open(method, url, true);
    xhr.send();
    return deferred.promise;
}
George Vinokhodov
fonte
3

Você pode forçar o JavaScript assíncrono no NodeJS a ser síncrono com o sync-rpc .

Definitivamente, irá congelar sua interface do usuário, por isso ainda sou um pessimista quando se trata do que é possível usar o atalho necessário. Não é possível suspender o thread único e único em JavaScript, mesmo que o NodeJS permita que você o bloqueie às vezes. Nenhum retorno de chamada, evento ou qualquer coisa assíncrona poderá processar até que sua promessa seja resolvida. Portanto, a menos que o leitor tenha uma situação inevitável como o OP (ou, no meu caso, esteja escrevendo um script de shell glorificado sem retornos de chamada, eventos etc.), NÃO FAÇA ISSO!

Mas aqui está como você pode fazer isso:

./calling-file.js

var createClient = require('sync-rpc');
var mySynchronousCall = createClient(require.resolve('./my-asynchronous-call'), 'init data');

var param1 = 'test data'
var data = mySynchronousCall(param1);
console.log(data); // prints: received "test data" after "init data"

./my-asynchronous-call.js

function init(initData) {
  return function(param1) {
    // Return a promise here and the resulting rpc client will be synchronous
    return Promise.resolve('received "' + param1 + '" after "' + initData + '"');
  };
}
module.exports = init;

LIMITAÇÕES:

Ambos são uma consequência de como sync-rpcé implementado, que é abusar require('child_process').spawnSync:

  1. Isso não funcionará no navegador.
  2. Os argumentos para sua função devem ser serializáveis. Seus argumentos serão transferidos para dentro e para fora JSON.stringify, para que funções e propriedades não enumeráveis, como cadeias de protótipos, sejam perdidas.
meustrus
fonte
1

Você também pode convertê-lo em retornos de chamada.

function thirdPartyFoo(callback) {    
  callback("Hello World");    
}

function foo() {    
  var fooVariable;

  thirdPartyFoo(function(data) {
    fooVariable = data;
  });

  return fooVariable;
}

var temp = foo();  
console.log(temp);
Nikhil
fonte
0

O que você quer é realmente possível agora. Se você pode executar o código assíncrono em um trabalhador de serviço e o código síncrono em um trabalhador da Web, pode pedir ao trabalhador da Web que envie um XHR síncrono ao trabalhador de serviço e, enquanto o trabalhador de serviço faz as coisas assíncronas, o trabalhador da Web thread vai esperar. Esta não é uma ótima abordagem, mas poderia funcionar.

Talvez você veja esse nome.
fonte
-4

A idéia que você espera alcançar pode ser possível se você ajustar um pouco o requisito

O código abaixo é possível se o seu tempo de execução suportar a especificação ES6.

Mais sobre funções assíncronas

async function myAsynchronousCall(param1) {
    // logic for myAsynchronous call
    return d;
}

function doSomething() {

  var data = await myAsynchronousCall(param1); //'blocks' here until the async call is finished
  return data;
}
eragon512
fonte
4
Firefox dá o erro: SyntaxError: await is only valid in async functions and async generators. Sem mencionar que o param1 não está definido (e nem usado).
Harvey