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

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

  1. Po kliknięciu przycisku, zainicjuj dwa zapytania $.getJSON(): jedno po dane użytkownika 2, drugie po jego posty.
  2. Użyj $.when(), przekazując mu oba obiekty Promise zwrócone przez $.getJSON().
  3. W funkcji .done() dołączonej do $.when():
  4. Pobierz dane użytkownika z pierwszego argumentu (argument1[0]) i dane postów z drugiego argumentu (argument2[0]).
  5. Wyświetl w div-ie "dane-kompletne" imię użytkownika oraz listę tytułów jego postów.
  6. 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():

  1. Po kliknięciu przycisku, wykonaj zapytanie GET do https://jsonplaceholder.typicode.com/todos?userId=1.
  2. 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).
  3. Użyj .fail(), aby wyświetlić komunikat błędu.
  4. 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().