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:
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:
changeDetection: ChangeDetectionStrategy.default
Skorzystajmy z tego property, aby wykonać zmianę domyślnej detekcji:
@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:
@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:
@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.
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():
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:
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:
@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.