Wywołaj synchroniczną funkcję asynchronicznego Javascript

221

Po pierwsze, jest to bardzo szczególny przypadek, gdy celowo zrobimy to w niewłaściwy sposób, aby zmodernizować asynchroniczne wywołanie w bardzo synchroniczną bazę kodu, która ma wiele tysięcy linii, a czas nie pozwala obecnie na wprowadzenie zmian w „zrobić dobrze ”. Boli mnie każde włókno mojej istoty, ale rzeczywistość i ideały często się nie łączą. Wiem, że to do bani.

OK, to na marginesie, jak to zrobić, żebym mógł:

function doSomething() {

  var data;

  function callBack(d) {
    data = d;
  }

  myAsynchronousCall(param1, callBack);

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

Wszystkie przykłady (lub ich brak) wykorzystują biblioteki i / lub kompilatory, które nie są wykonalne dla tego rozwiązania. Potrzebuję konkretnego przykładu, jak to zablokować (np. NIE zostawiaj funkcji doSomething, dopóki nie zostanie wywołane wywołanie zwrotne) BEZ zamrażania interfejsu użytkownika. Jeśli coś takiego jest możliwe w JS.

Robert C. Barth
źródło
16
Po prostu nie można zablokować przeglądarki i czekać. Po prostu tego nie zrobią.
Pointy
2
javascript nie zgadza się z mechanizmami blokującymi w większości przeglądarek ... będziesz chciał utworzyć wywołanie zwrotne, które jest wywoływane, gdy połączenie asynchroniczne zakończy się, aby zwrócić dane
Nadir Muzaffar
8
Pytasz o sposób, aby powiedzieć przeglądarce „Wiem, że właśnie powiedziałem ci, abyś uruchomił poprzednią funkcję asynchronicznie, ale tak naprawdę nie miałem tego na myśli!”. Dlaczego miałbyś oczekiwać, że to będzie możliwe?
Wayne
2
Dzięki Dan za edycję. Nie byłem absolutnie niegrzeczny, ale twoje sformułowania są lepsze.
Robert C. Barth
2
@ RobertC.Barth Teraz jest to również możliwe dzięki JavaScript. Funkcje async oczekują, że funkcje nie zostały jeszcze ratyfikowane w standardzie, ale planowane jest wprowadzenie ich w ES2017. Zobacz moją odpowiedź poniżej, aby uzyskać więcej szczegółów.
John

Odpowiedzi:

135

„nie mów mi, jak powinienem to zrobić„ we właściwy sposób ”lub cokolwiek innego”

DOBRZE. ale naprawdę powinieneś to zrobić we właściwy sposób ... czy cokolwiek

„Potrzebuję konkretnego przykładu, jak sprawić, by blokował ... BEZ zamrażania interfejsu użytkownika. Jeśli takie rozwiązanie jest możliwe w JS”.

Nie, nie można zablokować działającego JavaScript bez blokowania interfejsu użytkownika.

Biorąc pod uwagę brak informacji, trudno jest zaproponować rozwiązanie, ale jedną z opcji może być włączenie funkcji wywoływania w celu sprawdzenia zmiennej globalnej, a następnie ustawienia wywołania zwrotnego datana wartość globalną.

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

Wszystko to zakłada, że ​​możesz modyfikować doSomething() . Nie wiem czy to jest w kartach.

Jeśli można to zmodyfikować, to nie wiem, dlaczego po prostu nie oddzwonisz, doSomething()aby zostać wywołanym z innego oddzwaniania, ale lepiej przestanę, zanim wpadnę w kłopoty. ;)


Co do cholery. Podałeś przykład, który sugeruje, że można to zrobić poprawnie, więc pokażę to rozwiązanie ...

function doSomething( func ) {

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

  myAsynchronousCall(param1, callBack);

}

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

Ponieważ twój przykład zawiera wywołanie zwrotne, które jest przekazywane do wywołania asynchronicznego, właściwym sposobem byłoby przekazanie funkcji do doSomething() wywołania z wywołania zwrotnego.

Oczywiście, jeśli to jedyna czynność wywołania zwrotnego, wystarczy przekazać funcbezpośrednio ...

myAsynchronousCall(param1, func);
1106925
źródło
22
Tak, wiem, jak to zrobić poprawnie, muszę wiedzieć, jak to zrobić / czy można to zrobić niepoprawnie z podanego powodu. Najważniejsze jest to, że nie chcę opuszczać doSomething (), dopóki myAsynchronousCall nie zakończy wywołania funkcji callback. Bleh, nie da się tego zrobić, jak podejrzewałam, po prostu potrzebowałem zgromadzonej mądrości Internetów, aby mnie poprzeć. Dziękuję Ci. :-)
Robert C. Barth
2
@ RobertC.Barth: Tak, twoje podejrzenia były niestety słuszne.
Czy to ja, czy działa tylko wersja „poprawnie wykonana”? Pytanie obejmowało wezwanie zwrotne, przed którym powinno być coś, co czeka na zakończenie asynchronicznego połączenia, którego ta pierwsza część tej odpowiedzi nie obejmuje ...
ravemir
@ravemir: Odpowiedź stwierdza, że nie można robić tego, co chce. To ważna część do zrozumienia. Innymi słowy, nie można wykonać asynchronicznego wywołania i zwrócić wartości bez blokowania interfejsu użytkownika. Tak więc pierwszym rozwiązaniem jest brzydki hack przy użyciu zmiennej globalnej i odpytywania, aby sprawdzić, czy zmienna ta została zmodyfikowana. Druga wersja jest poprawna.
1
@Leonardo: To tajemnicza funkcja wywołana w pytaniu. Zasadniczo reprezentuje wszystko, co uruchamia kod asynchronicznie i daje wynik, który należy otrzymać. Może to być prośba AJAX. Przekazujesz callbackfunkcję do myAsynchronousCallfunkcji, która wykonuje swoje asynchroniczne czynności i wywołuje wywołanie zwrotne po zakończeniu. Oto demo.
60

Funkcje asynchroniczne , funkcja w ES2017 , sprawiają, że kod asynchroniczny wygląda na zsynchronizowany przy użyciu obietnic (szczególnej formy kodu asynchronicznego) i awaitsłowa kluczowego. Zwróć też uwagę na przykłady kodu poniżej słowa kluczowego asyncprzed functionsłowem kluczowym, które oznacza funkcję asynchroniczną / oczekującą. Słowo awaitkluczowe nie będzie działać bez funkcji wstępnie ustawionej za pomocą asyncsłowa kluczowego. Ponieważ obecnie nie ma wyjątku od tego, co oznacza, że ​​nie będzie oczekiwał żaden poziom najwyższy (poziom najwyższy czeka, co oznacza oczekiwanie poza jakąkolwiek funkcją). Chociaż jest propozycja na najwyższym poziomieawait .

ES2017 został ratyfikowany (tj. Sfinalizowany) jako standard dla JavaScript w dniu 27 czerwca 2017 r. Async oczekuje może już działać w przeglądarce, ale jeśli nie, nadal możesz korzystać z tej funkcji za pomocą transpilatora javascript, takiego jak babel lub traceur . Chrome 55 ma pełne wsparcie funkcji asynchronicznych. Jeśli masz nowszą przeglądarkę, możesz wypróbować poniższy kod.

Zobacz tabelę zgodności kangax es2017 dla kompatybilności przeglądarki.

Oto przykładowa wywoływana funkcja asynchroniczna, doAsyncktóra wymaga trzech jednosekundowych pauz i drukuje różnicę czasu po każdej pauzie od czasu rozpoczęcia:

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

Po umieszczeniu słowa kluczowego oczekującego przed wartością przyrzeczenia (w tym przypadku wartością przyrzeczenia jest wartość zwrócona przez funkcję doSomethingAsync) słowo kluczowe oczekujące wstrzyma wykonanie wywołania funkcji, ale nie wstrzyma żadnych innych funkcji i będzie kontynuowane wykonywanie innego kodu, dopóki obietnica nie zostanie rozwiązana. Po spełnieniu obietnicy odwija ​​wartość obietnicy i możesz pomyśleć o wyrażeniu oczekiwania i obietnicy, które jest teraz zastępowane przez tę nieopakowaną wartość.

Tak więc, ponieważ czekanie tylko pauza czeka, a następnie rozpakowuje wartość przed wykonaniem reszty linii, możesz użyć jej do pętli i wywołań funkcji wewnętrznych, jak w poniższym przykładzie, który zbiera oczekiwane różnice czasowe w tablicy i drukuje tablicę.

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

Sama funkcja asynchroniczna zwraca obietnicę, dzięki czemu można jej użyć jako obietnicy z łańcuchem takim jak ja powyżej lub w innej funkcji oczekującej asynchronizacji.

Funkcja powyżej czekałaby na każdą odpowiedź przed wysłaniem kolejnego żądania, jeśli chcesz wysłać żądania jednocześnie, możesz użyć 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)
})

Jeśli obietnica prawdopodobnie zostanie odrzucona, możesz zawinąć ją w try catch lub pomiń try catch i pozwolić, aby błąd rozprzestrzenił się na wywołanie funkcji async / oczekuj. Należy uważać, aby nie pozostawić nieobsługiwanych błędów obietnicy, szczególnie w Node.js. Poniżej kilka przykładów, które pokazują, jak działają błędy.

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)

Jeśli pójdziesz tutaj , możesz zobaczyć gotowe propozycje przyszłych wersji ECMAScript.

Alternatywą dla tego, która może być używana tylko z ES2015 (ES6), jest użycie specjalnej funkcji, która otacza funkcję generatora. Funkcje generatora mają słowo kluczowe wydajności, które może być użyte do replikacji słowa kluczowego oczekującego z funkcją otaczającą. Słowo kluczowe wydajności i funkcja generatora są znacznie bardziej ogólne i mogą robić znacznie więcej niż tylko to, co robi funkcja asynchroniczna. Jeśli chcesz otoki funkcji generatora, której można użyć do replikacji asynchronizacji, czekam co.js . Nawiasem mówiąc, funkcja co podobnie jak async oczekuje na funkcje zwracające obietnicę. Szczerze mówiąc, w tym momencie kompatybilność przeglądarki jest prawie taka sama zarówno dla funkcji generatora, jak i funkcji asynchronicznych, więc jeśli chcesz, aby funkcja async oczekiwała funkcjonalności, powinieneś używać funkcji asynchronicznych bez co.js.

Obsługa przeglądarek jest teraz całkiem dobra dla funkcji Async (od 2017 r.) We wszystkich głównych przeglądarkach (Chrome, Safari i Edge) z wyjątkiem IE.

Jan
źródło
2
Podoba mi się ta odpowiedź
ycomp
1
jak daleko zaszliśmy :)
Derek
3
To świetna odpowiedź, ale myślę, że w przypadku problemu z oryginalnymi plakatami przesuwa problem o jeden poziom wyżej. Powiedzmy, że zmienia doSomething w funkcję asynchroniczną z oczekiwaniem w środku. Ta funkcja zwraca teraz obietnicę i jest asynchroniczna, więc będzie musiał ponownie rozwiązać ten sam problem przy każdym wywołaniu tej funkcji.
dpwrussell
1
@dpwrussell to prawda, w bazie kodu jest pełno funkcji asynchronicznych i obietnic. Najlepszym sposobem na rozwiązanie obietnic od wkradania się do wszystkiego jest po prostu zapisywanie synchronicznych wywołań zwrotnych, nie ma sposobu na synchroniczne zwracanie wartości asynchronicznej, chyba że zrobisz coś bardzo dziwnego i kontrowersyjnego, takiego jak ten twitter.com/sebmarkbage/status/941214259505119232, którego nie polecić. Dodam edycję na końcu pytania, aby pełniej odpowiedzieć na zadane pytanie, a nie tylko na tytuł.
Jan
To świetna odpowiedź +1 i wszystko inne, ale napisane w obecnym kształcie, nie widzę, aby było to mniej skomplikowane niż używanie wywołań zwrotnych.
Altimus Prime
47

Spójrz na obietnice JQuery:

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

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

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

Zmień kod:

    var dfd = new jQuery.Deferred ();


    funkcja callBack (dane) {
       dfd.notify (dane);
    }

    // wykonaj wywołanie asynchroniczne.
    myAsynchronousCall (param1, callBack);

    funkcja doSomething (data) {
     // rób rzeczy z danymi ...
    }

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


Matt Taylor
źródło
3
+1 dla tej odpowiedzi, to prawda. Chciałbym jednak zaktualizować linię dfd.notify(data)dodfd.resolve(data)
Jason
7
Czy to przypadek, w którym kod daje złudzenie, że jest synchroniczny, a tak naprawdę NIE jest asynchroniczny?
saurshaz
2
obietnice są IMO tylko dobrze zorganizowanymi wywołaniami zwrotnymi :) jeśli potrzebujesz asynchronicznego wywołania, powiedzmy, że inicjalizacja obiektu, to obietnice mają małą różnicę.
webduvet,
10
Obietnice nie są synchronizowane.
Vans S
6

Jest jedno miłe obejście na http://taskjs.org/

Korzysta z generatorów, które są nowością w javascript. Dlatego obecnie nie jest implementowany przez większość przeglądarek. Przetestowałem to w firefoxie i dla mnie to dobry sposób na zawinięcie funkcji asynchronicznej.

Oto przykładowy kod z projektu 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
źródło
3

Państwo może zmusić asynchroniczny JavaScript w NodeJS być synchroniczny z synchronizacją-RPC .

Zdecydowanie jednak zamrozi twój interfejs użytkownika, więc wciąż jestem niezdecydowany, jeśli chodzi o to, czy można skorzystać ze skrótu, którego potrzebujesz. Nie można zawiesić jedynego wątku w JavaScript, nawet jeśli NodeJS pozwala czasem go zablokować. Żadne wywołania zwrotne, zdarzenia, wszystko asynchroniczne w ogóle nie będzie w stanie przetworzyć, dopóki obietnica się nie spełni. Tak więc, chyba że czytelnik ma nieuniknioną sytuację, taką jak OP (lub, w moim przypadku, pisze chwalebny skrypt powłoki bez wywołań zwrotnych, zdarzeń itp.), NIE Rób tego!

Ale oto jak możesz to zrobić:

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

OGRANICZENIA:

Oba są konsekwencją sposobu sync-rpcimplementacji, który polega na nadużyciach require('child_process').spawnSync:

  1. To nie zadziała w przeglądarce.
  2. Argumenty funkcji muszą być możliwe do serializacji. Twoje argumenty JSON.stringifybędą wchodzić i wychodzić , więc funkcje i właściwości niepoliczalne, takie jak łańcuchy prototypów, zostaną utracone.
meustrus
źródło
1

Możesz także przekształcić go w wywołania zwrotne.

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

function foo() {    
  var fooVariable;

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

  return fooVariable;
}

var temp = foo();  
console.log(temp);
Nikhil
źródło
0

To, czego chcesz, jest teraz możliwe. Jeśli możesz uruchomić kod asynchroniczny w pracowniku usługowym, a kod synchroniczny w pracowniku internetowym, możesz poprosić pracownika sieci web o wysłanie synchronicznego XHR do pracownika usługowego, a gdy pracownik usługowy wykonuje czynności asynchroniczne, pracownik sieciowy wątek będzie czekać. To nie jest świetne podejście, ale może zadziałać.

Być może widzisz to imię.
źródło
-4

Pomysł, który masz nadzieję osiągnąć, można zrealizować, jeśli nieco poprawisz wymagania

Poniższy kod jest możliwy, jeśli środowisko wykonawcze obsługuje specyfikację ES6.

Więcej informacji o funkcjach asynchronicznych

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
źródło
4
Firefox daje błąd: SyntaxError: await is only valid in async functions and async generators. Nie wspominając o tym, że param1 nie jest zdefiniowany (i nawet nie używany).
Harvey