Lekcja 15: AJAX w jQuery - Obiekty Deferred i Promises (.done(), .fail(), .always())
Metody AJAX w jQuery (w tym $.ajax() i metody skrótowe jak $.get(), $.getJSON()) zwracają specjalny obiekt zwany jqXHR (jQuery XMLHttpRequest). Obiekt ten implementuje interfejs Promise, co pozwala na bardziej elastyczne i czytelne zarządzanie operacjami asynchronicznymi za pomocą metod .done(), .fail() i .always(), zamiast tradycyjnych callbacków w opcjach konfiguracyjnych.
Interfejs Promise w jQuery AJAX
Zamiast przekazywać funkcje success, error i complete w obiekcie konfiguracyjnym $.ajax(), możemy dołączyć je do obiektu jqXHR zwróconego przez wywołanie AJAX:
let zapytanie = $.ajax({
url: "/api/dane",
dataType: "json"
});
// Rejestrowanie callbacków za pomocą metod Promise
zapytanie.done(function(data, textStatus, jqXHR) {
// Wykonywane po sukcesie (odpowiednik `success`)
console.log("Sukces (done):", data);
});
zapytanie.fail(function(jqXHR, textStatus, errorThrown) {
// Wykonywane po błędzie (odpowiednik `error`)
console.error("Błąd (fail):", textStatus, errorThrown);
});
zapytanie.always(function(dataOrJqXHR, textStatus, jqXHROrErrorThrown) {
// Wykonywane zawsze po zakończeniu (sukces lub błąd) (odpowiednik `complete`)
console.log("Zawsze (always): Zapytanie zakończone.");
});
// Można też łączyć wywołania (chaining)
$.ajax({ url: "/api/inne-dane" })
.done(function(dane) { /* obsługa sukcesu */ })
.fail(function() { /* obsługa błędu */ })
.always(function() { /* wykonaj zawsze */ });
Zalety Używania .done(), .fail(), .always():
- Czytelność: Oddzielenie konfiguracji zapytania od logiki obsługi odpowiedzi może poprawić czytelność kodu.
- Elastyczność: Można dołączyć wiele handlerów do tego samego zdarzenia (np. kilka funkcji
.done()). - Kompozycja: Umożliwia łatwiejsze zarządzanie wieloma zapytaniami AJAX (np. za pomocą
$.when()). - Obsługa błędów w metodach skrótowych: Jest to standardowy sposób obsługi błędów dla metod takich jak
$.get()czy$.getJSON(), które nie przyjmują callbackuerrorjako argumentu.
Przykład z $.getJSON()
$("#pobierz-komentarze").on("click", function() {
let postId = $("#post-id-input").val();
$("#status").text("Ładowanie komentarzy...");
$.getJSON("https://jsonplaceholder.typicode.com/comments", { postId: postId })
.done(function(komentarze) {
let listaHtml = "";
komentarze.forEach(kom => listaHtml += `- ${kom.name}: ${kom.body}
`);
listaHtml += "
";
$("#lista-komentarzy").html(listaHtml);
$("#status").text("Komentarze załadowane.");
})
.fail(function(jqXHR, textStatus, errorThrown) {
$("#lista-komentarzy").empty();
$("#status").text(`Błąd ładowania: ${textStatus} - ${errorThrown}`).css("color", "red");
})
.always(function() {
console.log("Zakończono próbę pobrania komentarzy.");
});
});
Łączenie Wielu Zapytań AJAX za pomocą $.when()
Czasami potrzebujemy wykonać kilka niezależnych zapytań AJAX i zareagować dopiero wtedy, gdy wszystkie się zakończą (pomyślnie). Do tego służy metoda $.when().
$.when() przyjmuje jako argumenty jeden lub więcej obiektów Deferred/Promise (np. zwróconych przez $.ajax()). Zwraca nowy obiekt Promise, który zostanie rozwiązany (resolved), gdy wszystkie przekazane Promise zostaną rozwiązane.
let zapytanieUzytkownikow = $.getJSON("/api/users");
let zapytanieProduktow = $.getJSON("/api/products");
let zapytanieUstawien = $.getJSON("/api/settings");
$.when(zapytanieUzytkownikow, zapytanieProduktow, zapytanieUstawien)
.done(function(wynikUser, wynikProd, wynikUst) {
// Ten callback wykona się, gdy WSZYSTKIE 3 zapytania zakończą się sukcesem
// Argumenty funkcji `done` to tablice, gdzie pierwszy element [0] zawiera dane odpowiedzi
let uzytkownicy = wynikUser[0];
let produkty = wynikProd[0];
let ustawienia = wynikUst[0];
console.log("Wszystkie dane załadowane:", uzytkownicy, produkty, ustawienia);
// Tutaj można zbudować interfejs używając wszystkich danych
})
.fail(function() {
// Ten callback wykona się, jeśli KTÓREKOLWIEK z zapytań się nie powiedzie
console.error("Wystąpił błąd podczas ładowania danych.");
});
Ważne: Argumenty przekazywane do funkcji .done() po użyciu $.when() są tablicami. Każda tablica odpowiada jednemu z przekazanych Promise i zawiera argumenty, które normalnie trafiłyby do pojedynczego .done() (czyli [data, textStatus, jqXHR]).
Zadanie praktyczne
Użyj API JSONPlaceholder. Chcemy pobrać dane użytkownika (/users/2) oraz jego posty (/posts?userId=2) i wyświetlić je dopiero, gdy oba zapytania się powiodą.
Stwórz plik HTML z przyciskiem "Pobierz Dane Usera 2" i div-em z ID "dane-kompletne".
Używając $.when() oraz $.getJSON():
- Po kliknięciu przycisku, zainicjuj dwa zapytania
$.getJSON(): jedno po dane użytkownika 2, drugie po jego posty. - Użyj
$.when(), przekazując mu oba obiekty Promise zwrócone przez$.getJSON(). - W funkcji
.done()dołączonej do$.when(): - Pobierz dane użytkownika z pierwszego argumentu (
argument1[0]) i dane postów z drugiego argumentu (argument2[0]). - Wyświetl w div-ie "dane-kompletne" imię użytkownika oraz listę tytułów jego postów.
- W funkcji
.fail()dołączonej do$.when(), wyświetl komunikat o błędzie w div-ie.
Pokaż rozwiązanie
HTML:
<!DOCTYPE html>
<html>
<head>
<title>Test $.when()</title>
</head>
<body>
<button id="pobierz-dane-usera2">Pobierz Dane Usera 2</button>
<div id="dane-kompletne" style="margin-top: 10px; border: 1px solid #ccc; padding: 10px; min-height: 100px;">
<!-- Tutaj pojawią się dane -->
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script>
$(function() {
$("#pobierz-dane-usera2").on("click", function() {
$("#dane-kompletne").html("Ładowanie danych...");
let zapytanieUsera = $.getJSON("https://jsonplaceholder.typicode.com/users/2");
let zapytaniePostow = $.getJSON("https://jsonplaceholder.typicode.com/posts", { userId: 2 });
$.when(zapytanieUsera, zapytaniePostow)
.done(function(wynikUser, wynikPosty) {
let user = wynikUser[0]; // Dane użytkownika są w pierwszym elemencie pierwszej tablicy
let posts = wynikPosty[0]; // Dane postów są w pierwszym elemencie drugiej tablicy
let html = `<h3>${user.name}</h3>`;
html += "<h4>Posty:</h4>";
posts.forEach(function(post) {
html += `- ${post.title}
`;
});
html += "
";
$("#dane-kompletne").html(html);
})
.fail(function() {
$("#dane-kompletne").html("<p style=\'color: red;\'>Błąd podczas ładowania danych.</p>");
});
});
});
</script>
</body>
</html>
Zadanie do samodzielnego wykonania
Stwórz przycisk "Pobierz Todos" i div z ID "lista-todos".
Używając $.ajax() i metod .done()/.fail()/.always():
- Po kliknięciu przycisku, wykonaj zapytanie GET do
https://jsonplaceholder.typicode.com/todos?userId=1. - Użyj
.done(), aby wyświetlić listę zadań (title) w div-ie "lista-todos". Oznacz wizualnie (np. przekreśleniem lub innym stylem) zadania, które są ukończone (completed: true). - Użyj
.fail(), aby wyświetlić komunikat błędu. - Użyj
.always(), aby wypisać w konsoli "Zakończono zapytanie o todos.".
FAQ - jQuery AJAX Promises (.done(), .fail(), .always(), $.when())
Czy mogę używać .done() itp. razem z callbackami w $.ajax()?
Tak, można mieszać oba podejścia. Jeśli zdefiniujesz np. callback success w opcjach $.ajax() oraz dołączysz funkcję .done() do zwróconego obiektu jqXHR, obie funkcje zostaną wykonane po pomyślnym zakończeniu zapytania. Jednak dla spójności kodu zaleca się trzymanie jednego stylu.
Co jeśli jedno z zapytań w $.when() się nie powiedzie?
Jeśli którekolwiek z zapytań przekazanych do $.when() zakończy się błędem, zostanie wywołany callback .fail() dołączony do $.when(). Callback .done() nie zostanie wykonany.
Czy $.when() działa tylko z zapytaniami AJAX jQuery?
$.when() może przyjmować dowolne obiekty implementujące interfejs Promise (posiadające metodę .then()), a także inne wartości. Jeśli przekażesz wartość niebędącą Promise, $.when() potraktuje ją jako natychmiast rozwiązaną Promise. Jest to przydatne do synchronizacji operacji AJAX z innymi operacjami.
Jaka jest różnica między .always() a .then()?
.always() jest specyficzne dla implementacji Deferred/Promise w jQuery i jest wywoływane zawsze, niezależnie od sukcesu czy porażki. Standardowa metoda Promises .then(onFulfilled, onRejected) pozwala zdefiniować osobne funkcje dla sukcesu (onFulfilled) i porażki (onRejected). jQuery również wspiera .then(), które zachowuje się podobnie do standardu.
Czy powinienem używać natywnych Promises (ES6) zamiast jQuery Deferred?
Natywne Promises są standardem w nowoczesnym JavaScript i oferują nieco inną składnię (np. .then().catch()). Jeśli pracujesz w środowisku wspierającym ES6+ i nie potrzebujesz specyficznych funkcji jQuery Deferred, używanie natywnych Promises jest często preferowane dla lepszej kompatybilności i zgodności ze standardami. Jednak obiekty jqXHR zwracane przez jQuery AJAX nadal dobrze integrują się z metodami .done(), .fail(), .always().