Jak zwrócić odpowiedź z połączenia asynchronicznego?

5506

Mam funkcję, fooktóra powoduje żądanie Ajax. Jak mogę zwrócić odpowiedź foo?

Próbowałem successzwrócić wartość z wywołania zwrotnego, a także przypisać odpowiedź do zmiennej lokalnej wewnątrz funkcji i zwrócić tę, ale żaden z tych sposobów nie zwrócił odpowiedzi.

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
źródło

Odpowiedzi:

5700

→ Aby uzyskać bardziej ogólne wyjaśnienie zachowania asynchronicznego z różnymi przykładami, zobacz Dlaczego moja zmienna pozostaje niezmieniona po zmodyfikowaniu jej w funkcji? - Odwołanie do kodu asynchronicznego

→ Jeśli już rozumiesz problem, przejdź do możliwych rozwiązań poniżej.

Problem

W Ajax oznacza asynchroniczny . Oznacza to, że wysłanie żądania (a raczej otrzymanie odpowiedzi) jest usuwane z normalnego przepływu wykonania. W twoim przykładzie natychmiast zwraca, a następna instrukcja, jest wykonywana przed funkcją, którą przekazałeś jako wywołanie zwrotne.$.ajaxreturn result;success

Oto analogia, która, miejmy nadzieję, czyni różnicę między przepływem synchronicznym i asynchronicznym:

Synchroniczny

Wyobraź sobie, że dzwonisz do przyjaciela i prosisz go, aby coś dla ciebie poszukał. Chociaż może to chwilę potrwać, czekasz przez telefon i wpatrywasz się w kosmos, aż znajomy udzieli odpowiedzi, której potrzebujesz.

To samo dzieje się, gdy wykonujesz wywołanie funkcji zawierające „normalny” kod:

function findItem() {
    var item;
    while(item_not_found) {
        // search
    }
    return item;
}

var item = findItem();

// Do something with item
doSomethingElse();

Mimo że wykonanie findItemmoże potrwać długo, każdy następny kod var item = findItem();musi poczekać, aż funkcja zwróci wynik.

Asynchroniczny

Z tego samego powodu ponownie dzwonisz do swojego przyjaciela. Ale tym razem powiesz mu, że się spieszysz i powinien oddzwonić na twój telefon komórkowy. Rozłączasz się, wychodzisz z domu i robisz wszystko, co planujesz. Gdy znajomy oddzwoni, masz do czynienia z informacjami, które ci przekazał.

Dokładnie tak się dzieje, kiedy wykonujesz żądanie Ajax.

findItem(function(item) {
    // Do something with item
});
doSomethingElse();

Zamiast czekać na odpowiedź, wykonywanie jest kontynuowane natychmiast, a instrukcja jest wykonywana po wywołaniu Ajax. Aby ostatecznie uzyskać odpowiedź, udostępniasz funkcję, która ma być wywoływana po odebraniu odpowiedzi, oddzwanianie (zauważ coś? Oddzwonić ?). Wszelkie instrukcje przychodzące po tym wywołaniu są wykonywane przed wywołaniem wywołania zwrotnego.


Rozwiązania)

Uwzględnij asynchroniczną naturę JavaScript! Chociaż niektóre operacje asynchroniczne zapewniają synchroniczne odpowiedniki (podobnie jak „Ajax”), generalnie odradza się ich używanie, szczególnie w kontekście przeglądarki.

Dlaczego tak źle pytasz?

JavaScript działa w wątku interfejsu użytkownika przeglądarki, a każdy długotrwały proces zablokuje interfejs użytkownika, co spowoduje, że przestanie on odpowiadać. Dodatkowo istnieje górny limit czasu wykonywania dla JavaScript, a przeglądarka zapyta użytkownika, czy kontynuować wykonywanie, czy nie.

Wszystko to jest naprawdę złe doświadczenie użytkownika. Użytkownik nie będzie w stanie stwierdzić, czy wszystko działa dobrze, czy nie. Ponadto efekt będzie gorszy dla użytkowników z wolnym połączeniem.

Poniżej przyjrzymy się trzem różnym rozwiązaniom, które wszystkie budują jeden na drugim:

  • async/awaitObiecuje z (ES2017 +, dostępny w starszych przeglądarkach, jeśli używasz transpilatora lub regeneratora)
  • Callbacki (popularne w węźle)
  • then()Obiecuje za pomocą (ES2015 +, dostępny w starszych przeglądarkach, jeśli korzystasz z jednej z wielu bibliotek obietnic)

Wszystkie trzy są dostępne w obecnych przeglądarkach, a węzeł 7+.


ES2017 +: Obiecuje z async/await

Wersja ECMAScript wydana w 2017 r. Wprowadziła obsługę funkcji asynchronicznych na poziomie składni . Za pomocą asynci awaitmożesz pisać asynchronicznie w „stylu synchronicznym”. Kod jest nadal asynchroniczny, ale łatwiej go odczytać / zrozumieć.

async/awaitopiera się na obietnicach: asyncfunkcja zawsze zwraca obietnicę. await„rozpakowuje” obietnicę i albo powoduje, że wartość przyrzeczenia została rozwiązana, albo zgłasza błąd, jeśli obietnica została odrzucona.

Ważne: Możesz używać tylko awaitwewnątrz asyncfunkcji. W tej chwili najwyższy poziom awaitnie jest jeszcze obsługiwany, więc może być konieczne utworzenie asynchronicznego IIFE ( Natychmiastowe wywołanie wyrażenia funkcji ), aby rozpocząć asynckontekst.

Możesz przeczytać więcej o MDN asynci awaitna jego temat.

Oto przykład, który opiera się na opóźnieniu powyżej:

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

Aktualne przeglądarek i węzłów wersje obsługują async/await. Możesz również obsługiwać starsze środowiska, przekształcając swój kod do ES5 za pomocą regeneratora (lub narzędzi korzystających z regeneratora, takich jak Babel ).


Niech funkcje akceptują oddzwanianie

Oddzwonienie to po prostu funkcja przekazana do innej funkcji. Ta inna funkcja może wywołać funkcję przekazaną, ilekroć jest gotowa. W kontekście procesu asynchronicznego wywołanie zwrotne będzie wywoływane za każdym razem, gdy proces asynchroniczny zostanie wykonany. Zwykle wynik jest przekazywany do wywołania zwrotnego.

W przykładzie pytania możesz foozaakceptować oddzwonienie i użyć go jako successoddzwonienia. Więc to

var result = foo();
// Code that depends on 'result'

staje się

foo(function(result) {
    // Code that depends on 'result'
});

Tutaj zdefiniowaliśmy funkcję „inline”, ale możesz przekazać dowolne odwołanie do funkcji:

function myCallback(result) {
    // Code that depends on 'result'
}

foo(myCallback);

foo sam jest zdefiniowany w następujący sposób:

function foo(callback) {
    $.ajax({
        // ...
        success: callback
    });
}

callbackbędzie odnosić się do funkcji, którą przekazujemy, fookiedy ją wywołujemy i po prostu ją przekazujemy success. To znaczy, gdy żądanie Ajax zakończy się powodzeniem, $.ajaxzadzwoni callbacki przekaże odpowiedź do wywołania zwrotnego (do którego można się odwołać result, ponieważ w ten sposób zdefiniowaliśmy wywołanie zwrotne).

Możesz również przetworzyć odpowiedź przed przekazaniem jej do wywołania zwrotnego:

function foo(callback) {
    $.ajax({
        // ...
        success: function(response) {
            // For example, filter the response
            callback(filtered_response);
        }
    });
}

Łatwiej jest pisać kod za pomocą wywołań zwrotnych, niż może się wydawać. W końcu JavaScript w przeglądarce jest silnie sterowany zdarzeniami (zdarzenia DOM). Otrzymanie odpowiedzi Ajax to nic innego jak wydarzenie.
Problemy mogą pojawić się, gdy trzeba pracować z kodem innej firmy, ale większość problemów można rozwiązać, po prostu analizując przepływ aplikacji.


ES2015 +: Obiecuje za pomocą then ()

Obietnica API jest nowa funkcja ECMAScript 6 (ES2015), ale ma dobrą obsługę przeglądarki już. Istnieje również wiele bibliotek, które implementują standardowy interfejs API Promises i zapewniają dodatkowe metody ułatwiające korzystanie z funkcji asynchronicznych i ich komponowanie (np. Bluebird ).

Obietnice są pojemnikami na przyszłe wartości. Kiedy obietnica otrzyma wartość (zostanie rozwiązana ) lub gdy zostanie anulowana ( odrzucona ), powiadomi wszystkich swoich „słuchaczy”, którzy chcą uzyskać dostęp do tej wartości.

Zaletą zwykłych wywołań zwrotnych jest to, że pozwalają one oddzielić kod i są łatwiejsze do skomponowania.

Oto prosty przykład użycia obietnicy:

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

W przypadku naszego połączenia Ajax możemy użyć takich obietnic:

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

Opisanie wszystkich korzyści, które obiecują, wykracza poza zakres tej odpowiedzi, ale jeśli napiszesz nowy kod, powinieneś poważnie je rozważyć. Zapewniają doskonałą abstrakcję i separację kodu.

Więcej informacji o obietnicach: skały HTML5 - obietnice JavaScript

Uwaga dodatkowa: odroczone obiekty jQuery

Odroczone obiekty są niestandardową implementacją obietnic jQuery (przed standaryzacją interfejsu Promise API). Zachowują się prawie jak obietnice, ale ujawniają nieco inny interfejs API.

Każda metoda Ajaks jQuery zwraca już „odroczony obiekt” (faktycznie obietnica odroczonego obiektu), który możesz po prostu zwrócić z funkcji:

function ajax() {
    return $.ajax(...);
}

ajax().done(function(result) {
    // Code depending on result
}).fail(function() {
    // An error occurred
});

Uwaga dodatkowa: Obietnica gotchas

Pamiętaj, że obietnice i odroczone obiekty są tylko pojemnikami na przyszłą wartość, a nie samą wartością. Załóżmy na przykład, że masz:

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
}

Ten kod źle rozumie powyższe problemy z asynchronią. W szczególności $.ajax()nie zamraża kodu podczas sprawdzania strony „/ password” na serwerze - wysyła żądanie do serwera i podczas oczekiwania natychmiast zwraca obiekt jQuery Ajax Deferred, a nie odpowiedź z serwera. Oznacza to, że ifinstrukcja będzie zawsze pobierać ten obiekt odroczony, traktować go jak truei postępować tak, jakby użytkownik był zalogowany. Nie dobrze.

Ale poprawka jest łatwa:

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

Niezalecane: Synchroniczne wywołania „Ajax”

Jak wspomniałem, niektóre (!) Operacje asynchroniczne mają synchroniczne odpowiedniki. Nie opowiadam się za ich użyciem, ale ze względu na kompletność, oto jak możesz wykonać połączenie synchroniczne:

Bez jQuery

Jeśli bezpośrednio używasz XMLHTTPRequestobiektu, przekaż falsejako trzeci argument do .open.

jQuery

Jeśli używasz jQuery , możesz ustawić asyncopcję na false. Zauważ, że ta opcja jest przestarzała od jQuery 1.8. Następnie możesz albo użyć successwywołania zwrotnego, albo uzyskać dostęp do responseTextwłaściwości obiektu jqXHR :

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

Jeśli używasz innej metody Ajax jQuery, takiej jak $.get, $.getJSONitp., Musisz ją zmienić na $.ajax(ponieważ możesz przekazać tylko parametry konfiguracyjne $.ajax).

Heads-up! Nie można wykonać synchronicznego żądania JSONP . JSONP ze swej natury jest zawsze asynchroniczny (jeszcze jeden powód, aby nawet nie rozważać tej opcji).

Felix Kling
źródło
74
@Pommy: Jeśli chcesz korzystać z jQuery, musisz go dołączyć. Zapoznaj się z docs.jquery.com/Tutorials:Getting_Started_with_jQuery .
Felix Kling
11
W Solution 1, sub jQuery, nie mogłem zrozumieć tej linii: If you use any other jQuery AJAX method, such as $.get, $.getJSON, etc., you have them to $.ajax.(Tak, zdaję sobie sprawę, że mój nick jest odrobinę ironiczny w tym przypadku)
cssyphus
32
@ gibberish: Mmmh, nie wiem, jak można to wyjaśnić. Czy widzisz, jak foojest wywoływana, a funkcja jest do niej przekazywana ( foo(function(result) {....});)? resultjest używany wewnątrz tej funkcji i jest odpowiedzią na żądanie Ajax. Aby odwołać się do tej funkcji, pierwszy parametr foo jest wywoływany callbacki przypisywany successzamiast funkcji anonimowej. Więc $.ajaxzadzwoni, callbackgdy żądanie zakończy się powodzeniem. Próbowałem to trochę wyjaśnić.
Felix Kling
43
Czat dla tego pytania jest martwy, więc nie jestem pewien, gdzie zaproponować przedstawione zmiany, ale proponuję: 1) Zmień część synchroniczną na proste omówienie, dlaczego jest złe, bez kodu przykładowego, jak to zrobić. 2) Usuń / połącz przykłady wywołań zwrotnych, aby pokazać tylko bardziej elastyczne podejście odroczone, które moim zdaniem może być nieco łatwiejsze dla osób uczących się Javascript.
Chris Moschini 16.04.13
14
@Jessi: Myślę, że źle zrozumiałeś tę część odpowiedzi. Nie możesz użyć, $.getJSONjeśli chcesz, aby żądanie Ajax było synchroniczne. Jednak nie powinieneś chcieć, aby żądanie było synchroniczne, więc to nie ma zastosowania. Powinieneś używać wywołań zwrotnych lub obietnic do obsługi odpowiedzi, jak wyjaśniono to wcześniej w odpowiedzi.
Felix Kling
1071

Jeśli nie używasz jQuery w kodzie, ta odpowiedź jest dla Ciebie

Twój kod powinien być mniej więcej taki:

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 wykonał świetną robotę, pisząc odpowiedź dla osób używających jQuery dla AJAX, postanowiłem zapewnić alternatywę dla ludzi, którzy nie są.

( Uwaga: dla osób korzystających z nowego fetchAPI, Angulara lub obietnic dodałem inną odpowiedź poniżej )


Co masz do czynienia?

To jest krótkie podsumowanie „Wyjaśnienia problemu” z drugiej odpowiedzi, jeśli nie jesteś pewien po przeczytaniu tego, przeczytaj to.

W AJAX oznacza asynchroniczny . Oznacza to, że wysłanie żądania (a raczej otrzymanie odpowiedzi) jest usuwane z normalnego przepływu wykonania. W twoim przykładzie natychmiast zwraca, a następna instrukcja, jest wykonywana przed funkcją, którą przekazałeś jako wywołanie zwrotne..sendreturn result;success

Oznacza to, że gdy wracasz, zdefiniowany przez ciebie detektor jeszcze się nie uruchomił, co oznacza, że ​​zwracana wartość nie została zdefiniowana.

Oto prosta analogia

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

(Skrzypce)

Wartość azwracana jest, undefinedponieważ a=5część nie została jeszcze wykonana. AJAX działa w ten sposób, zwracasz wartość, zanim serwer będzie miał szansę powiedzieć przeglądarce, co to jest.

Jednym z możliwych rozwiązań tego problemu jest ponowne aktywne kodowanie , informujące program, co ma robić po zakończeniu obliczeń.

function onComplete(a){ // When the code completes, do this
    alert(a);
}

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

To się nazywa CPS . Zasadniczo przekazujemy getFiveakcję do wykonania po jej zakończeniu, mówimy naszemu kodowi, jak zareagować po zakończeniu zdarzenia (jak nasze wywołanie AJAX lub w tym przypadku limit czasu).

Wykorzystanie byłoby:

getFive(onComplete);

Co powinno ostrzec „5” na ekranie. (Skrzypce) .

Możliwe rozwiązania

Istnieją zasadniczo dwa sposoby rozwiązania tego problemu:

  1. Zsynchronizuj połączenie AJAX (nazwijmy to SJAX).
  2. Zmień strukturę kodu, aby działał poprawnie z wywołaniami zwrotnymi.

1. Synchroniczny AJAX - nie rób tego !!

Jeśli chodzi o synchroniczny AJAX, nie rób tego! Odpowiedź Felixa podnosi kilka przekonujących argumentów na temat tego, dlaczego jest to zły pomysł. Podsumowując, zawiesi przeglądarkę użytkownika, dopóki serwer nie zwróci odpowiedzi i spowoduje bardzo złe wrażenia użytkownika. Oto kolejne krótkie podsumowanie zaczerpnięte z MDN na temat tego, dlaczego:

XMLHttpRequest obsługuje zarówno synchroniczną, jak i asynchroniczną komunikację. Ogólnie jednak ze względów wydajnościowych preferowane powinny być żądania asynchroniczne niż żądania synchroniczne.

Krótko mówiąc, żądania synchroniczne blokują wykonywanie kodu ... ... może to powodować poważne problemy ...

Jeśli mają to zrobić, można przekazać flagi: Oto jak to zrobić:

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. Kod restrukturyzacji

Pozwól swojej funkcji zaakceptować oddzwonienie. W tym przykładzie foomożna wprowadzić kod, aby akceptować oddzwonienie. Powiemy naszemu kodowi, jak zareagować po foozakończeniu.

Więc:

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

Staje się:

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

Przekazaliśmy tutaj funkcję anonimową, ale równie łatwo moglibyśmy przekazać odwołanie do istniejącej funkcji, dzięki czemu wygląda ona następująco:

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

Aby uzyskać więcej informacji o tym, jak tego rodzaju projektowanie połączeń zwrotnych jest wykonywane, sprawdź odpowiedź Felixa.

Teraz zdefiniujmy sam foo, aby działał odpowiednio

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

(skrzypce)

Teraz sprawiliśmy, że nasza funkcja foo akceptuje działanie, które zostanie uruchomione, gdy AJAX zakończy się pomyślnie, możemy to rozszerzyć, sprawdzając, czy status odpowiedzi nie ma wartości 200 i działając odpowiednio (utwórz moduł obsługi błędów i tym podobne). Skutecznie rozwiązujemy nasz problem.

Jeśli nadal masz trudności ze zrozumieniem tego, przeczytaj AJAX przewodnik dla początkujących w MDN.

Benjamin Gruenbaum
źródło
20
„żądania synchroniczne blokują wykonywanie kodu i mogą wyciekać pamięć i zdarzenia” w jaki sposób może wyciekać pamięć synchroniczna?
Matthew G
10
@MatthewG W tym pytaniu dodałem nagrodę , zobaczę, co mogę wyłowić. W międzyczasie usuwam cytat z odpowiedzi.
Benjamin Gruenbaum,
17
Tylko dla odniesienia, XHR 2 pozwala nam korzystać z programu onloadobsługi, który uruchamia się tylko wtedy, gdy readyStatejest 4. Oczywiście nie jest obsługiwany w IE8. (iirc, może wymagać potwierdzenia.)
Florian Margaine
9
Wyjaśnienie, jak przekazać anonimową funkcję jako wywołanie zwrotne, jest prawidłowe, ale wprowadza w błąd. Przykładowy var bar = foo (); prosi o zdefiniowanie zmiennej, podczas gdy sugerowane przez ciebie foo (funkim () {}); nie definiuje paska
Robbie Averill
396

XMLHttpRequest 2 (przede wszystkim przeczytaj odpowiedzi Benjamina Gruenbauma i Felixa Klinga )

Jeśli nie używasz jQuery i chcesz mieć ładny krótki XMLHttpRequest 2, który działa w nowoczesnych przeglądarkach, a także w przeglądarkach mobilnych, sugeruję użyć go w ten sposób:

function ajax(a, b, c){ // URL, callback, just a placeholder
  c = new XMLHttpRequest;
  c.open('GET', a);
  c.onload = b;
  c.send()
}

Jak widzisz:

  1. Jest krótszy niż wszystkie inne wymienione funkcje.
  2. Oddzwanianie jest ustawiane bezpośrednio (więc bez dodatkowych niepotrzebnych zamknięć).
  3. Wykorzystuje nowy onload (więc nie musisz sprawdzać statusu readystate &&)
  4. Są też inne sytuacje, których nie pamiętam, które sprawiają, że XMLHttpRequest 1 jest denerwujący.

Istnieją dwa sposoby uzyskania odpowiedzi na to wywołanie Ajax (trzy przy użyciu nazwy zmiennej XMLHttpRequest):

Najprostszy:

this.response

Lub jeśli z jakiegoś powodu jesteś bind()oddzwanianiem do klasy:

e.target.response

Przykład:

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

Lub (powyższe jest lepsze, anonimowe funkcje zawsze stanowią problem):

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

Nic prostszego.

Teraz niektórzy ludzie zapewne powiedzą, że lepiej jest użyć onreadystatechange lub nawet nazwy zmiennej XMLHttpRequest. To jest źle.

Sprawdź zaawansowane funkcje XMLHttpRequest

Obsługuje wszystkie * nowoczesne przeglądarki. I mogę potwierdzić, ponieważ używam tego podejścia, ponieważ istnieje XMLHttpRequest 2. Nigdy nie miałem żadnego problemu we wszystkich przeglądarkach, z których korzystam.

onreadystatechange jest przydatny tylko wtedy, gdy chcesz uzyskać nagłówki w stanie 2.

Używanie XMLHttpRequestnazwy zmiennej to kolejny duży błąd, ponieważ musisz wykonać wywołanie zwrotne wewnątrz zamknięć onload / oreadystatechange, w przeciwnym razie ją utracisz.


Teraz, jeśli chcesz czegoś bardziej złożonego za pomocą post i FormData, możesz łatwo rozszerzyć tę funkcję:

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

Znowu ... jest to bardzo krótka funkcja, ale pobiera i wysyła.

Przykłady użycia:

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

Lub przekaż pełny element form ( document.getElementsByTagName('form')[0]):

var fd = new FormData(form);
x(url, callback, 'post', fd);

Lub ustaw wartości niestandardowe:

var fd = new FormData();
fd.append('key', 'val')
x(url, callback, 'post', fd);

Jak widać, nie wdrożyłem synchronizacji ... to zła rzecz.

Powiedziawszy to ... dlaczego nie zrobić tego w prosty sposób?


Jak wspomniano w komentarzu, użycie error && synchronous całkowicie łamie sens odpowiedzi. Jaka jest dobra krótka droga do właściwego korzystania z Ajax?

Obsługa błędów

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

W powyższym skrypcie masz moduł obsługi błędów, który jest statycznie zdefiniowany, aby nie wpływał na działanie funkcji. Program obsługi błędów może być również używany do innych funkcji.

Ale aby naprawdę wyjść z błędu, jedynym sposobem jest napisanie niewłaściwego adresu URL, w którym to przypadku każda przeglądarka zgłasza błąd.

Procedury obsługi błędów mogą być przydatne, jeśli ustawisz niestandardowe nagłówki, ustaw właściwość responseType na bufor tablicy obiektów blob lub cokolwiek ...

Nawet jeśli podasz metodę „POSTAPAPAP” jako metodę, nie spowoduje to błędu.

Nawet jeśli podasz „fdggdgilfdghfldj” jako dane formalne, nie spowoduje to błędu.

W pierwszym przypadku błąd znajduje się wewnątrz displayAjax()pod this.statusTextjako Method not Allowed.

W drugim przypadku po prostu działa. Musisz sprawdzić po stronie serwera, czy przekazałeś odpowiednie dane postu.

niedozwolone w wielu domenach automatycznie generuje błąd.

W odpowiedzi na błąd nie ma kodów błędów.

Jest tylko to, this.typeco jest ustawione na błąd.

Po co dodawać moduł obsługi błędów, jeśli całkowicie nie ma się kontroli nad błędami? Większość błędów jest zwracanych w tym funkcji zwrotnej displayAjax().

Więc: Nie ma potrzeby sprawdzania błędów, jeśli możesz poprawnie skopiować i wkleić adres URL. ;)

PS: Jako pierwszy test napisałem x ('x', displayAjax) ... i całkowicie otrzymałem odpowiedź ... ??? Sprawdziłem więc folder, w którym znajduje się HTML, i znalazłem plik o nazwie „x.xml”. Więc nawet jeśli zapomnisz rozszerzenia swojego pliku XMLHttpRequest 2, ZNAJDUJE SIĘ . LOL'd


Czytaj plik synchroniczny

Nie rób tego

Jeśli chcesz zablokować przeglądarkę na jakiś czas, załaduj ładny duży .txtplik synchroniczny.

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

Teraz możesz zrobić

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

Nie ma innego sposobu na zrobienie tego w sposób niesynchroniczny. (Tak, z pętlą setTimeout ... ale poważnie?)

Kolejną kwestią jest ... jeśli pracujesz z interfejsami API lub tylko plikami własnej listy lub czymkolwiek, zawsze używasz różnych funkcji dla każdego żądania ...

Tylko jeśli masz stronę, na której zawsze ładujesz ten sam kod XML / JSON lub cokolwiek, czego potrzebujesz tylko jedną funkcję. W takim przypadku zmodyfikuj trochę funkcję Ajax i zastąp b funkcją specjalną.


Powyższe funkcje są do podstawowego użytku.

Jeśli chcesz PRZEDŁUŻYĆ funkcję ...

Tak, możesz.

Korzystam z wielu interfejsów API, a jedną z pierwszych funkcji, które integruję na każdej stronie HTML, jest pierwsza funkcja Ajax w tej odpowiedzi, tylko z GET ...

Ale możesz zrobić wiele rzeczy z XMLHttpRequest 2:

Zrobiłem menedżera pobierania (używając zakresów po obu stronach z CV, lidera plików, systemu plików), różnych konwerterów resizera obrazu za pomocą płótna, wypełniania internetowych baz danych SQL obrazami base64 i wiele więcej ... Ale w takich przypadkach powinieneś utworzyć funkcję tylko do tego cel ... czasami potrzebujesz obiektu blob, buforów macierzy, możesz ustawić nagłówki, przesłonić typ MIME i jest o wiele więcej ...

Ale pytanie tutaj brzmi: jak zwrócić odpowiedź Ajax ... (dodałem prosty sposób).

cocco
źródło
15
Chociaż ta odpowiedź jest fajna (wszyscy uwielbiamy XHR2, a publikowanie danych plików i danych wieloczęściowych jest całkowicie niesamowite) - pokazuje to cukier syntaktyczny do publikowania XHR za pomocą JavaScript - możesz chcieć umieścić to w blogu (chciałbym) lub nawet w bibliotece (nie jestem pewien co do nazwy x, ajaxlub xhrmoże być ładniejszy :)). Nie widzę, w jaki sposób odnosi się do zwracania odpowiedzi z połączenia AJAX. (ktoś nadal może to zrobić var res = x("url")i nie rozumie, dlaczego to nie działa;)). Na marginesie - byłoby fajnie, gdybyś wrócił cz tej metody, aby użytkownicy mogli się przyczepić erroritp.
Benjamin Gruenbaum,
25
2.ajax is meant to be async.. so NO var res=x('url')..To jest sedno tego pytania i odpowiedzi :)
Benjamin Gruenbaum,
3
dlaczego w funkcjach jest parametr „c”, jeśli w pierwszym wierszu zastępujesz jakąkolwiek wartość, którą miał? czy coś mi brakuje?
Brian H.,
2
Możesz użyć parametrów jako symbolu zastępczego, aby uniknąć wielokrotnego pisania „var”
cocco
11
@cocco Więc napisałeś mylący, nieczytelny kod w odpowiedzi SO , aby zaoszczędzić kilka naciśnięć klawiszy? Proszę nie rób tego.
kamień
316

Jeśli korzystasz z obietnic, ta odpowiedź jest dla Ciebie.

Oznacza to AngularJS, jQuery (z odroczeniem), zastępowanie natywnego XHR (pobieranie), EmberJS, zapisywanie BackboneJS lub dowolną bibliotekę węzłów, która zwraca obietnice.

Twój kod powinien być mniej więcej taki:

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 wykonał świetną robotę, pisząc odpowiedź dla osób używających jQuery z wywołaniami zwrotnymi dla AJAX. Mam odpowiedź dla natywnego XHR. Ta odpowiedź dotyczy ogólnego użycia obietnic na interfejsie użytkownika lub na zapleczu.


Podstawowy problem

Model współbieżności JavaScript w przeglądarce i na serwerze z NodeJS / io.js jest asynchroniczny i reaktywny .

Za każdym razem, gdy wywołujesz metodę, która zwraca obietnicę, procedury thenobsługi są zawsze wykonywane asynchronicznie - to znaczy po kodzie poniżej nich, który nie jest w .thenmodule obsługi.

Znaczy to, że wracasz datado thenobsługi nie masz zdefiniowanego jeszcze nie wykonać. To z kolei oznacza, że ​​zwracana wartość nie została ustawiona na prawidłową wartość w czasie.

Oto prosta analogia do problemu:

    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

Wartość datawynosi, undefinedponieważ data = 5część nie została jeszcze wykonana. Prawdopodobnie wykona się za sekundę, ale do tego czasu nie ma znaczenia dla zwracanej wartości.

Ponieważ operacja jeszcze się nie wydarzyła (AJAX, połączenie z serwerem, IO, timer), zwracasz wartość, zanim żądanie dostanie szansę, by powiedzieć Twojemu kodowi, jaka jest ta wartość.

Jednym z możliwych rozwiązań tego problemu jest ponowne aktywne kodowanie , informujące program, co ma robić po zakończeniu obliczeń. Obietnice aktywnie to umożliwiają, ponieważ mają charakter czasowy (wrażliwy na czas).

Szybkie podsumowanie obietnic

Obietnica jest wartością w czasie . Obietnice mają swój stan, zaczynają się jako oczekujące bez wartości i mogą zostać rozliczone na:

  • spełnione, co oznacza, że ​​obliczenia zostały zakończone pomyślnie.
  • odrzucono, co oznacza, że ​​obliczenia nie powiodły się.

Obietnica może zmienić stan tylko raz, po czym zawsze pozostanie w tym samym stanie na zawsze. Możesz dołączyć programy thenobsługi do obietnic, aby wydobyć ich wartość i obsłużyć błędy. thenteleskopowe umożliwiają łączenia rozmów. Obietnice są tworzone przy użyciu interfejsów API, które je zwracają . Na przykład, bardziej nowoczesna zamiana AJAX fetchlub $.getobietnica zwrotu jQuery .

Kiedy zadzwonić .thenna obietnicy i powrotnej coś z tym - mamy obietnicę dla przetworzonego wartości . Jeśli zwrócimy kolejną obietnicę, otrzymamy niesamowite rzeczy, ale trzymajmy nasze konie.

Z obietnicami

Zobaczmy, jak możemy rozwiązać powyższy problem za pomocą obietnic. Po pierwsze, pokażmy nasze zrozumienie stanów obietnicy z góry, używając konstruktora Promise do utworzenia funkcji opóźnienia:

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

Teraz, po przekonwertowaniu setTimeout do korzystania z obietnic, możemy użyć, thenaby liczyć:

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

Zasadniczo, zamiast wrócić do wartości , której nie możemy zrobić, ponieważ model współbieżności - wracamy do otoki dla wartości, które możemy rozpakować z then. To jest jak pudełko, które można otworzyć then.

Stosuję to

To samo dotyczy Twojego oryginalnego wywołania API, możesz:

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

Więc to działa równie dobrze. Dowiedzieliśmy się, że nie możemy zwracać wartości z już asynchronicznych wywołań, ale możemy używać obietnic i łączyć je w celu przetwarzania. Wiemy już, jak zwrócić odpowiedź z połączenia asynchronicznego.

ES2015 (ES6)

ES6 wprowadza generatory, które są funkcjami, które mogą powrócić na środku, a następnie wznowić od momentu, w którym były. Jest to zwykle przydatne w przypadku sekwencji, na przykład:

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

Jest funkcją, która zwraca iterator nad sekwencją, 1,2,3,3,3,3,....którą można iterować. Chociaż jest to interesujące samo w sobie i otwiera wiele możliwości, jest jeden szczególnie interesujący przypadek.

Jeśli tworzona przez nas sekwencja jest sekwencją akcji, a nie liczb - możemy wstrzymać funkcję, ilekroć akcja zostanie wykonana, i poczekać na nią, zanim wznowimy funkcję. Zamiast sekwencji liczb potrzebujemy sekwencji przyszłych wartości - to znaczy: obietnic.

Ta nieco trudna, ale bardzo skuteczna sztuczka pozwala nam pisać kod asynchroniczny w sposób synchroniczny. Jest kilku „biegaczy”, którzy robią to za Ciebie, napisanie jednego to kilka krótkich linii kodu, ale wykracza poza zakres tej odpowiedzi. Będę tu używał Bluebird Promise.coroutine, ale są też inne opakowania, takie jak colub 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
});

Ta metoda sama w sobie zwraca obietnicę, którą możemy spożywać z innych koroutyn. Na przykład:

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)

W ES7 jest to jeszcze bardziej znormalizowane, jest teraz kilka propozycji, ale we wszystkich z nich możesz awaitobiecać. To jest po prostu „cukier” (ładniejsza składnia) dla powyższej propozycji ES6 poprzez dodanie słów kluczowych asynci await. Robiąc powyższy przykład:

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
}

Nadal zwraca obietnicę tak samo :)

Benjamin Gruenbaum
źródło
To powinna być zaakceptowana odpowiedź. +1 za asynchronizację / czekanie (chociaż nie powinniśmy return await data.json();?)
Lewis Donovan,
247

Używasz Ajax niepoprawnie. Chodzi o to, aby nie zwracać niczego, ale zamiast tego przekazać dane do czegoś, co nazywa się funkcją wywołania zwrotnego, która obsługuje dane.

To jest:

function handleData( responseData ) {

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

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

Zwracanie czegokolwiek w module obsługi przesyłania nic nie robi. Zamiast tego musisz przekazać dane lub zrobić to, co chcesz, bezpośrednio w funkcji sukcesu.

Nic
źródło
13
Ta odpowiedź jest całkowicie semantyczna ... Twoja metoda sukcesu to tylko oddzwanianie w ramach oddzwaniania. Możesz po prostu mieć success: handleDatai to zadziała.
Jacques ジ ャ ッ ク
5
A jeśli chcesz zwrócić „responseData” poza „handleData” ... :) ... jak to zrobisz ...? ... bo zwykły powrót zwróci go do wywołania zwrotnego „ajax” odesłania… a nie poza „handleData” ...
pesho hristov
@Jacques & @pesho hristov Przegapiłeś ten punkt. Prześlij moduł obsługi nie jest successmetodą, jest to otaczający zakres $.ajax.
travnik
@travnik Nie przegapiłem tego. Jeśli weźmiesz zawartość handleData i umieścisz ją w metodzie sukcesu, będzie działać dokładnie tak samo ...
Jacques ジ ャ ッ ク
234

Najprostszym rozwiązaniem jest utworzenie funkcji JavaScript i wywołanie jej dla successwywołania zwrotnego 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
źródło
3
Nie wiem, kto głosował negatywnie. Ale jest to obejście, które faktycznie działało. Użyłem tego podejścia do stworzenia całej aplikacji. Plik jquery.ajax nie zwraca danych, więc lepiej zastosować powyższe podejście. Jeśli to źle, proszę wyjaśnić i zasugerować lepszy sposób na zrobienie tego.
Hemant Bavle
11
Przepraszam, zapomniałem zostawić komentarz (zwykle tak!). Głosowałem za tym. Komentarze negatywne nie wskazują na faktyczną poprawność lub brak, wskazują na przydatność w kontekście lub brak. Nie uważam twojej odpowiedzi za przydatną, biorąc pod uwagę Felixa, który już wyjaśnia to tylko bardziej szczegółowo. Na marginesie, dlaczego miałbyś skorygować odpowiedź, jeśli jest to JSON?
Benjamin Gruenbaum,
5
ok .. @ Benjamin użyłem stringify, aby przekonwertować obiekt JSON na string. I dziękuję za wyjaśnienie swojej kwestii. Pamiętaj, aby opublikować bardziej szczegółowe odpowiedzi.
Hemant Bavle
A jeśli chcesz zwrócić „responseObj” poza „successCallback” ... :) ... jak to zrobisz ...? ... ponieważ zwykły powrót zwróci go do wywołania zwrotnego „sukces” ajax ... a nie poza „successCallback” ...
pesho hristov
221

Odpowiem okropnie wyglądającym, ręcznie rysowanym komiksem. Drugi obraz jest powodem, dla którego resultznajduje się undefinedw twoim kodzie przykład.

wprowadź opis zdjęcia tutaj

Johannes Fahrenkrug
źródło
32
Zdjęcie jest warte tysiąca słów , Osoba A - Zapytaj osobę B, aby naprawić swój samochód, z kolei Osoba B - Wykonuje połączenie Ajax i czeka na odpowiedź z serwera w sprawie szczegółów naprawy samochodu, gdy odpowiedź zostanie odebrana, funkcja Sukces Ajax wywołuje Osobę Funkcja B i przekazuje odpowiedź jako argument, osoba A otrzymuje odpowiedź.
shaijut
10
Byłoby wspaniale, gdybyś dodał wiersze kodu do każdego obrazu w celu zilustrowania pojęć.
Hassan Baig
1
Tymczasem facet z samochodem utknął na poboczu drogi. On wymaga samochód jest ustalona przed kontynuowaniem. Jest teraz sam na poboczu drogi i czeka ... Wolałby rozmawiać przez telefon, czekając na zmianę statusu, ale mechanik by tego nie zrobił ... Mechanik powiedział, że musi zająć się swoją pracą i nie może po prostu spędzaj wolny czas na telefonie. Mechanik obiecał, że oddzwoni, jak tylko będzie mógł. Po około 4 godzinach facet poddaje się i dzwoni do Ubera. - Przykład przekroczenia limitu czasu.
barrypicker
@barrypicker :-D Brilliant!
Johannes Fahrenkrug
159

Kątowy 1

Dla osób korzystających z AngularJS , można poradzić sobie z tą sytuacją przy użyciu Promises.

Tutaj mówi:

Obietnic można użyć do oderwania funkcji asynchronicznych i umożliwia połączenie wielu funkcji razem.

Można znaleźć ładne wyjaśnienie tutaj również.

Przykład znaleziony w dokumentach wymienionych poniżej.

  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 i późniejsze

W Angular2ze spojrzeniem na poniższym przykładzie, ale jego zalecany do stosowania Observablesz Angular2.

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

}

Możesz to wykorzystać w ten sposób,

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

Zobacz oryginalny post tutaj. Ale Maszynopis nie obsługuje natywnych obietnic es6 , jeśli chcesz go użyć, możesz potrzebować do tego wtyczki.

Dodatkowo tutaj są określone specyfikacje obietnic .

Maleen Abewardana
źródło
15
Nie wyjaśnia to jednak, w jaki sposób obietnice rozwiązałyby ten problem.
Benjamin Gruenbaum,
4
Metody jQuery i fetch również zwracają obietnice. Proponuję zrewidować twoją odpowiedź. Chociaż jQuery's nie jest dokładnie taki sam (wtedy jest, ale catch nie jest).
Tracker1
153

Większość odpowiedzi tutaj zawiera przydatne sugestie dotyczące pojedynczej operacji asynchronicznej, ale czasami pojawia się, gdy trzeba wykonać operację asynchroniczną dla każdej pozycji w tablicy lub innej strukturze podobnej do listy. Pokusa polega na tym, aby to zrobić:

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

Przykład:

Powodem, dla którego nie działa, jest to, że wywołania zwrotne z doSomethingAsyncjeszcze nie zostały uruchomione, zanim spróbujesz użyć wyników.

Tak więc, jeśli masz tablicę (lub jakąś listę) i chcesz wykonać operacje asynchroniczne dla każdego wpisu, masz dwie opcje: Wykonaj operacje równolegle (nakładające się) lub szeregowo (jedna po drugiej w sekwencji).

Równolegle

Możesz uruchomić je wszystkie i śledzić liczbę oczekiwanych oddzwonień, a następnie użyć wyników, gdy otrzymujesz tyle oddzwonień:

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

Przykład:

(Moglibyśmy pozbyć się expectingi po prostu użyć results.length === theArray.length, ale to pozostawia nas otwartymi na możliwość, która theArrayjest zmieniana, gdy połączenia są zaległe ...)

Zwróć uwagę, w jaki sposób używamy indexfrom od, forEachaby zapisać wynik w resultstej samej pozycji, co wpis, którego dotyczy, nawet jeśli wyniki pojawiają się poza kolejnością (ponieważ wywołania asynchroniczne niekoniecznie kończą się w kolejności, w której zostały uruchomione).

Ale co, jeśli chcesz zwrócić te wyniki z funkcji? Jak wskazały inne odpowiedzi, nie możesz; musisz zaakceptować funkcję i oddzwonić (lub zwrócić obietnicę ). Oto wersja oddzwaniania:

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

Przykład:

Lub oto wersja zwracająca Promisezamiast tego:

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

Oczywiście, jeśli doSomethingAsyncprzekażą nam błędy, rejectodrzucilibyśmy obietnicę, gdy dostaliśmy błąd).

Przykład:

(Lub możesz zrobić opakowanie, doSomethingAsyncktóre zwróci obietnicę, a następnie wykonaj poniższe czynności ...)

Jeśli doSomethingAsyncdajesz obietnicę , możesz użyć Promise.all:

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

Jeśli wiesz, że doSomethingAsynczignoruje drugi i trzeci argument, możesz po prostu przekazać go bezpośrednio map( mapwywołuje funkcję zwrotną za pomocą trzech argumentów, ale większość ludzi używa tylko pierwszego).

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

Przykład:

Zauważ, że Promise.allrozwiązuje swoją obietnicę za pomocą szeregu wyników wszystkich obietnic, które dajesz, gdy wszystkie zostaną rozpatrzone, lub odrzuca obietnicę, gdy pierwsza z obietnic, które dajesz, odrzuca.

Seria

Załóżmy, że nie chcesz, aby operacje były równoległe? Jeśli chcesz je uruchamiać jeden po drugim, musisz poczekać na zakończenie każdej operacji przed rozpoczęciem następnej. Oto przykład funkcji, która to robi i wywołuje wywołanie zwrotne z wynikiem:

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

(Ponieważ wykonujemy pracę szeregowo, możemy po prostu użyć, results.push(result)ponieważ wiemy, że nie uzyskamy wyników poza kolejnością. W powyższym przypadku mogliśmy zastosować results[index] = result;, ale w niektórych z poniższych przykładów nie mamy indeksu używać.)

Przykład:

(Lub ponownie zbuduj opakowanie, doSomethingAsyncktóre daje ci obietnicę i wykonaj poniższe czynności ...)

Jeśli doSomethingAsyncdaje obietnicę, jeśli możesz użyć składni ES2017 + (być może z transpilerem takim jak Babel ), możesz użyć asyncfunkcji za pomocą for-ofi 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);
});

Przykład:

Jeśli nie możesz użyć składni ES2017 + (jeszcze), możesz użyć wariacji na wzorzec „Obietnica zmniejszenia” (jest to bardziej złożone niż zwykłe zmniejszenie Obietnicy, ponieważ nie przekazujemy wyniku z jednego do drugiego, ale zamiast tego gromadzenie ich wyników w tablicy):

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

Przykład:

... co jest mniej kłopotliwe dzięki funkcjom strzałek 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);
});

Przykład:

TJ Crowder
źródło
1
Czy mógłbyś wyjaśnić, jak działa if (--expecting === 0)część kodu? Wersja oddzwaniania twojego rozwiązania działa dla mnie świetnie, po prostu nie rozumiem, w jaki sposób dzięki temu stwierdzeniu sprawdzasz liczbę ukończonych odpowiedzi. Doceń to po prostu brak wiedzy z mojej strony. Czy istnieje alternatywny sposób na napisanie czeku?
Sarah
@ Sarah: expectingzaczyna się od wartości array.length, czyli liczby żądań, które złożymy. Wiemy, że oddzwonienie nie zostanie wywołane, dopóki wszystkie te żądania nie zostaną uruchomione. W wywołaniu zwrotnym if (--expecting === 0)robi to: 1. Dekrementacje expecting(otrzymaliśmy odpowiedź, więc oczekujemy o jeden mniej odpowiedzi) i jeśli wartość po dekrementacji wynosi 0 (nie oczekujemy żadnych odpowiedzi), to gotowy!
TJ Crowder
1
@PatrickRoberts - Dzięki !! Tak, błąd kopiowania i wklejania, ten drugi argument został całkowicie zignorowany w tym przykładzie (co jest jedynym powodem, dla którego nie zawiódł, ponieważ, jak wskazałeś, resultsnie istniał). :-) Naprawione.
TJ Crowder,
111

Spójrz na ten przykład:

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

Jak widać getJokejest zwrócenie rozwiązany obietnicę (nie zostanie rozwiązany po powrocie res.data.value). Poczekaj więc, aż żądanie $ http.get zostanie zakończone, a następnie zostanie uruchomiony plik console.log (res.joke) (jako normalny przepływ asynchroniczny).

To jest plnkr:

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

Sposób ES6 (asynchronizacja - oczekiwanie)

(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
źródło
107

Jest to jedno z miejsc, w których dwa sposoby wiązania danych lub przechowywania danych, które są używane w wielu nowych ramach JavaScript, będą dla Ciebie świetne ...

Więc jeśli używasz Angulara, Reacta lub jakiegokolwiek innego frameworka, który łączy dwa sposoby wiązania lub przechowywania danych, ten problem jest po prostu dla Ciebie naprawiony, więc jednym słowem, twój wynik jest undefinedna pierwszym etapie, więc masz result = undefinedprzed otrzymaniem dane, a następnie, gdy tylko otrzymasz wynik, zostanie on zaktualizowany i zostanie przypisany do nowej wartości, która odpowiada na wywołanie Ajax ...

Ale jak możesz to zrobić na przykład w javascript lub jQuery, tak jak zadałeś to pytanie?

Możesz użyć funkcji zwrotnej , obietnicy i ostatnio obserwowalnej, aby obsłużyć ją za Ciebie, na przykład w obietnicach mamy jakąś funkcję, taką jak success()lub, then()która zostanie wykonana, gdy Twoje dane będą dla Ciebie gotowe, to samo z funkcją zwrotną lub subskrypcją na obserwowalnym .

Na przykład w twoim przypadku, w którym używasz jQuery , możesz zrobić coś takiego:

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

Aby uzyskać więcej informacji, zapoznaj się z obietnicami i obserwowalnościami, które są nowszymi sposobami wykonywania tego asynchronicznego działania.

Alireza
źródło
Jest to dobre w $.ajax({url: "api/data", success: fooDone.bind(this)});
skali
8
Jest to w rzeczywistości niepoprawne, ponieważ React jest jednokierunkowym wiązaniem danych
Matthew Brent
@MatthewBrent nie mylisz się, ale nie masz racji, rekwizyty React są obiektami, a jeśli zostaną zmienione, zmieniają się w całej aplikacji, ale nie jest to sposób, w jaki programista React zaleca używanie go ...
Alireza
98

Jest to bardzo powszechny problem, z którym zmagamy się, zmagając się z „tajemnicami” JavaScript. Pozwólcie mi dzisiaj spróbować wyjaśnić tę tajemnicę.

Zacznijmy od prostej funkcji JavaScript:

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

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

Jest to proste wywołanie funkcji synchronicznej (gdzie każdy wiersz kodu jest „zakończony swoim zadaniem” przed kolejnym w sekwencji), a wynik jest taki sam, jak oczekiwano.

Dodajmy teraz nieco zwrotów akcji, wprowadzając niewielkie opóźnienie w naszej funkcji, aby wszystkie wiersze kodu nie były „gotowe” w sekwencji. W ten sposób będzie emulować asynchroniczne zachowanie funkcji:

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

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

Proszę bardzo, to opóźnienie po prostu zepsuło oczekiwaną funkcjonalność! Ale co dokładnie się stało? Cóż, jest to całkiem logiczne, jeśli spojrzysz na kod. funkcja foo()po wykonaniu nic nie zwraca (w ten sposób zwracana jest wartość undefined), ale uruchamia stoper, który wykonuje funkcję po 1 s, aby zwrócić „wohoo”. Ale jak widać, wartość przypisana do baru to natychmiast zwrócone rzeczy z foo (), co nie jest niczym, to jest po prostu undefined.

Jak więc rozwiązać ten problem?

Zapytajmy naszą funkcję o OBIETNICA . Obietnica tak naprawdę chodzi o to, co to znaczy: oznacza, że ​​funkcja gwarantuje, że zapewnisz wszelkie dane wyjściowe, które otrzyma w przyszłości. zobaczmy więc, jak działa nasz mały problem powyżej:

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

Tak więc podsumowanie jest następujące: aby poradzić sobie z funkcjami asynchronicznymi, takimi jak wywołania ajax itp., Możesz użyć obietnicy resolvewartości (którą zamierzasz zwrócić). Krótko mówiąc, rozwiązujesz wartość zamiast zwracać , w funkcjach asynchronicznych.

AKTUALIZACJA (obiecuje asynchronicznie / oczekuj)

Oprócz then/catchpracy z obietnicami istnieje jeszcze jedno podejście. Chodzi o to, aby rozpoznać funkcję asynchroniczną, a następnie poczekać na spełnienie obietnic przed przejściem do następnego wiersza kodu. To wciąż tylko promisespod maską, ale z innym podejściem syntaktycznym. Aby wszystko było bardziej zrozumiałe, możesz znaleźć porównanie poniżej:

następnie wersja / catch:

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

async / oczekiwanie na wersję:

  async function saveUsers(){
     try{
        let users = await getUsers()
        saveSomewhere(users);
     }
     catch(err){
        console.error(err);
     }
  }
Anish K.
źródło
czy jest to nadal uważany za najlepszy sposób na zwrócenie wartości z obietnicy lub asynchronizacji / oczekiwania?
edwardsmarkf
3
@edwardsmarkf Osobiście nie sądzę, że istnieje najlepszy sposób jako taki. Używam obietnic z wtedy / catch, async / czekaj, a także generatorów dla asynchronicznych części mojego kodu. W dużej mierze zależy to od kontekstu użytkowania.
Anish K.,
96

Innym podejściem do zwracania wartości z funkcji asynchronicznej jest przekazanie obiektu, który zapisze wynik funkcji asynchronicznej.

Oto przykład tego samego:

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

Korzystam z resultobiektu do przechowywania wartości podczas operacji asynchronicznej. Dzięki temu wynik będzie dostępny nawet po zadaniu asynchronicznym.

Często używam tego podejścia. Byłbym zainteresowany, aby dowiedzieć się, jak dobrze to podejście działa, gdy w grę wchodzi podłączenie wyniku z powrotem przez kolejne moduły.

jsbisht
źródło
9
Nie ma tu nic specjalnego w używaniu obiektu. Działałoby to również, gdybyś przypisał mu odpowiedź bezpośrednio result. Działa, ponieważ odczytujesz zmienną po zakończeniu funkcji asynchronicznej.
Felix Kling
85

Podczas gdy obietnice i oddzwaniania działają dobrze w wielu sytuacjach, trudno jest wyrazić coś z tyłu:

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

Skończysz przechodzić async1; sprawdź, czy namejest niezdefiniowany, czy nie i odpowiednio oddzwoń.

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

async1(name, async2)

Chociaż w małych przykładach jest to w porządku , denerwuje się, gdy masz wiele podobnych przypadków i obsługi błędów.

Fibers pomaga w rozwiązaniu problemu.

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
}

Możesz sprawdzić projekt tutaj .

rohithpr
źródło
1
@recurf - To nie jest mój projekt. Możesz spróbować użyć ich narzędzia do śledzenia problemów.
rohithpr
1
czy jest to podobne do funkcji generatora? developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… *
Emanegux
1
Czy to jest nadal aktualne?
Aluan Haddad
Możesz skorzystać, async-awaitjeśli korzystasz z niektórych najnowszych wersji węzła. Jeśli ktoś utknął w starszych wersjach, może skorzystać z tej metody.
rohithpr
83

Poniższy przykład, który napisałem, pokazuje, jak to zrobić

  • Obsługa asynchronicznych połączeń HTTP;
  • Poczekaj na odpowiedź od każdego wywołania API;
  • Użyj wzorca obietnicy ;
  • Użyj wzorca Promise.all , aby dołączyć do wielu połączeń HTTP;

Ten działający przykład jest samodzielny. Zdefiniuje prosty obiekt żądania, który używa XMLHttpRequestobiektu okna do wykonywania połączeń. Zdefiniuje prostą funkcję oczekiwania na wypełnienie szeregu obietnic.

Kontekst. Przykładem jest zapytanie do punktu końcowego Spotify Web API w celu wyszukania playlistobiektów dla danego zestawu ciągów zapytań:

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

Dla każdego elementu nowa obietnica uruchomi blok - ExecutionBlockprzeanalizuj wynik, zaplanuj nowy zestaw obietnic w oparciu o tablicę wyników, czyli listę userobiektów Spotify i wykonaj ExecutionProfileBlockasynchronicznie nowe wywołanie HTTP .

Następnie możesz zobaczyć zagnieżdżoną strukturę Promise, która pozwala spawnować wiele całkowicie asynchronicznych zagnieżdżonych wywołań HTTP i dołączać wyniki z każdego podzbioru wywołań Promise.all.

UWAGA Najnowsze searchinterfejsy API Spotify będą wymagały podania tokena dostępu w nagłówkach żądania:

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

Aby uruchomić następujący przykład, musisz umieścić token dostępu w nagłówkach żądania:

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" />

Mam obszernie omawiane rozwiązanie to tutaj .

loretoparisi
źródło
80

Krótka odpowiedź brzmi: musisz zaimplementować funkcję oddzwaniania:

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
źródło
78

Odpowiedź 2017: możesz teraz robić dokładnie to, co chcesz w każdej bieżącej przeglądarce i węźle

To jest dość proste:

  • Zwróć obietnicę
  • Użyj „czekaj” , co powie JavaScript, aby czekał na obietnicę, która zostanie przetworzona na wartość (np. Odpowiedź HTTP)
  • Dodaj słowo kluczowe „async” do funkcji nadrzędnej

Oto działająca wersja Twojego kodu:

(async function(){

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

})()

Oczekiwanie jest obsługiwane we wszystkich obecnych przeglądarkach i węźle 8

mikemaccana
źródło
7
Niestety działa to tylko z funkcjami, które zwracają obietnice - na przykład nie działa z interfejsem API Node.js, który wykorzystuje wywołania zwrotne. I nie polecałbym używania go bez Babel, ponieważ nie wszyscy używają „aktualnych przeglądarek”.
Michał Perłakowski
2
@ MichałPerłakowski węzeł 8 zawiera nodejs.org/api/util.html#util_util_promisify_original, którego można użyć do złożenia obietnic zwrotnych interfejsu API node.js. To, czy masz czas i pieniądze na obsługę przeglądarek nieaktualnych, zależy oczywiście od twojej sytuacji.
mikemaccana
Niestety, IE 11 jest wciąż aktualną przeglądarką w 2018 roku i nie obsługujeawait/async
Juan Mendes,
IE11 nie jest bieżącą przeglądarką. Został wydany 5 lat temu, jego udział w rynku światowym wynosi 2,5% zgodnie z kajakiem, i jeśli ktoś nie podwaja twojego budżetu, aby zignorować wszystkie obecne technologie, to nie jest warte czasu większości ludzi.
mikemaccana
76

Js jest pojedynczym wątkiem.

Przeglądarkę można podzielić na trzy części:

1) Pętla zdarzeń

2) Interfejs API sieci Web

3) Kolejka zdarzeń

Pętla zdarzeń działa wiecznie, tj. Jest to rodzaj nieskończonej pętli. Kolejka zdarzeń to miejsce, w którym wszystkie funkcje są wypychane w przypadku jakiegoś zdarzenia (na przykład: kliknięcie), to jest jedna po drugiej przeprowadzana z kolejki i umieszczana w pętli zdarzeń, która wykonuje tę funkcję i przygotowuje ją samodzielnie dla następnej po pierwszej. Oznacza to, że wykonanie jednej funkcji nie rozpoczyna się, dopóki funkcja przed kolejką nie zostanie wykonana w pętli zdarzeń.

Pomyślmy teraz, że wepchnęliśmy dwie funkcje w kolejce, jedna służy do pobierania danych z serwera, a inna wykorzystuje te dane. Najpierw umieściliśmy funkcję serverRequest () w kolejce, a następnie funkcję useData (). funkcja serverRequest przechodzi w pętlę zdarzeń i wywołuje serwer, ponieważ nigdy nie wiemy, ile czasu zajmie pobranie danych z serwera, więc oczekuje się, że ten proces zajmie trochę czasu, więc zajęliśmy się pętlą zdarzeń, zawieszając naszą stronę, tam gdzie jest sieć Interfejs API odgrywa rolę, bierze tę funkcję z pętli zdarzeń i zajmuje się zwolnieniem pętli zdarzeń z serwera, dzięki czemu możemy wykonać kolejną funkcję z kolejki. Następną funkcją w kolejce jest funkcja useiseData (), która przechodzi w pętlę, ale ze względu na brak dostępnych danych, przechodzi marnowanie i wykonywanie następnej funkcji trwa do końca kolejki. (Nazywa się to wywoływaniem asynchronicznym, tzn. możemy zrobić coś innego, dopóki nie otrzymamy danych)

Załóżmy, że nasza funkcja serverRequest () zawiera instrukcję return w kodzie, gdy otrzymamy dane z serwera Web API, wypchną je w kolejce na końcu kolejki. Gdy jest on wypychany na końcu w kolejce, nie możemy wykorzystać jego danych, ponieważ w naszej kolejce nie ma już funkcji do wykorzystania tych danych.Dlatego nie można zwrócić czegoś z Async Call.

Zatem rozwiązaniem tego jest oddzwonienie lub obietnica .

Obraz z jednej z odpowiedzi tutaj, poprawnie wyjaśnia użycie wywołania zwrotnego ... Dajemy naszą funkcję (funkcję wykorzystującą dane zwrócone z serwera) do funkcji wywołującej serwer.

Oddzwonić

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

}

W moim Kodeksie nazywa się to jako

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

Oddzwonienie Javscript.info

Aniket Jha
źródło
68

Możesz użyć tej niestandardowej biblioteki (napisanej przy użyciu Obietnicy), aby wykonać zdalne połączenie.

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

Prosty przykład użycia:

$http({
    method: 'get',
    url: 'google.com'
}).then(function(response) {
    console.log(response);
}, function(error) {
    console.log(error)
});
Vinoth Rajendran
źródło
67

Innym rozwiązaniem jest wykonanie kodu za pomocą sekwencyjnego modułu wykonującego nsynjs .

Jeśli funkcja podstawowa jest obiecana

nsynjs oceni kolejno wszystkie obietnice i umieści wynik obietnicy we datawłaściwości:

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>

Jeśli funkcja podstawowa nie jest obiecana

Krok 1. Zawijaj funkcję z wywołaniem zwrotnym do opakowania obsługującego nsynjs (jeśli ma obiecaną wersję, możesz pominąć ten krok):

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;

Krok 2. Włącz logikę synchroniczną w funkcję:

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

Krok 3. Uruchom funkcję w sposób synchroniczny przez nsynjs:

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

Nsynjs oceni krok po kroku wszystkie operatory i wyrażenia, wstrzymując wykonanie w przypadku, gdy wynik jakiejś wolnej funkcji nie będzie gotowy.

Więcej przykładów tutaj: https://github.com/amaksr/nsynjs/tree/master/examples

amaksr
źródło
2
To jest interesujące. Podoba mi się, jak pozwala kodować połączenia asynchroniczne tak, jak robisz to w innych językach. Ale technicznie nie jest to prawdziwy JavaScript?
J Morris,
41

ECMAScript 6 ma „generatory”, które pozwalają łatwo programować w stylu asynchronicznym.

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

Aby uruchomić powyższy kod, wykonaj następujące czynności:

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

Jeśli potrzebujesz kierować na przeglądarki, które nie obsługują ES6, możesz uruchomić kod przez Babel lub kompilator zamykający, aby wygenerować ECMAScript 5.

Wywołanie zwrotne ...argsjest owinięte w tablicę i zniszczone podczas ich czytania, dzięki czemu wzorzec radzi sobie z wywołaniami zwrotnymi z wieloma argumentami. Na przykład z węzłem fs :

const [err, data] = yield fs.readFile(filePath, "utf-8", callback);
James
źródło
39

Oto kilka podejść do pracy z żądaniami asynchronicznymi:

  1. Obiekt Promise przeglądarki
  2. Q - biblioteka obietnic dla JavaScript
  3. A + Promises.js
  4. jQuery odroczony
  5. API XMLHttpRequest
  6. Korzystanie z koncepcji oddzwaniania - jako implementacja w pierwszej odpowiedzi

Przykład: jQuery odroczyła implementację do pracy z wieloma żądaniami

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
źródło
38

Znajdujemy się we wszechświecie, który wydaje się postępować wzdłuż wymiaru, który nazywamy „czasem”. Naprawdę nie rozumiemy, która jest godzina, ale opracowaliśmy abstrakcje i słownictwo, które pozwalają nam rozumować i mówić o tym: „przeszłość”, „teraźniejszość”, „przyszłość”, „przed”, „po”.

Systemy komputerowe, które budujemy - coraz więcej - mają czas jako ważny wymiar. Pewne rzeczy mają się wydarzyć w przyszłości. Potem inne rzeczy muszą się wydarzyć po tym, jak te pierwsze rzeczy w końcu się pojawią. Jest to podstawowe pojęcie zwane „asynchronicznością”. W naszym coraz bardziej połączonym w sieć świecie najczęstszym przypadkiem asynchroniczności jest oczekiwanie, aż jakiś zdalny system odpowie na jakieś żądanie.

Rozważ przykład. Dzwonisz do mleczarza i zamawiasz mleko. Jeśli chodzi, chcesz włożyć ją do kawy. Nie możesz teraz wlewać mleka do kawy, ponieważ jeszcze jej tu nie ma. Musisz poczekać, aż nadejdzie, zanim włożysz go do kawy. Innymi słowy, następujące funkcje nie będą działać:

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

Ponieważ JS nie ma sposobu, aby wiedzieć, że musi czekać na order_milkzakończenie przed wykonaniem put_in_coffee. Innymi słowy, nie wie, że order_milkjest asynchroniczny - to coś, co nie spowoduje mleka aż do pewnego czasu. JS i inne języki deklaratywne wykonują jedną instrukcję po drugiej bez czekania.

Klasyczne podejście JS do tego problemu, wykorzystujące fakt, że JS obsługuje funkcje jako obiekty pierwszej klasy, które można przekazywać, polega na przekazaniu funkcji jako parametru do żądania asynchronicznego, które następnie wywoła po zakończeniu. jego zadanie kiedyś w przyszłości. To jest podejście „oddzwaniania”. To wygląda tak:

order_milk(put_in_coffee);

order_milk rozpoczyna, zamawia mleko, a potem, kiedy tylko dotrze, przywołuje put_in_coffee .

Problem z tym podejściem zwrotnym polega na tym, że zanieczyszcza on normalną semantykę funkcji zgłaszającej jej wynik return; zamiast tego funkcje nie mogą raportować swoich wyników przez wywołanie wywołania zwrotnego podanego jako parametr. Również to podejście może szybko stać się niewygodne w przypadku dłuższych sekwencji zdarzeń. Powiedzmy na przykład, że chcę poczekać, aż mleko zostanie wlane do kawy, i wtedy i tylko wtedy wykonaj trzeci krok, a mianowicie wypicie kawy. W końcu muszę napisać coś takiego:

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

gdzie przechodzę put_in_coffeezarówno do mleka, aby je wlać, jak i do akcji (drink_coffee ), które należy wykonać po włożeniu mleka. Taki kod staje się trudny do napisania, odczytania i debugowania.

W takim przypadku możemy przepisać kod w pytaniu jako:

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

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

Wprowadź obietnice

To była motywacja do pojęcia „obietnicy”, która jest szczególnym rodzajem wartości, która reprezentuje jakiś przyszły lub asynchroniczny rezultat. Może reprezentować coś, co już się wydarzyło lub wydarzy się w przyszłości, lub może nigdy się nie wydarzyć. Obietnice mają pojedynczą, nazwaną metodę, thenktórej przekazujesz akcję, która ma zostać wykonana, gdy wynik reprezentacji obietnicy zostanie zrealizowany.

W przypadku naszego mleka i kawy, możemy zaprojektować order_milk, aby powrócić obietnicę dla mleka przybyciu, a następnie określić put_in_coffeejako thendziałania, co następuje:

order_milk() . then(put_in_coffee)

Zaletą tego jest to, że możemy połączyć je ze sobą, aby utworzyć sekwencje przyszłych zdarzeń („łączenie”):

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

Zastosujmy obietnice do twojego konkretnego problemu. Opakowamy naszą logikę żądania do funkcji, która zwraca obietnicę:

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

W rzeczywistości wszystko, co zrobiliśmy, zostało dodane returndo wezwania do $.ajax. Działa to, ponieważ jQuery $.ajaxzwraca już coś w rodzaju obietnicy. (W praktyce, bez wchodzenia w szczegóły, wolelibyśmy zawinąć to wezwanie, aby zwrócić prawdziwą obietnicę, lub użyć innej alternatywy do $.ajaxtego.) Teraz, jeśli chcemy załadować plik i poczekać, aż zakończy się i następnie zrób coś, możemy po prostu powiedzieć

get_data() . then(do_something)

na przykład,

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

Podczas korzystania z obietnic kończymy przekazywanie wielu funkcji then, więc często pomocne jest użycie bardziej kompaktowych funkcji strzałek w stylu ES6:

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

Słowo asynckluczowe

Ale wciąż jest coś niejasno niezadowalającego w konieczności pisania kodu w jedną stronę, jeśli jest synchroniczna, i całkiem inną, jeśli w sposób asynchroniczny. Dla synchronicznych piszemy

a();
b();

ale jeśli ajest asynchroniczny, z obietnicami musimy napisać

a() . then(b);

Powyżej powiedzieliśmy: „JS nie ma sposobu, aby wiedzieć, że musi poczekać na zakończenie pierwszego połączenia, zanim wykona drugie”. Czy nie byłoby miło, gdyby tam był jakiś sposób, aby powiedzieć, że JS? Okazuje się, że istnieje - awaitsłowo kluczowe używane w specjalnym typie funkcji zwanej funkcją „asynchroniczną”. Ta funkcja jest częścią nadchodzącej wersji ES, ale jest już dostępna w transpilerach, takich jak Babel, przy odpowiednich ustawieniach wstępnych. To pozwala nam po prostu pisać

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

W twoim przypadku byłbyś w stanie napisać coś takiego

async function foo() {
  data = await get_data();
  console.log(data);
}
663031
źródło
37

Krótka odpowiedź : Twoja foo()metoda zwraca natychmiast, a $ajax()wywołanie jest wykonywane asynchronicznie po powrocie funkcji . Problem polega na tym, jak i gdzie przechowywać wyniki pobrane przez wywołanie asynchroniczne po jego zwróceniu.

W tym wątku podano kilka rozwiązań. Być może najłatwiejszym sposobem jest przekazanie obiektu do foo()metody i zapisanie wyników w elemencie tego obiektu po zakończeniu wywołania asynchronicznego.

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

Pamiętaj, że połączenie do foo()nadal nie zwróci niczego przydatnego. Jednak wynik wywołania asynchronicznego zostanie teraz zapisany w result.response.

David R. Tribble
źródło
14
Chociaż to działa, nie jest tak naprawdę lepsze niż przypisywanie do zmiennej globalnej.
Felix Kling
36

Użyj callback()funkcji wewnątrz foo()sukcesu. Spróbuj w ten sposób. Jest prosty i łatwy do zrozumienia.  

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
źródło
29

Pytanie brzmiało:

Jak zwrócić odpowiedź z połączenia asynchronicznego?

które MOŻNA interpretować jako:

Jak sprawić, by kod asynchroniczny wyglądał synchronicznie ?

Rozwiązaniem będzie uniknięcie wywołań zwrotnych i użycie kombinacji Obietnic i asynchronizacji / oczekiwania .

Chciałbym podać przykład żądania Ajax.

(Chociaż można go napisać w Javascripcie, wolę napisać go w Pythonie i skompilować do Javascript za pomocą Transcrypt . Będzie wystarczająco jasny).

Pozwala najpierw włączyć użycie JQuery, aby było $dostępne jako S:

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

Zdefiniuj funkcję, która zwraca Obietnicę , w tym przypadku wywołanie 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()

Użyj kodu asynchronicznego tak, jakby był synchroniczny :

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
źródło
29

Korzystanie z obietnicy

Najdoskonalszą odpowiedzią na to pytanie jest użycie 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);
  });
}

Stosowanie

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

Ale poczekaj...!

Wystąpił problem z korzystaniem z obietnic!

Dlaczego powinniśmy korzystać z naszej niestandardowej obietnicy?

Używałem tego rozwiązania przez pewien czas, dopóki nie zorientowałem się, że w starych przeglądarkach występuje błąd:

Uncaught ReferenceError: Promise is not defined

Więc postanowiłem zaimplementować własną klasę Promise dla ES3 do kompilatorów poniżej js, jeśli nie jest zdefiniowana. Po prostu dodaj ten kod przed głównym kodem, a następnie bezpiecznie skorzystaj z Promise!

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
źródło
28

Oczywiście istnieje wiele podejść, takich jak żądanie synchroniczne, obiecuję, ale z mojego doświadczenia uważam, że powinieneś zastosować podejście zwrotne. Naturalne jest zachowanie asynchroniczne Javascript. Tak więc fragment kodu można przepisać nieco inaczej:

function foo() {
    var result;

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

    return result;
}

function myCallback(response) {
    // Does something.
}
Khoa Bui
źródło
5
W callbackach i JavaScript nie ma nic z natury asynchronicznego.
Aluan Haddad
19

Zamiast rzucać w ciebie kodem, istnieją 2 koncepcje, które są kluczowe dla zrozumienia, w jaki sposób JS obsługuje wywołania zwrotne i asynchroniczność. (czy to w ogóle słowo?)

Model pętli zdarzeń i współbieżności

Są trzy rzeczy, o których musisz wiedzieć; Kolejka; pętla zdarzeń i stos

Mówiąc w uproszczeniu, pętla zdarzeń przypomina kierownika projektu, stale nasłuchuje wszelkich funkcji, które chcą uruchomić, i komunikuje się między kolejką a stosem.

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

Gdy otrzyma komunikat o uruchomieniu czegoś, dodaje go do kolejki. Kolejka to lista rzeczy, które czekają na wykonanie (np. Żądanie AJAX). wyobraź sobie to tak:

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

Kiedy jeden z tych komunikatów ma zostać wykonany, wyskakuje z kolejki i tworzy stos, stos to wszystko, co JS musi wykonać, aby wykonać instrukcję zawartą w komunikacie. W naszym przykładzie mówi się, żeby zadzwonićfoobarFunc

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

Więc wszystko, co foobarFunc musi wykonać (w naszym przypadku anotherFunction ) zostanie zepchnięte na stos. wykonane, a następnie zapomniane o - pętla zdarzeń przejdzie następnie do kolejnej rzeczy w kolejce (lub będzie nasłuchiwać wiadomości)

Kluczową kwestią jest tutaj kolejność wykonywania. To jest

KIEDY coś się uruchomi

Gdy wykonujesz połączenie za pomocą AJAX do strony zewnętrznej lub uruchamiasz jakikolwiek kod asynchroniczny (na przykład setTimeout), JavaScript jest zależny od odpowiedzi, zanim będzie mógł kontynuować.

Najważniejsze pytanie brzmi: kiedy otrzyma odpowiedź? Odpowiedź brzmi: nie wiemy - więc pętla zdarzeń czeka na komunikat z komunikatem „hej, uruchom mnie”. Jeśli JS po prostu zaczeka synchronicznie na tę wiadomość, aplikacja zawiesi się i będzie do dupy. Tak więc JS kontynuuje wykonywanie następnego elementu w kolejce, czekając, aż wiadomość zostanie dodana z powrotem do kolejki.

Dlatego w funkcji asynchronicznej używamy funkcji nazywanych wywołaniami zwrotnymi . To trochę jak obietnica dosłownie. Tak jak w obietnicy zwrócenia czegoś w pewnym momencie, jQuery używa określonych wywołań zwrotnych o nazwie deffered.done deffered.faili deffered.always(między innymi). Możesz je wszystkie zobaczyć tutaj

Tak więc musisz przekazać funkcję, która ma zostać wykonana w pewnym momencie z przekazanymi do niej danymi.

Ponieważ wywołanie zwrotne nie jest wykonywane natychmiast, ale później ważne jest, aby przekazać odwołanie do funkcji, która nie została wykonana. więc

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

więc przez większość czasu (ale nie zawsze) będziesz przechodzić fooniefoo()

Mam nadzieję, że to będzie miało sens. Kiedy napotykasz takie rzeczy, które wydają się mylące - bardzo polecam przeczytanie dokumentacji w pełni, aby przynajmniej ją zrozumieć. Dzięki temu będziesz znacznie lepszym programistą.

Matthew Brent
źródło
18

Używając ES2017, powinieneś mieć to jako deklarację funkcji

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

I wykonanie tego w ten sposób.

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

Lub składnia Promise

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

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

})
Fernando Carvajal
źródło
czy ta druga funkcja może być ponownie wykorzystana?
Zum Dummi
Jak korzystać z wyników, jeśli oncolse, log jest wywoływany? Czy w tym momencie nie wszystko idzie tylko na konsolę?
Ken Ingram