Angular i zone.js

Już ponad dobry rok temu, w artykule o systemie detekcji:

ANGULAR 2 CHANGE DETECTOR – mechanizmy detekcji oraz strategia onPush

napisałem:

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.

Zatem nadszedł czas, aby poruszyć temat ngZone oraz wykorzystać świadomość jego działania, do poprawy wydajności aplikacji.

Execution Context (EC)

Zanim przejdę do meritum, parę słów o EC w JavaScript.

W JS rozróżniamy trzy konteksty egzekucji naszego kodu:

  • globalny kontekst (Global Execution Context – GEC) – przykładowo, jeśli stworzymy plik JS i napiszemy:

A następnie uruchomimy nasz kod w przeglądarce, to pod this będzie kryć się obiekt window, który w tym przypadku jest naszym globalnym kontekstem. GEC jest ściśle związany ze środowiskiem, w którym uruchamiamy nasz kod oraz jest zawsze jeden.

 

  • kontekst w funkcji (Functional Execution Context) – kontekst tworzony poprzez wykonanie kodu wewnątrz funkcji. Każda funkcja ma swój FEC i takich kontekstów może mieć wiele. Gdy silnik JS, podczas wykonywania kodu zdefiniowanego w GEC, znajduje zawołanie funkcji, tworzy dla niej nowy FEC.

 

  • kontekst wewnątrz funkcji Eval (nie tykać! ;))- tzw calling context pochodzący z funkcji w której eval zostaje zawołany. Generalnie mało istotne dla nas bo używanie funkcji eval bez odpowiedniej świadomości, może być niebezpieczne.

Wszystkie aktywne konteksty układają się w stos  (Execution Context Stack), a na samym dole stosu ląduje GEC, a kolejno FEC.

Zones

Jeśli zajrzysz do pliku polyfills.ts w projekcie angularowym, stworzonym przez Angular CLI, znajdziesz następującą linijkę:

Zone jest to execution context  (EC) dla operacji asynchronicznych (np. pobranie danych z serwera) i jest bardzo przydatny w debuggingu, testowaniu i profilingu. Zone.js możesz używać w dowolnej aplikacji, np. pisanej w VanillaJS.

Gdy uruchamiamy nasz kod w  Zone EC, jesteśmy w stanie wyłapać moment rozpoczęcia i zakończenia asynchronicznych operacji (wejście i wyjście z Zone).

Nie będę omawiać jak korzystać z biblioteki Zone.js. Polecam zagłębić temat, oglądając prelekcję Briana Forda na Ng-Conf 2014.

Zone w Angular – ngZone

Jak wspomniałem, Zone.js pozwala nam śledzić momenty wejścia i wyjścia z kontekstu wykonania kodu asynchronicznego. Własnie dzięki Zone, Angular wie kiedy ma uruchomić wykrywanie zmian (Change Detection). Angulara nie obchodzi co się zmieniło – ważne, że dostaje powiadomienie od Zone, że operacje asynchroniczne się zakończyły i być może warto odświeżyć widok użytkownikowi (np. po zaciągnięciu listy produktów z serwera).

Angular na swoje potrzeby stworzył rozgałęzienie zone.js, w postaci ngZone.
Co ważne, NgZone nie jest częścią implementacji systemu detekcji, on tylko powiadamia, że system detekcji ma się uruchomić. Ba, możemy nawet usunąć z aplikacji angularowej Zones (pokażę pod koniec artykułu) a system detekcji uruchamiać manualnie poprzez tick().

Wykorzystanie ngZone

Wiemy już do czego służą Zones w Angular. Jesteśmy również świadomi, że system detekcji jest uruchamiany za każdym razem, jak zachodzą operacje asynchroniczne (np. calle XHR, eventy, powiadomienia ze strumieni).

Moim ulubionym przykładem wykorzystania NgZone, jest optymalizacja aplikacji, która nasłuchuje na event scroll przeglądarki.

Stwórzmy prosty przykład – jeśli scroll przekroczy 100px, to header ma zwiększyć swoją wysokość. Jednocześnie do bindingu wrzucę Math.random(), aby sprawdzać, czy binding jest odswieżany podczas scrolla.

Link do przykładu:
https://stackblitz.com/edit/angular-dxaxhh?file=app/app.component.ts

Za każdym razem jak scrollujemy, wykonuje się następujący scenariusz:

  • event ‚scroll’ powoduje uruchomienie się systemu detekcji w komponencie
  • System detekcji wykonuje następujące operacje w konkretnej kolejności:
  1. aktualizuje data-bounds w child-components
  2. woła hooki OnChanges (tylko jeśli input się zmienił),  OnInit (tylko pierwsze sprawdzenie)  i DoCheck (zawsze) w child-components
  3. aktualizuje data-bounds w aktualnym komponencie
  4. uruchamia system detekcji w child-components (wszystkie kroki lecą od nowa, tym razem dla dziecka)
  5. woła hook AfterViewInit (tylko podczas pierwszego sprawdzenia) we wszystkich child-components
  6. … oraz wykonuje się jeszcze parę innych operacji po drodze (np. jeśli skorzystaliśmy z content-projection: <ng-content></ng-content>)

Angular jest bardzo szybki, ale zapewne rozum Ci podpowiada, że jest to nieco bez sensu, prawda? Przecież chcemy zmieniać tylko kolor i wysokość headera, a nie wykonywać setki razy tyle operacji za każdym scrollem usera.

Tutaj z pomocą przychodzi właśnie NgZone, który pozwoli nam wykonać dany kod poza Angular Zone, co doprowadzi do tego, że system detekcji się nie uruchomi.

Wystarczy wstrzyknąć NgZone do konstruktora:

I następnie przekazać funkcję do metody runOutsideAngular(function), która uruchomi się  synchronicznie poza kontekstem Zone:

Od tego momentu, podczas scrolla w naszym przykładzie, bindingi nie są już odswieżane, a efekt z headerem nadal działa:

https://stackblitz.com/edit/angular-6r6sm6?file=app/app.component.ts

Możemy również w dowolnym momencie w tasku, który jest uruchomiony poza Angular Zone, wpiąć się znowu w Angular Zone, za pomocą metody run():

Angular całkowicie bez ngZone

Jeśli lubisz eksperymentować, możesz w ogóle wyłączyć Zones i dalej korzystać z Angulara, ale może to być nieco uciążliwe… ale spróbujmy! Przechodzimy do pliku main.ts, ustawiamy wartość ngZone na noop:

Następnie w komponencie wstrzykujemy ApplicationRef, i wołamy metodę tick:

Tick() woła jawnie system detekcji, a w development mode uruchamia system detekcji dwukrotnie, aby upewnić się, że w czasie pierwszej rundy, nie pojawiły się żadne nowe zmiany. Jeśli jakieś zmiany jednak się pojawiły w czasie drugiej rundy systemu detekcji, Angular rzuci nam error, ponieważ aplikacja Angulara może mieć tylko jedną rundę systemu detekcji, w której wszystkie Change Detectory się skompletują.

Przykład: https://stackblitz.com/edit/angular-bof16j?file=app%2Fapp.component.ts

Podsumowanie

Zapamiętajmy, że Zone jest kontekstem wykonania asynchronicznego kodu. Zone pozwala na tracking asynchronicznych operacji, dzięki czemu Angular wie, że ma uruchomić system detekcji i zrobić update bindingów. Metodę runOutsideAngular wykorzystujmy zawsze, gdy wielokrotnie wykonujemy powtarzającą się asynchronicznie operację, po której nie mamy potrzeby odświeżania UI.

Dodaj komentarz

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