Zaawansowana reaktywność

Pasek boczny 11.5

procent przetłumaczone

W tym rozdziale dowiesz się:

  • jak tworzyć reaktywne źródła danych w Meteorze.
  • jak zaimplementować prosty przykład reaktywnego źródła danych.
  • jak zależności (Deps) mają się do AngularJS.
  • Rzadko zachodzi potrzeba napisania samemu kodu do śledzenia zależności, ale z pewnością warto zrozumieć ten mechanizm aby później móc śledzić sposób jego działania.

    Wyobraź sobie, że chcemy śledzić liczbę przyjaciół danego użytkownika Facebooka, którzy ‘polubili’ każdy post na Microscope. Przyjmijmy, że już zaimplementowaliśmy logowanie użytkownika za pomocą Facebooka, wywołaliśmy odpowiednie funkcje API i parsowaliśmy odpowienie dane. Mamy asynchroniczną funkcję po stronie klienta, która zwraca liczbę polubień posta, getFacebookLikeCount(user, url, callback).

    Ważną sprawą wartą zapamiętania jest, w jakim stopniu taka funkcja jest niereaktywna i w jakim stopniu nie jest czasu rzeczywistego. Funkcja wywołuje żądanie HTTP do Facebooka, odczyta trochę danych i udostępni je aplikacji w asynchronicznym callbacku. Nie wywoła się ponownie sama gdy liczba polubień ulegnie zmianie i nasze UI nie uaktualni się podczas zmiany danych.

    Aby to naprawić, możemy zacząć przez ustawienie odpowiedniego czasu setInterval, aby wołać funkcję co kilka sekund.

    currentLikeCount = 0;
    Meteor.setInterval(function() {
      var postId;
      if (Meteor.user() && postId = Session.get('currentPostId')) {
        getFacebookLikeCount(Meteor.user(), Posts.find(postId), 
          function(err, count) {
            if (!err)
              currentLikeCount = count;
          });
      }
    }, 5 * 1000);
    

    Za każdym razem, gdy sprawdzamy zmienną currentLikeCount, możemy oczekiwać na zwrócenie prawidłowej liczby z pięciosekundowym marginesem błędu. Możemy użyć tą zmienną w helperze jak poniżej:

    Template.postItem.likeCount = function() {
      return currentLikeCount;
    }
    

    Jednak nic nie wywołuje renderowania szablonu po zmianie currentLikeCount. Pomimo tego, że zmienna ta jest pseudo- czasu rzeczywistego, nie jest jeszcze reaktywna i nie może się w odpowiedni sposób komunikować z resztą ekosystemu Meteora.

    Śledzenie reaktywności: Obliczenia

    Reaktywność Meteora działa za pośrednictwem zależności, struktur danych, które śledzą zbiór komputacji.

    Jak widzieliśmy we wcześniejszym pasku bocznym reaktywności, komputacja jest blokiem kodu, który używa reaktywnych danych. W naszym przypadku istnieje komputacja pośrednio utworzona dla szablonu postItem. Każdy helper managera tego szablonu pracuję z tą komputacją.

    Możesz myśleć o komputacji, jako o sekcji kodu, która “troszczy się” o reaktywne dane. Gdy dane zmieniają się, to ta komputacja zostaje o tym poinformowana (przez invalidate()) i to ta komputacja decyduje o tym, czy wykonać jakąś akcję.

    Przemiana zwykłej zmiennej w funkcję reaktywną

    Aby zmienić naszą zmienną currentLikeCount w reaktywne źródło danych, musimy śledziź wszystkie komputacje, które używają jej w zależności. Wymaga to zmiany ze zmiennej na funkcję (która zwróci wartość):

    var _currentLikeCount = 0;
    var _currentLikeCountListeners = new Deps.Dependency();
    
    currentLikeCount = function() {
      _currentLikeCountListeners.depend();
      return _currentLikeCount;
    }
    
    Meteor.setInterval(function() {
      var postId;
      if (Meteor.user() && postId = Session.get('currentPostId')) {
        getFacebookLikeCount(Meteor.user(), Posts.find(postId), 
          function(err, count) {
            if (!err && count !== _currentLikeCount) {
              _currentLikeCount = count;
              _currentLikeCountListeners.changed();
            }
          });
      }
    }, 5 * 1000);
    

    Co osiągneliśmy to ustawienie zależności _currentLikeCountListeners, która śledzi wszystkie komputacje w której użyty był currentLikeCount(). Gdy wartość _currentLikeCountListeners ulega zmianie, wołamy funkcję changed() na tej zależności, która unieważnia wszystkie śledzone komputacje.

    Wtedy komputacje mogą kontynuować i reagować na zmiany odpowiednio do każdego przypadku. W przypadku komputacji szablonu oznacza to, że szablon się renderuje.

    Obliczenie szablonów i kontrola ich renderowania

    Główną przyczyną, dla której każdy szablon posiada własną komputację jest kontrola liczby odświeżania, która zachodzi na ekranie.

    Gdy wołamy szablon z innego szablonu, druga komputacja zostaje wstawiona w pierwszą. Zatem jeżeli reaktywne dane z wewnętrznego szablonu zmieniają się, ten wewnętrzny szablon jest odświeżony, natomiast zewnętrzny szablon pozostaje nienaruszony. W ten sposób komputacje kontrolują zakres zmian na reaktywne dane.

    Meteor wspomaga także trochę podstawowy mechanizm zagnieżdżania szablonów.

    Po pierwsze, helper {{#constant}} wyłącza reaktywność. Jakiekolwiek dane, które są zbierane przez helpery szablonu są używane tylko raz. Nawet jeżeli szablon go zawierający jest odświeżony, dane HTML stałego bloku zostają nienaruszone. Umożliwia to wykorzystanie stałych regionów do zarządzania widgetami pochodzących od osób trzecich, które nie spodziewają się odświeżenia DOM pod nimi.

    Po drugie, helper {{#isolate}} ustawia nową komputację w środku szablonu. Innymi słowy ma taki sam efekt jak zagnieżdżony szablon.

    Zatem jeżeli jeden z reaktywnych źródeł danych będący w obrębie bloku isolate zmieni się, izolowany obszer ulegnie odświeżeniu, ale reszta zawierającego go szablonu nie. Jednakże gdy szablon zewnętrzny się odświeży, odświeży się także izolowany obszar.

    Porównianie zależności do Angular

    Angular jest reaktywną biblioteką działającą po stronie klienta, rozwijaną przez dobrych ludzi z Google. Zilustrujemy podejście zastosowane w Meteorze do śledzenia zależności w Angular, jako że dwa rozwiązania bardzo różnią się od siebie.

    Widzieliśmy, że model Meteora używa bloków kodu nazywanych komputacjami. Komputaje te są śledzone przez specjalne reaktywne źródła danych (funkcje), które troszczą się o unieważnienie ich gdy zachodzi taka potrzeba. Zatem źródło danych jasno informuje wszystkie swoje zależności, gdy potrzebują zawołać invalidate(). Zauważ, że ogólnie dzieje się to, gdy dane uległy zmianie, ale można być także wywołać unieważnienie z innych powodów.

    Dodatkowo, pomimo tego, że komputacje są ponownie uruchamiane podczas unieważnienia, można ustawić je tak, aby zachowywały się w dowolny sposób. Daje to wysoki poziom kontroli nad reaktywnością.

    W Angular, reaktywność jest zarządzana przez obiekt scope. Można o nim myśleć jako o zwykłym obiekcie JavaScript z kilkoma specjalnymi metodami.

    Jeżeli chcesz reaktywnie zależeć na zmiennej w scope, wywołujesz scope.$watch, dostarczając wyrażenia które chcesz monitorować (t.j. na której części scope Ci zależy) i funkcję obserwująca (listener), która ma być wywoływana po każdej zmianie wyrażenia. Możesz zatem otwarcie zdefiniować co zrobić za każdą zmianą wartości wyrażenia.

    Wracając do przykładu Facebooka, napisalibyśmy:

    $rootScope.$watch('currentLikeCount', function(likeCount) {
      console.log('Current like count is ' + likeCount);
    });
    

    Oczywiście tak rzadko, jak ustawia się komputacje w Meteorze, zarówno w Angular nie woła się często $watch ponieważ instrukcje ng-model i {{expressions}} automatycznie ustawiają obserwatorów, którzy troszczą się o odświeżanie po każdej zmianie.

    Po zmianie takiej reaktywnej zmiennej, musi być zawołana scope.$apply(). Funkcja ta przechodzi przez każdego obserwatora scope ale wywołuje wyłącznie funkcje obserwujące obserwatorów, których zmieniła się wartość wyrażenia.

    Zatem scope.$apply() jest podobne do dependency.changed(), z tym wyjątkiem, że działa z poziomu scope raczej niż daje pełną kontrolę nad tym, które funkcje oberwujące mają być przeliczone. Mówiąc o tym, ten lekki brak całkowitej kontroli daje Angular możliwość bycia sprytnym i wydajnym w sposobie w jakim ustalone zostaje które funkcje obserwujące mają być przeliczone.

    Z Angular, kod naszej funkcjigetFacebookLikeCount() wyglądałby mniej więcej tak:

    Meteor.setInterval(function() {
      getFacebookLikeCount(Meteor.user(), Posts.find(postId), 
        function(err, count) {
          if (!err) {
            $rootScope.currentLikeCount = count;
            $rootScope.$apply();
          }
        });
    }, 5 * 1000);
    

    Trzeba przyznać, że Meteor troszczy się o szczegóły za nas i pozwala na czerpanie pełnych korzyści z reaktywności bez wkładania w to większego wysiłku. Mamy nadzieję, że nauka o tym okaże się pomocna, gdy zechcesz rozwinąć aplikację i pchnąć ją poza granice obecnej implementacji.