Rufen Sie eine asynchrone Javascript-Funktion synchron auf

221

Erstens ist dies ein sehr spezifischer Fall, bei dem es absichtlich falsch gemacht wird, einen asynchronen Aufruf in eine sehr synchrone Codebasis nachzurüsten, die viele tausend Zeilen lang ist und die Zeit derzeit nicht die Möglichkeit bietet, die Änderungen an "do" vorzunehmen es richtig." Es tut jeder Faser meines Seins weh, aber Realität und Ideale greifen oft nicht ineinander. Ich weiß, das ist scheiße.

OK, das aus dem Weg, wie mache ich es so, dass ich:

function doSomething() {

  var data;

  function callBack(d) {
    data = d;
  }

  myAsynchronousCall(param1, callBack);

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

Die Beispiele (oder deren Fehlen) verwenden alle Bibliotheken und / oder Compiler, die beide für diese Lösung nicht geeignet sind. Ich brauche ein konkretes Beispiel, wie man es blockiert (z. B. die Funktion doSomething NICHT verlassen, bis der Rückruf aufgerufen wird), OHNE die Benutzeroberfläche einzufrieren. Wenn so etwas in JS möglich ist.

Robert C. Barth
quelle
16
Es ist einfach nicht möglich, einen Browser zu blockieren und zu warten. Sie werden es einfach nicht tun.
Pointy
2
Javascript Dosent mit Blockierungsmechanismen in den meisten Browsern ... Sie möchten einen Rückruf erstellen, der aufgerufen wird, wenn der asynchrone Aufruf beendet ist, um die Daten zurückzugeben
Nadir Muzaffar
8
Sie fragen nach einer Möglichkeit, dem Browser mitzuteilen: "Ich weiß, ich habe Ihnen gerade gesagt, dass Sie diese vorherige Funktion asynchron ausführen sollen, aber ich habe es nicht wirklich so gemeint!". Warum würden Sie überhaupt erwarten, dass dies möglich ist?
Wayne
2
Danke Dan für die Bearbeitung. Ich war nicht unbedingt unhöflich, aber Ihre Formulierung ist besser.
Robert C. Barth
2
@ RobertC.Barth Es ist jetzt auch mit JavaScript möglich. asynchrone Wartefunktionen wurden im Standard noch nicht ratifiziert, sollen jedoch in ES2017 enthalten sein. Siehe meine Antwort unten für weitere Details.
John

Antworten:

135

"Erzähl mir nicht, wie ich es einfach" richtig "machen soll oder was auch immer"

OK. aber du solltest es wirklich richtig machen ... oder was auch immer

"Ich brauche ein konkretes Beispiel, wie man es blockiert ... OHNE die Benutzeroberfläche einzufrieren. Wenn so etwas in JS möglich ist."

Nein, es ist unmöglich, das laufende JavaScript zu blockieren, ohne die Benutzeroberfläche zu blockieren.

Angesichts des Mangels an Informationen ist es schwierig, eine Lösung anzubieten. Eine Möglichkeit besteht jedoch darin, dass die aufrufende Funktion eine Abfrage durchführt, um eine globale Variable zu überprüfen, und dann den Rückruf dataauf global setzt.

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

All dies setzt voraus, dass Sie Änderungen vornehmen können doSomething() . Ich weiß nicht, ob das in den Karten ist.

Wenn es geändert werden kann, weiß ich nicht, warum Sie nicht einfach einen Rückruf an doSomething()den anderen Rückruf weiterleiten würden, aber ich höre besser auf, bevor ich in Schwierigkeiten gerate. ;)


Oh, was zum Teufel. Sie haben ein Beispiel gegeben, das darauf hinweist, dass es richtig gemacht werden kann, also werde ich diese Lösung zeigen ...

function doSomething( func ) {

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

  myAsynchronousCall(param1, callBack);

}

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

Da Ihr Beispiel einen Rückruf enthält, der an den asynchronen Aufruf übergeben wird, besteht der richtige Weg darin, eine Funktion zu übergeben doSomething(), die vom Rückruf aufgerufen werden soll.

Wenn dies das einzige ist, was der Rückruf bewirkt, würden Sie natürlich funcdirekt weitergeben ...

myAsynchronousCall(param1, func);
Benutzer1106925
quelle
22
Ja, ich weiß, wie man es richtig macht, ich muss wissen, wie / ob es aus dem angegebenen Grund falsch gemacht werden kann. Der springende Punkt ist, dass ich doSomething () erst verlassen möchte, wenn myAsynchronousCall den Aufruf der Rückruffunktion abgeschlossen hat. Bleh, es geht nicht, wie ich vermutet habe, ich brauchte nur die gesammelte Weisheit des Internets, um mich zu unterstützen. Danke dir. :-)
Robert C. Barth
2
@ RobertC.Barth: Ja, dein Verdacht war leider richtig.
Bin ich es oder nur die "richtig gemacht" Version funktioniert? Die Frage beinhaltete einen Rückruf, vor dem etwas warten sollte, bis der asynchrone Anruf beendet ist, was in diesem ersten Teil dieser Antwort nicht behandelt wird ...
Ravemir
@ravemir: Die Antwort besagt, dass es nicht möglich ist, das zu tun, was er will. Das ist der wichtige Teil zu verstehen. Mit anderen Worten, Sie können keinen asynchronen Aufruf tätigen und keinen Wert zurückgeben, ohne die Benutzeroberfläche zu blockieren. Die erste Lösung ist also ein hässlicher Hack, bei dem eine globale Variable verwendet und abgefragt wird, ob diese Variable geändert wurde. Die zweite Version ist der richtige Weg.
1
@ Leonardo: Es ist die mysteriöse Funktion, die in der Frage genannt wird. Grundsätzlich stellt es alles dar, was Code asynchron ausführt und ein Ergebnis erzeugt, das empfangen werden muss. Es könnte also wie eine AJAX-Anfrage sein. Sie übergeben die callbackFunktion an die myAsynchronousCallFunktion, die ihre asynchronen Aufgaben ausführt und den Rückruf aufruft, wenn sie abgeschlossen ist. Hier ist eine Demo.
60

Async-Funktionen , eine Funktion in ES2017 , sorgen dafür , dass Async-Code mithilfe von Versprechungen (einer bestimmten Form von Async-Code) und dem awaitSchlüsselwort synchronisiert aussieht . Beachten Sie auch in den Codebeispielen unter dem Schlüsselwort asyncvor dem functionSchlüsselwort, das eine asynchrone / wartende Funktion kennzeichnet. Das awaitSchlüsselwort funktioniert nicht, ohne in einer Funktion zu sein, die mit dem asyncSchlüsselwort voreingestellt ist . Da es derzeit keine Ausnahme gibt, bedeutet dies, dass keine Wartezeiten der obersten Ebene funktionieren (Wartezeiten der obersten Ebene bedeuten ein Warten außerhalb einer Funktion). Es gibt jedoch einen Vorschlag für die oberste Ebeneawait .

ES2017 wurde am 27. Juni 2017 als Standard für JavaScript ratifiziert (dh finalisiert). Async await funktioniert möglicherweise bereits in Ihrem Browser. Andernfalls können Sie die Funktionalität weiterhin mit einem Javascript-Transpiler wie babel oder traceur verwenden . Chrome 55 bietet volle Unterstützung für asynchrone Funktionen. Wenn Sie also einen neueren Browser haben, können Sie möglicherweise den folgenden Code ausprobieren.

Siehe kangax des es2017 Kompatibilitätstabelle für Browser - Kompatibilität.

Hier ist ein Beispiel für eine asynchrone Wartefunktion, doAsyncdie drei Pausen von einer Sekunde benötigt und die Zeitdifferenz nach jeder Pause ab der Startzeit ausgibt:

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

Wenn das Schlüsselwort await vor einem Versprechungswert steht (in diesem Fall ist der Versprechenswert der von der Funktion doSomethingAsync zurückgegebene Wert), pausiert das Schlüsselwort await die Ausführung des Funktionsaufrufs, hält jedoch keine anderen Funktionen an und fährt fort Ausführen von anderem Code, bis das Versprechen aufgelöst ist. Nachdem das Versprechen aufgelöst wurde, wird der Wert des Versprechens ausgepackt, und Sie können sich vorstellen, dass der Ausdruck Warten und Versprechen jetzt durch diesen ausgepackten Wert ersetzt wird.

Da das Warten nur pausiert und wartet, wird dann ein Wert entpackt, bevor der Rest der Zeile ausgeführt wird. Sie können ihn für Schleifen und interne Funktionsaufrufe wie im folgenden Beispiel verwenden, in dem die in einem Array erwarteten Zeitunterschiede erfasst und das Array ausgedruckt werden.

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

Die asynchrone Funktion selbst gibt ein Versprechen zurück, sodass Sie dieses als Versprechen mit Verkettung verwenden können, wie ich es über oder innerhalb einer anderen asynchronen Wartefunktion tue.

Die obige Funktion wartet auf jede Antwort, bevor eine weitere Anfrage gesendet wird . Wenn Sie die Anfragen gleichzeitig senden möchten, können Sie Promise.all verwenden .

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

Wenn das Versprechen möglicherweise abgelehnt wird, können Sie es in einen Try-Catch einschließen oder den Try-Catch überspringen und den Fehler auf den Catch-Aufruf der asynchronen / wait-Funktionen übertragen. Sie sollten darauf achten, Versprechungsfehler nicht unbehandelt zu lassen, insbesondere in Node.js. Im Folgenden finden Sie einige Beispiele, die zeigen, wie Fehler funktionieren.

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)

Wenn du hierher gehst Sie die fertigen Vorschläge für kommende ECMAScript-Versionen.

Eine Alternative dazu, die nur mit ES2015 (ES6) verwendet werden kann, ist die Verwendung einer speziellen Funktion, die eine Generatorfunktion umschließt. Generatorfunktionen verfügen über ein Yield-Schlüsselwort, mit dem das Schlüsselwort await mit einer umgebenden Funktion repliziert werden kann. Das Yield-Schlüsselwort und die Generatorfunktion sind viel allgemeiner und können viel mehr als das, was die asynchrone Wartefunktion tut. Wenn Sie einen Generatorfunktions-Wrapper benötigen , der zum Replizieren von asynchronem Warten verwendet werden kann, würde ich co.js überprüfen . Übrigens, die Funktion von co, ähnlich wie die asynchronen Wartefunktionen, gibt ein Versprechen zurück. Ehrlich gesagt ist die Browserkompatibilität zu diesem Zeitpunkt sowohl für Generatorfunktionen als auch für Async-Funktionen ungefähr gleich. Wenn Sie also nur die Async-Wait-Funktion wünschen, sollten Sie Async-Funktionen ohne co.js verwenden.

Die Browserunterstützung ist jetzt für Async-Funktionen (Stand 2017) in allen gängigen Browsern (Chrome, Safari und Edge) mit Ausnahme des IE ziemlich gut.

John
quelle
2
Ich mag diese Antwort
ycomp
1
wie weit wir gekommen sind :)
Derek
3
Dies ist eine großartige Antwort, aber für das ursprüngliche Posterproblem denke ich, dass alles, was es tut, das Problem um eine Ebene nach oben bewegt. Angenommen, er verwandelt doSomething in eine asynchrone Funktion mit einem Warten im Inneren. Diese Funktion gibt jetzt ein Versprechen zurück und ist asynchron, sodass er sich bei allen Aufrufen dieser Funktion erneut mit demselben Problem befassen muss.
dpwrussell
1
@dpwrussell das ist wahr, es gibt eine Menge asynchroner Funktionen und Versprechen in der Codebasis. Der beste Weg, um Versprechen zu lösen, indem man sich in alles einschleicht, besteht darin, synchrone Rückrufe zu schreiben. Es gibt keine Möglichkeit, einen asynchronen Wert synchron zurückzugeben, es sei denn, Sie tun etwas extrem Seltsames und Kontroverses wie dieses twitter.com/sebmarkbage/status/941214259505119232, was ich nicht tue empfehlen. Ich werde am Ende der Frage eine Bearbeitung hinzufügen, um die gestellte Frage vollständiger zu beantworten und nicht nur den Titel zu beantworten.
John
Es ist eine großartige Antwort +1 und alles, aber so wie es ist geschrieben, sehe ich nicht, dass dies weniger kompliziert ist als die Verwendung von Rückrufen.
Altimus Prime
47

Schauen Sie sich JQuery Promises an:

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

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

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

Refactor den Code:

    var dfd = new jQuery.Deferred ();


    Funktion callBack (Daten) {
       dfd.notify (Daten);
    }}

    // Async-Aufruf ausführen.
    myAsynchronousCall (param1, callBack);

    Funktion doSomething (Daten) {
     // Sachen mit Daten machen ...
    }}

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


Matt Taylor
quelle
3
+1 für diese Antwort ist dies richtig. Allerdings würde ich die Zeile aktualisieren mit dfd.notify(data)zudfd.resolve(data)
Jason
7
Ist dies ein Fall, in dem der Code die Illusion vermittelt, synchron zu sein, ohne tatsächlich NICHT asynchron zu sein?
Saurshaz
2
Versprechen sind IMO nur gut organisierte Rückrufe :) Wenn Sie einen asynchronen Aufruf in beispielsweise einer Objektinitialisierung benötigen, machen Versprechen einen kleinen Unterschied.
Webduvet
10
Versprechen sind nicht synchron.
Vans S
6

Es gibt eine nette Problemumgehung unter http://taskjs.org/

Es werden Generatoren verwendet, die für Javascript neu sind. Daher wird es derzeit von den meisten Browsern nicht implementiert. Ich habe es in Firefox getestet und für mich ist es eine gute Möglichkeit, die asynchrone Funktion zu verpacken.

Hier ist Beispielcode aus dem Projekt 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
quelle
3

Sie können asynchrones JavaScript in NodeJS erzwingen, damit es mit sync-rpc synchron ist .

Es wird Ihre Benutzeroberfläche definitiv einfrieren, daher bin ich immer noch ein Neinsager, wenn es darum geht, ob es möglich ist, die Verknüpfung zu verwenden, die Sie benötigen. Es ist nicht möglich, den One And Only Thread in JavaScript auszusetzen, selbst wenn Sie ihn mit NodeJS manchmal blockieren können. Keine Rückrufe, Ereignisse oder asynchronen Daten können verarbeitet werden, bis Ihr Versprechen erfüllt ist. Wenn Sie als Leser also keine unvermeidbare Situation wie das OP haben (oder in meinem Fall ein verherrlichtes Shell-Skript ohne Rückrufe, Ereignisse usw. schreiben), TUN SIE DAS NICHT!

Aber so können Sie das machen:

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

EINSCHRÄNKUNGEN:

Beides ist eine Folge der Art und Weise der sync-rpcImplementierung, nämlich durch Missbrauch require('child_process').spawnSync:

  1. Dies funktioniert im Browser nicht.
  2. Die Argumente für Ihre Funktion müssen serialisierbar sein. Ihre Argumente werden ein- und ausgeblendet JSON.stringify, sodass Funktionen und nicht aufzählbare Eigenschaften wie Prototypketten verloren gehen.
Meustrus
quelle
1

Sie können es auch in Rückrufe konvertieren.

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

function foo() {    
  var fooVariable;

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

  return fooVariable;
}

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

Was Sie wollen, ist jetzt tatsächlich möglich. Wenn Sie den asynchronen Code in einem Service Worker und den synchronen Code in einem Web Worker ausführen können, können Sie den Web Worker eine synchrone XHR an den Service Worker senden lassen, und während der Service Worker die asynchronen Aufgaben ausführt, den Web Worker Thread wird warten. Dies ist kein großartiger Ansatz, aber er könnte funktionieren.

Vielleicht sehen Sie diesen Namen.
quelle
-4

Die Idee, die Sie erreichen möchten, kann ermöglicht werden, wenn Sie die Anforderungen ein wenig anpassen

Der folgende Code ist möglich, wenn Ihre Laufzeit die ES6-Spezifikation unterstützt.

Weitere Informationen zu asynchronen Funktionen

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
quelle
4
Firefox gibt den Fehler aus : SyntaxError: await is only valid in async functions and async generators. Ganz zu schweigen davon, dass param1 nicht definiert ist (und nicht einmal verwendet wird).
Harvey