JavaScript Promises, czy rzeczywiście takie przydatne?

Pamiętam, gdy kilka lat nie doceniałem siły i wartości JavaScriptu. Zmieniło się to zupełnie, kiedy poznałem jQuery. W ostatnich latach powstało wiele projektów i bibliotek, które naprawdę wyciskają prawdziwy sok. Wszystko co nowe, wymaga czasu, cierpliwości i zaangażowania, ale kiedy zrozumiesz podstawy, docenisz zaawansowane techniki. Podobnie było ze mną, gdy pierwszy raz zobaczyłem implementację obiektu Deferred w jQuery 1.5.

Obietnice (ang. Promises) stanowią nowy paradygmat programowania w JavaScripcie. Jednak jego zrozumienie wymaga czasu i pewnej uwagi. Generalnie, Obietnica stanowi wynik pewnego zadania, które się powiodło lub nie. Jedyne wymaganie jakie stawia Obietnica to funkcja then, która określa funkcje zwrotne w przypadku sukcesu lub porażki zadania. Bardzo dokładnie ta koncepcja została opisana w artykule CommonJS Promises/A proposal.

Przykładowo, dla lepszego zrozumienia przedstawimy asynchroniczną operacje zapisu dla naszego obiektu Parse.Object, która w ujęciu funkcji zwrotnych przedstawia się następująco:

object.save({ key: value }, {
  success: function(object) {
    // obiekt zapisany.
  },
  error: function(object, error) {
    // nieudane zachowanie obiektu.
  }
});

Analogiczne zachowanie przy rozważeniu paradygmatu obietnic, równie prosto:

object.save({ key: value }).then(
  function(object) {
    // obiekt zapisany.
  },
  function(error) {
    // nieudane zachowanie obiektu.
  });

Bardzo podobnie? Więc o co tyle szumu? Prawdziwa moc obietnic to wielokrotne wywoływanie i wzajemne łączenie. Wywołanie promise.then(func) zwraca nową obietnicę, która nie spełni się się dopóki func nie zakończy się. Istnieje pewna wyjątkowa kwestia, dotycząca sposobu wywołania funkcji przy udziale obietnic. Jeśli funkcja zwrotna związana z then zwraca nową obietnicę, wówczas obietnica zwrócona przez then nie spełni się, dopóki nie spełni się obietnica funkcji zwrotnej. Brzmi skomplikowanie, ale dokładne zachowanie takiego scenariusza przedstawiono w artykule Promises/A+. Ponieważ przytoczony artykuł jest skomplikowany, omówimy konkretny przykład, który lepiej przedstawi całość zagadnienia.

Wyobraź sobie kod odpowiedzialny za logowanie, szukanie obiektu i wreszcie jego aktualizację. W przypadku starego paradygmatu funkcji zwrotnych, skończy się to wielkim zagnieżdżonym kodem:

Parse.User.logIn("user", "pass", {
  success: function(user) {
    query.find({
      success: function(results) {
        results[0].save({ key: value }, {
          success: function(result) {
            // obiekt zapisany.
          }
        });
      }
    });
  }
});

Szybko taki kod staje się przerażający, nawet bez jakiejkolwiek obsługi błędów. Jednak przy podejściu kolejnych wywołań i zastosowaniu obietnic prezentuje się to bardziej czytelnie:

Parse.User.logIn("user", "pass").then(function(user) {
  return query.find();
}).then(function(results) {
  return results[0].save({ key: value });
}).then(function(result) {
  // obiekt zapisany.
});

O wiele lepiej, czyż nie!

Obsługa błędów

Powyższy kod, prezentuje idealnie wady dotychczasowego podejścia, ponieważ po dodaniu obsługi błędów, kod staje się trudny w zrozumieniu:

Parse.User.logIn("user", "pass", {
  success: function(user) {
    query.find({
      success: function(results) {
        results[0].save({ key: value }, {
          success: function(result) {
            // obiekt zapisany.
          },
          error: function(result, error) {
            // wystąpienie błędu.
          }
        });
      },
      error: function(error) {
        // wystąpienie błędu.
      }
    });
  },
  error: function(user, error) {
    // wystąpienie błędu.
  }
});

Obietnice wiedzą, kiedy zostały spełnione lub nie, zatem spokojnie przekazują błędy bez wywoływania funkcji zwrotnych, dopóki obsługa błędów nie zostanie zakończona. Powyższy kod zapiszemy jeszcze prościej:

Parse.User.logIn("user", "pass").then(function(user) {
  return query.find();
}).then(function(results) {
  return results[0].save({ key: value });
}).then(function(result) {
  // obiekt zapisany.
}, function(error) {
  // wystąpienie błędu.
});

Generalnie, programiści używają niepowodzenia obietnic jako asynchronicznego wariantu rzucenia wyjątku. W rzeczywistości, jeśli funkcja zwrotna przekazana do then zgłosi błąd to zwrócona obietnica, także zakończy się z błędem. Propagowanie błędu do następnego bloku obsługi błędów jest równoważne bąbelkowaniu wyjątku do momentu spotkania instrukcji catch.

jQuery, Backbone oraz Parse

Istnieje wiele implementacji obietnic dostępnych dla programistów. Na przykład, wspomniany na początku obiekt Deffered w jQuery, WinJS.Promise Microsoftu, when.js, q, i dojo.Deferred.

Jest jeszcze jeden istotny punkt widzenia całej sprawy, którego powinniśmy być świadomi. Długa i fascynująca dyskusja dla jQuery, które nie do końca implementuje obsługę specyfikację Promises/A w sposób jaki robią to inne popularne biblioteki. Znalazłem tylko jeden przypadek, w którym oba podejścia rozchodzą się. Jeśli obsługa błędu zwraca coś innego niż obietnicę, większość implementacji rozważa obsługę błędu zamiast jego zgłaszania. Jednakże, jQuery nie rozważa obsługi błędu w tym przypadku, a jedynie jego dalsze zgłaszanie. Pomimo różnych implementacji, całość powinna pracować płynnie, dlatego miej oko na takie sytuacje. Potencjalne rozwiązanie to zwracanie obietnic (zamiast surowych wartości) zawsze podczas obsługi błędów, ponieważ są one traktowane tak samo.

doFailingAsync().then(function() {
  // wywołanie doFailingAsync nie powiodło się.
}, function(error) {
  // Próba obsługi błędu.
  return "It's all good.";
}).then(function(result) {
  // Inne (nie jQuery) implementacje osiągną zakładany wynik, czyli równoważne "It's all good.".
}, function(error) {
  // jQuery osiągnie to z błędem, czyli równoważne "It's all good.".
});

W najnowszym wydaniu Backbone 0.9.10, metody asynchroniczne zwracają teraz obiekt jqXHR, któr jest typem obietnicy jQuery. Jednym z celów dla Parse JavaScript SDK jest utrzymanie jak największe zgodności z Backbone. Nie możemy zwrócić jqXHR, bo to nie będzie dobrze działał w Cloud Code. Rozwiązaniem okazało się stworzenie klasy o nazwie Parse.Promise, która jest zgodna z semantyką obiektu Deffered w jQuery. Ostatnia wersja Parse JavaScript SDK posiada zmodernizowane wszystkie asynchroniczne metody, aby zwracały te nowe obiekty. Stare funkcje zwrotne są nadal akceptowane. Ale w oparciu o przykłady wymienione powyżej, sądzimy, że wolisz nową drogę. Zatem daj obietnicom szansę!

Autorem oryginalnego artykułu What’s so great about JavaScript Promises? jest Bryan Klimt. Parse jest platformą aplikacji w chmurze dla iOS, Android, JavaScript, Windows 8, Windows Phone 8 oraz OS X.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *