Chiamare una funzione Javascript asincrona in modo sincrono


222

In primo luogo, questo è un caso molto specifico di farlo nel modo sbagliato di proposito per adeguare una chiamata asincrona in una base di codice molto sincrona che è lunga migliaia di linee e che il tempo non offre attualmente la possibilità di apportare le modifiche a "fare giusto ". Mi fa male ogni fibra del mio essere, ma la realtà e gli ideali spesso non si intrecciano. So che fa schifo.

OK, a parte questo, come posso farlo in modo da poter:

function doSomething() {

  var data;

  function callBack(d) {
    data = d;
  }

  myAsynchronousCall(param1, callBack);

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

Gli esempi (o la mancanza di questi) utilizzano tutti librerie e / o compilatori, entrambi non fattibili per questa soluzione. Ho bisogno di un esempio concreto di come bloccarlo (ad es. NON lasciare la funzione doSomething fino a quando non viene chiamato il callback) SENZA bloccare l'interfaccia utente. Se una cosa del genere è possibile in JS.


16
Semplicemente non è possibile creare un blocco del browser e attendere. Semplicemente non lo faranno.
Punta

2
javascript dosent con meccanismi di blocco sulla maggior parte dei browser ... ti consigliamo di creare un callback che viene chiamato quando termina la chiamata asincrona per restituire i dati
Nadir Muzaffar

8
Stai chiedendo un modo per dire al browser "So di averti appena detto di eseguire la funzione precedente in modo asincrono, ma non intendevo davvero!". Perché ti aspetteresti che fosse possibile?
Wayne,

2
Grazie Dan per la modifica. Non ero rigorosamente maleducato, ma la tua formulazione è migliore.
Robert C. Barth,

2
@ RobertC.Barth Ora è possibile anche con JavaScript. le funzioni di attesa asincrona non sono state ancora ratificate nello standard, ma sono previste per ES2017. Vedi la mia risposta di seguito per maggiori dettagli.
Giovanni

Risposte:


135

"non dirmi come dovrei farlo" nel modo giusto "o qualunque cosa"

OK. ma dovresti davvero farlo nel modo giusto ... o qualunque cosa

"Ho bisogno di un esempio concreto di come bloccarlo ... SENZA congelare l'interfaccia utente. Se una cosa del genere è possibile in JS."

No, è impossibile bloccare JavaScript in esecuzione senza bloccare l'interfaccia utente.

Data la mancanza di informazioni, è difficile offrire una soluzione, ma un'opzione potrebbe essere quella di fare in modo che la funzione di chiamata esegua il polling per controllare una variabile globale, quindi impostare il callback datasu globale.

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

Tutto ciò presuppone che tu possa modificare doSomething(). Non so se sia nelle carte.

Se può essere modificato, non so perché non si debba semplicemente passare un callback doSomething()per essere chiamato dall'altro callback, ma è meglio che mi fermi prima di mettermi nei guai. ;)


Oh, che diamine. Hai fornito un esempio che suggerisce che può essere fatto correttamente, quindi mostrerò quella soluzione ...

function doSomething( func ) {

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

  myAsynchronousCall(param1, callBack);

}

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

Poiché il tuo esempio include un callback passato alla chiamata asincrona, il modo giusto sarebbe passare una funzione doSomething()da invocare dal callback.

Naturalmente se questa è l'unica cosa che sta facendo il callback, passeresti funcdirettamente ...

myAsynchronousCall(param1, func);

22
Sì, so come farlo correttamente, devo sapere come / se può essere fatto in modo errato per il motivo specifico indicato. Il punto cruciale è che non voglio lasciare doSomething () fino a quando myAsynchronousCall completa la chiamata alla funzione di richiamata. Bleh, non si può fare, come sospettavo, avevo solo bisogno della saggezza raccolta degli Internet per supportarmi. Grazie. :-)
Robert C. Barth

2
@ RobertC.Barth: Sì, sfortunatamente i tuoi sospetti erano corretti.

Sono io o solo la versione "fatto correttamente" funziona? La domanda includeva una chiamata di ritorno, prima della quale dovrebbe esserci qualcosa che attende il termine della chiamata asincrona, che questa prima parte di questa risposta non copre ...
ravemir,

@ravemir: la risposta afferma che non è possibile fare ciò che vuole. Questa è la parte importante da capire. In altre parole, non è possibile effettuare una chiamata asincrona e restituire un valore senza bloccare l'interfaccia utente. Quindi la prima soluzione è un brutto hack che utilizza una variabile globale e il polling per vedere se quella variabile è stata modificata. La seconda versione è il modo corretto.

1
@Leonardo: è la misteriosa funzione chiamata nella domanda. Fondamentalmente rappresenta tutto ciò che esegue il codice in modo asincrono e produce un risultato che deve essere ricevuto. Quindi potrebbe essere come una richiesta AJAX. Si passa la callbackfunzione alla myAsynchronousCallfunzione, che fa la sua roba asincrona e invoca il callback quando completo. Ecco una demo.

60

Le funzioni asincrone , una funzionalità di ES2017 , fanno sembrare il codice asincrono sincronizzato usando le promesse (una particolare forma di codice asincrono) e la awaitparola chiave. Notare anche negli esempi di codice sotto la parola chiave asyncdavanti alla functionparola chiave che indica una funzione asincrona / waitit. La awaitparola chiave non funzionerà senza essere in una funzione prestabilita con la asyncparola chiave. Dal momento che attualmente non vi è alcuna eccezione a ciò, il che significa che nessun livello di attesa superiore funzionerà (il livello di attesa superiore significa un'attesa al di fuori di qualsiasi funzione). Sebbene ci sia una proposta per il massimo livelloawait .

ES2017 è stato ratificato (ovvero finalizzato) come standard per JavaScript il 27 giugno 2017. Async wait potrebbe già funzionare nel tuo browser, ma in caso contrario puoi comunque utilizzare la funzionalità utilizzando un transpiler javascript come babel o traceur . Chrome 55 ha il pieno supporto delle funzioni asincrone. Quindi, se hai un browser più recente, potresti essere in grado di provare il codice qui sotto.

Vedi la tabella di compatibilità es2017 di kangax per la compatibilità del browser.

Ecco un esempio di funzione di attesa asincrona chiamata doAsyncche richiede tre pause di un secondo e stampa la differenza oraria dopo ogni pausa dall'ora di inizio:

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 la parola chiave wait viene posizionata prima di un valore di promessa (in questo caso il valore di promessa è il valore restituito dalla funzione doSomethingAsync) la parola chiave wait attende metterà in pausa l'esecuzione della chiamata di funzione, ma non metterà in pausa altre funzioni e continuerà eseguendo altro codice fino alla risoluzione della promessa. Dopo che la promessa si è risolta, scarterà il valore della promessa e puoi pensare all'espressione di attesa e promessa come ora sostituita da quel valore da scartare.

Quindi, poiché wait attende solo pause, quindi scartare un valore prima di eseguire il resto della linea, è possibile utilizzarlo per i loop e le chiamate di funzioni interne come nell'esempio seguente che raccoglie le differenze di tempo attese in un array e stampa l'array.

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

La stessa funzione asincrona restituisce una promessa in modo da poterla utilizzare come promessa con il concatenamento come faccio sopra o all'interno di un'altra funzione di attesa asincrona.

La funzione sopra attenderebbe ogni risposta prima di inviare un'altra richiesta se si desidera inviare le richieste contemporaneamente è possibile utilizzare 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 la promessa può essere rifiutata, puoi racchiuderla in un catch di prova o saltare il tentativo di cattura e lasciare che l'errore si propaghi alle funzioni di cattura asincrona / wait. Dovresti stare attento a non lasciare errori promettenti, specialmente in Node.js. Di seguito sono riportati alcuni esempi che mostrano come funzionano gli errori.

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 vai qui puoi vedere le proposte finite per le prossime versioni di ECMAScript.

Un'alternativa a questa che può essere utilizzata solo con ES2015 (ES6) è quella di utilizzare una funzione speciale che avvolge una funzione del generatore. Le funzioni del generatore hanno una parola chiave yield che può essere utilizzata per replicare la parola chiave wait con una funzione circostante. La parola chiave yield e la funzione del generatore sono molto più generiche e possono fare molte più cose rispetto a ciò che fa la funzione di attesa asincrona. Se si desidera una funzione wrapper generatore che può essere utilizzato per asincrona replica attendere avrei fatto il check out co.js . A proposito, le funzioni di co molto come le funzioni di attesa asincrono restituiscono una promessa. Onestamente, sebbene a questo punto la compatibilità del browser sia pressoché identica sia per le funzioni del generatore che per le funzioni asincrone, quindi se si desidera semplicemente che la funzionalità di attesa asincrona sia necessario utilizzare le funzioni Async senza co.js.

Il supporto del browser è attualmente abbastanza buono per le funzioni Async (a partire dal 2017) in tutti i principali browser attuali (Chrome, Safari e Edge) tranne IE.


2
Mi piace questa risposta
ycomp

1
quanto siamo arrivati ​​:)
Derek

3
Questa è un'ottima risposta, ma per il problema dei poster originali, penso che tutto ciò che fa è spostare il problema su un livello. Supponi che trasforma doSomething in una funzione asincrona con un'attesa interna. Quella funzione ora restituisce una promessa ed è asincrona, quindi dovrà affrontare nuovamente lo stesso problema in qualunque chiamata quella funzione.
dpwrussell,

1
@dpwrussell questo è vero, c'è un brivido di funzioni asincrone e promesse nella base di codice. Il modo migliore per risolvere le promesse dall'insinuarsi a tutto è solo scrivere callback sincroni, non c'è modo di restituire un valore asincrono in modo sincrono a meno che tu non faccia qualcosa di estremamente strano e controverso come questo twitter.com/sebmarkbage/status/941214259505119232 che non raccomandare. Aggiungerò una modifica alla fine della domanda per rispondere in modo più completo alla domanda come è stata posta e non solo per rispondere al titolo.
Giovanni,

È un'ottima risposta +1 e tutto, ma scritta così com'è, non vedo come sia meno complicato dell'uso dei callback.
Altimus Prime,

47

Dai un'occhiata alle promesse di JQuery:

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

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

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

Rifattorizzare il codice:

    var dfd = new jQuery.Deferred ();


    funzione callBack (dati) {
       dfd.notify (dati);
    }

    // effettua la chiamata asincrona.
    myAsynchronousCall (param1, callBack);

    funzione doSomething (data) {
     // fai cose con i dati ...
    }

    $ .Quando (DFD) .poi (doSomething);



3
+1 per questa risposta, questo è corretto. Tuttavia, vorrei aggiornare la linea con dfd.notify(data)adfd.resolve(data)
Jason

7
È questo un caso del codice che dà l'illusione di essere sincrono, senza in realtà NON essere asincrono?
Saurshaz,

2
le promesse sono solo callback IMO ben organizzati :) se hai bisogno di una chiamata asincrona diciamo un po 'di inizializzazione dell'oggetto, che le promesse fanno una piccola differenza.
webduvet,

10
Le promesse non sono sincronizzate.
Vans S,

6

C'è una bella soluzione a http://taskjs.org/

Usa generatori che sono nuovi a JavaScript. Quindi attualmente non è implementato dalla maggior parte dei browser. L'ho testato su Firefox e per me è un buon modo per avvolgere la funzione asincrona.

Ecco un esempio di codice dal progetto 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;
}

3

È possibile forzare JavaScript asincrono in NodeJS per essere sincrono con sync-rpc .

Tuttavia, bloccherà sicuramente la tua interfaccia utente, quindi sono ancora un disastro quando si tratta di sapere se è possibile prendere la scorciatoia che devi prendere. Non è possibile sospendere il thread One And Only in JavaScript, anche se NodeJS ti consente di bloccarlo a volte. Nessun callback, eventi, nulla di asincrono sarà in grado di elaborare fino a quando la promessa non si risolverà. Quindi, a meno che tu non abbia una situazione inevitabile come l'OP (o, nel mio caso, stai scrivendo uno script di shell glorificato senza richiamate, eventi, ecc.), NON FARLO!

Ma ecco come puoi farlo:

./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;

LIMITAZIONI:

Queste sono entrambe una conseguenza di come sync-rpcviene implementata, ovvero abusando di require('child_process').spawnSync:

  1. Questo non funzionerà nel browser.
  2. Gli argomenti della tua funzione devono essere serializzabili. I tuoi argomenti passeranno dentro e fuori JSON.stringify, quindi le funzioni e le proprietà non enumerabili come le catene di prototipi andranno perse.

1

Puoi anche convertirlo in callback.

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

function foo() {    
  var fooVariable;

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

  return fooVariable;
}

var temp = foo();  
console.log(temp);

0

Ciò che vuoi è attualmente possibile. Se è possibile eseguire il codice asincrono in un lavoratore del servizio e il codice sincrono in un lavoratore Web, è possibile fare in modo che il lavoratore Web invii un XHR sincrono al lavoratore del servizio e mentre il lavoratore del servizio fa le cose asincrone, il lavoratore del web il thread attenderà. Questo non è un ottimo approccio, ma potrebbe funzionare.


-4

L'idea che speri di realizzare può essere resa possibile se modifichi un po 'il requisito

Il codice seguente è possibile se il tuo runtime supporta la specifica ES6.

Ulteriori informazioni sulle funzioni asincrone

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

4
Firefox dà l'errore: SyntaxError: await is only valid in async functions and async generators. Per non parlare del fatto che param1 non è definito (e nemmeno usato).
Harvey,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.