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:
1 2 3 4 5 |
getHeroes(): Observable<Hero[]> { return this.http.get('api/heroes') .map(this.extractData) .catch(this.handleError); } |
- 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:
1 |
changeDetection: ChangeDetectionStrategy.default |
Skorzystajmy z tego property, aby wykonać zmianę domyślnej detekcji:
1 2 3 4 5 6 7 |
@Component({ ... changeDetection: ChangeDetectionStrategy.OnPush }) export class CarComponent { @Input() car; } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 |
@Component({ ... changeDetection: ChangeDetectionStrategy.OnPush, template: `<p> {{ car.name }} </p><button (click)="changeName()">Change name</button>` }) export class CarComponent { @Input() car; changeName() : void { this.car.name = 'Mazda Rx'; } } |
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:
- Gdy zmieni się wartość Input w komponencie
- W templatce zostanie wyemitowany event
- 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:
1 2 3 4 5 6 7 8 |
@Component({ ... changeDetection: ChangeDetectionStrategy.OnPush }) export class myComponent { constructor(private changeDetector : ChangeDetectorRef) {} } |
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.
1 2 3 |
constructor(private changeDetector : ChangeDetectorRef) { this.changeDetector.detach() } |
2. Wywołać detekcję w wybranym przez nas momencie, lub np. odświeżać komponent regularnie co określony czas, poprzez markForCheck():
1 2 3 |
constructor(private changeDetector : ChangeDetectorRef) { setInterval(() => this.changeDetector.markForCheck() }, 1500); } |
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:
1 2 3 |
set live(isLive : boolean) : void { isLive ? this.changeDetector.reattach() : this.changeDetector.detach(); } |
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:
1 2 3 4 5 6 7 8 9 10 |
@Component({ ... changeDetection: ChangeDetectionStrategy.OnPush }) export class myComponent { constructor() { console.log('CDS', 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.
Fajny artykuł, nie mniej w połowię musiałem iść kawy zrobić.. serio ludziom trzeba tak proste rzeczy tłumaczyć i rysować?
Cześć, dzięki za opinię! wg mnie, graficzne zobrazowanie problemu przyśpiesza zrozumienie treści, nie trzeba się tak skupiać na słowie pisanym.
Co za głupi komentarz. Zdajesz sobie sprawę, że ludzie mają różny poziom wiedzy i doświadczenia? :/
Pingback: Angular i zone.js – Angular.love
Super informacje. Czekam na artykuł o NgZone!
http://www.angular.love/2018/03/04/angular-i-zone-js/
Pingback: Rozmowa o pracę na Angular Developera - jakich pytań możesz się spodziewać? - No Fluff Jobs - blog
Pingback: Angular performance tips – Angular.love
Super, dzięki!
Słuchałem i czytałem już trochę na ten temat i ten artykuł jest na prawdę klarowny! 🙂
Bardzo fajny artykuł – dzięki, a jednocześnie przestrzegam wszystkich przed tzw. “mikro-optymalizacjami” tzn. przeznaczaniem czasu i energii (i pieniędzy klienta…) na przyśpieszanie elementów finalnie którego końcowy user praktycznie i tak nie odczuje.
Tym co piszą że to dla nich proste dedykuje ksiażkę: Zapomniał wół jak cielęciem był.
“Nie przyniesie żadnego efektu. Nasz binding {{ car.name }}, nie zostanie odświeżony.”
Podany przykład jest błędny.
Sam niżej napisałeś, że “W templatce zostanie wyemitowany event” co jest prawdą, template zostanie odświeżony.