→ 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.$.ajax
return 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 findItem
moż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/await
Obiecuje 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+.
Wersja ECMAScript wydana w 2017 r. Wprowadziła obsługę funkcji asynchronicznych na poziomie składni . Za pomocą async
i await
możesz pisać asynchronicznie w „stylu synchronicznym”. Kod jest nadal asynchroniczny, ale łatwiej go odczytać / zrozumieć.
async/await
opiera się na obietnicach: async
funkcja 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 await
wewnątrz async
funkcji. W tej chwili najwyższy poziom await
nie jest jeszcze obsługiwany, więc może być konieczne utworzenie asynchronicznego IIFE ( Natychmiastowe wywołanie wyrażenia funkcji ), aby rozpocząć async
kontekst.
Możesz przeczytać więcej o MDN async
i await
na 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 foo
zaakceptować oddzwonienie i użyć go jako success
oddzwonienia. 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
});
}
callback
będzie odnosić się do funkcji, którą przekazujemy, foo
kiedy ją wywołujemy i po prostu ją przekazujemy success
. To znaczy, gdy żądanie Ajax zakończy się powodzeniem, $.ajax
zadzwoni callback
i 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 if
instrukcja będzie zawsze pobierać ten obiekt odroczony, traktować go jak true
i 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 XMLHTTPRequest
obiektu, przekaż false
jako trzeci argument do .open
.
jQuery
Jeśli używasz jQuery , możesz ustawić async
opcję na false
. Zauważ, że ta opcja jest przestarzała od jQuery 1.8. Następnie możesz albo użyć success
wywołania zwrotnego, albo uzyskać dostęp do responseText
wł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
, $.getJSON
itp., 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).