ANGULAR 2 CHANGE DETECTOR – mechanizmy detekcji oraz strategia onPush

Twórcy Angulara tym razem stanęli na wysokości zadania i stworzyli przemyślany system śledzenia zmian w komponentach. Po cyklu $digest z Angulara 1.x nie został nawet ślad. W tym artykule omówię jak działa system detekcji w komponentach oraz jak możemy zoptymalizować detekcję za pomocą strategii onPush.

CHANGE DETECTOR KOMPONENTU, CO TO JEST I KIEDY SIĘ URUCHAMIA?

Na początek nieco suchej teorii.

  • każdy komponent ma swój Change Detector (w dalszej części artykułu będę używać skrótu CD). Change Detector jest odpowiedzialny za sprawdzanie bindingów w templatce komponentu, bindingi to np. {{ name }}, [name]=”user.name”
  • równolegle z drzewem komponentów biegnie drzewo CD
  • w przypadku uruchomienia CD w którymś komponencie, Angular odpala wszystkie CD dla całego drzewa komponentów, Angular jest w stanie sprawdzić kilkaset tysięcy bindingów w kilkadziesiąt milisekund!
  • CD uruchamiane są zawsze w jednym kierunku – zawsze od góry w dół, zgodnie z drzewem komponentów, czyli najpierw jest sprawdzany parentComponent a następnie childComponent. Dlatego też dane płyną z góry w dół.
  • klasa CD dla danego komponentu jest tworzona automatycznie podczas runtime’u

Co wywołuje uruchomienie się mechanizmu detekcji w komponencie? Generalnie wszystkie asynchroniczne operacje:

  • calle XHR, np:

  • eventy DOM: kliknięcie na buttona, zatwierdzenia formularza,  keyup, onmouseover etc.
  • użycie funkcji setTimeout(), setInterval()

DRZEWO CHANGE DETECTORÓW

Jak wspomniałem, drzewo CD biegnie równolegle do drzewa komponentów. Spójrzmy jak to wygląda w interpretacji graficznej:

 

Jak widzimy, każdy komponent ma swój CD. Załóżmy, że w najniżej położonym komponencie, OrderCarComponent, wystąpi event, który zawoła nam system detekcji, tak będzie wyglądać propagacja:

 

Angular sprawdził bindingi wszystkich komponentów począwszy od Roota, w kolejności first-depth-order (numerki w niebieskich rombach oznaczają kolejność wywołania się kolejnych CD).

Angular nie wie co się zmieniło w danym komponencie, wie tylko, że jak pojawił się event w którymś komponencie (np. click), jest sygnał, że coś się zmieniło i jest to czas aby uruchomić system detekcji. Sprawdzenie następuje wyłącznie raz.

Nasuwa się teraz pytanie, kto powiadamia Angulara, że właśnie w tym momencie, ma nastąpić update widoku? Odpowiada za to Zones, a dokładniej ngZone. Zones to odrębny, duży temat, którego nie będę poruszać w tym artykule.

CHANGE DETECTION STRATEGY – ZMIENIAMY DOMYŚLNĄ STRATEGIĘ

Zajrzyjmy do API komponentu, w dokumentacji Angulara:
https://angular.io/docs/ts/latest/api/core/index/Component-decorator.html

Jak widzimy, dekorator @Component posiada meta-data property „changeDetection”.

Domyślna strategia changeDetection dla @Component, która jest już automatycznie ustawiona to:

Skorzystajmy z tego property, aby wykonać zmianę domyślnej detekcji:

 

Co powoduje onPush?

  • informuje Angulara, że nasz komponent zależy tylko od Inputów
  • obiekt przekazany do Inputa uważamy za niemutowalny (immutable).
  • Angular ominie całe subtree changeDetectorów, jeśli inputy w komponencie się nie zmieniły

Należy, pamiętać, że w przypadku użycia strategii onPush, Angular traktuje nasze dane jako niemutowalne, także próba wywołania poniższego kodu:

Nie przyniesie żadnego efektu. Nasz binding {{ car.name }}, nie zostanie odświeżony.

Zobaczymy, jak będzie wyglądać nasze drzewo changeDetectorów, po użyciu strategii onPush na komponencie np. TrucksComponent, który zależałby tylko od @Input() trucks:

 

Jak widać, nasz system detekcji nie sprawdził w ogóle bindingów w prawym poddrzewie.

Podsumowując strategię onPush:

  • poprawia performance naszej aplikacji, w przypadku gdy operujemy na immutable data
  • standardowo stosujemy go do komponentów bez logiki, które mają wyłącznie @Input() i służą po prostu do wyświetlania danych.
  • setTimeout() w komponencie nie wywoła mechanizmu CD!
  • change detector dla komponentu z onPush zostanie uruchomiony tylko w 3 przypadkach:
  1. Gdy zmieni się wartość Input w komponencie
  2. W templatce zostanie wyemitowany event
  3.  Observable w komponencie odpali event

MANUALNE STEROWANIE SYSTEMEM DETEKCJI KOMPONENTU

Parę linijek wyżej wspomniałem, że mutowanie danych w komponencie z OnPush oraz użycie setTimeout() nie uruchomi detekcji. Jak zwykle istnieje obejście, możemy wywołać detekcję w dowolnym momencie.

Aby ręcznie manipulować detekcją, zaczynamy od wstrzyknięcia klasy ChangeDetectorRef do konstruktora komponentu:

Uzyskaliśmy bezpośredni dostęp do ChangeDetectora komponentu. Co możemy teraz zrobić?

1. Odłączyć CD komponentu z drzewa Change Detectorów poprzez użycie metody detach(). Tym niemniej, mechanizm detekcji nadal działa dla tego komponentu!, po prostu nie jest uwzględniony w drzewie CD.

 

2. Wywołać detekcję w wybranym przez nas momencie, lub np. odświeżać komponent regularnie co określony czas, poprzez markForCheck():

Użycie markForCheck(), uruchamia system detekcji od Roota do naszego komponentu, wywołuje po drodze napotkane Change Detectory, ale wyłącznie na naszej ścieżce do komponentu, tzn:

 

3. Ponownie przypiąć CD do drzewa CD poprzez reattach(), np. mamy dane, które płyną na żywo lub nie. W tym przypadku możemy odpinać i przypinać changeDetector, w zależności od potrzeby:

STATUS CHANGE DETECTORA

Change detector komponentu posiada sześć możliwych statusów, które są ENUMAMI:

  • CheckOnce = 0 – ten status oznacza, że po zawołaniu detectChanges, status detektora przeskoczy na Checked.
  • Checked  = 1 – CD powinien być pomijany, aż jego status nie wróci do CheckOnce
  • CheckAlways = 2 – default status, change detector odpala się zawsze
  • Detached = 3 – CD i jego subdrzewo CD, nie jest już częścią głównego drzewa i powinno być pomijane
  • Errored = 4 – CD napotkał błędy sprawdzając bindingi. Detektor o tym statusie nie sprawdza już dłużej zmian.
  • Destroyed = 5 – po prostu znaczy, że CD został zniszczony

Status CD danego komponentu może się zmienić z powodu różnych czynników (np errorów, lub zmiany strategii).

Sprawdźmy teraz poprzez console.log, co trzyma ChangeDetectionStrategy:

Screen loga:

Żadnej magii! ChangeDetectionStrategy operuje wyłącznie na enumach CheckOnce i Checked. Wniosek:

OnPush znaczy, że change detector status zostaje przestawiony na enum CheckOnce z CheckAlways.

PODSUMOWANIE

Mam nadzieję, że ten artykuł objaśnił, jak angular aktualizuje widoki komponentów. Mechanizm detekcji w Angularze 2 w stosunku do Angulara 1.x to niebo a ziemia a użycie immutable data w aplikacji oraz strategii onPush, istotnie wpłynie na szybkość działania naszej aplikacji.

5 Comments

  1. Pingback: Angular i zone.js – Angular.love

Dodaj komentarz

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