Implementacja sygnałów do wersji 16 Angulara to kolejna przełomowa zmiana wprowadzona w ostatnim czasie. Wpływa na wiele kluczowych aspektów takich jak przepływ danych, mechanizm detekcji zmian, cykl życia komponentów czy użycie reaktywnych wartości. Warto zatem bliżej przyjrzeć się nadchodzącym zmianom, ponieważ z pewnością będą miały wpływ na proces tworzenia aplikacji.
Czym są sygnały?
Sygnały to Reactive Primitives, oparte na wzorcu zwanym Observer Design Pattern. Według tego wzorca mamy do czynienia z Publisherem, który przechowuje jakąś wartość wraz z listą subskrybentów (Subscribers), którzy są nią zainteresowani, a w momencie zmiany tej wartości otrzymują o tym powiadomienie.
Mówiąc prościej sygnał “opakowuje” pewną wartość, dając możliwość reagowania na jej zmiany. Mechanizm ten jest dobrze znany w Angularze za przyczyną RxJS, który udostępnia nam kilka rodzajów Subjectów, a najlepszą analogią jest BehaviorSubject, który tak jak sygnał, ma wartość początkową.
Celem, który przyświecał nowej wersji, było udoskonalenie mechanizmu detekcji zmian. Dlaczego spośród wielu rozważanych opcji, takich jak usprawnienie Zone.js, wprowadzenie API w stylu “setState”, czy właśnie wykorzystanie RxJS, zdecydowano się akurat na sygnały? Oto kilka powodów:
- Angular może śledzić, które sygnały są odczytywane w widoku, co daje informację które komponenty wymagają odświeżenia ze względu na zmianę stanu
- możliwość synchronicznego odczytania wartości, która jest zawsze dostępna
- odczytywanie wartości nie wywołuje dodatkowych efektów (side effects)
- brak glitchy, które powodowałyby niespójność odczytywanego stanu
- automatyczne i dynamiczne śledzenie zależności, a co za tym idzie brak jawnych subskrypcji, co zwalnia nas z konieczności zarządzania nimi, aby uniknąć wycieków pamięci
- możliwość korzystania z nich poza komponentami, co świetnie współgra z Dependency Injection
- możliwość pisania kodu w deklaratywny sposób
Jak z nich korzystać?
Skoro poznaliśmy już powody, dla których wprowadzono sygnały oraz mechanizm ich działania, czas przyjrzeć się bliżej API, które pozwala na korzystanie z nich.
W Angularze sygnał jest reprezentowany przez interfejs z
- funkcją
getter
, której wywołanie zwraca aktualną wartość oraz - symbolem
SIGNAL
pozwalający na jego rozpoznanie przez framework
Odczytanie wartości jest rejestrowane i służy do budowy grafu zależności pomiędzy sygnałami.
1 2 3 4 |
interface Signal<T> { (): T; [SIGNAL]: unkown; } |
Co do zasady sygnały służą tylko do odczytu – chcemy dostać aktualną wartość i śledzić jej zmiany. Angular jednak udostępnia nam interfejs, który pozwala na zmianę wartości za pomocą wbudowanych metod:
1 2 3 4 5 6 |
interface WritableSignal<T> extends Signal<T> { set(value: T): void; update(updateFn: (value: T) => T): void; mutate(mutatorFn: (value: T) => void): void; asReadonly(): Signal<T>; } |
Metoda set służy do zmiany aktualnie przechowywanej wartości. Korzystając z analogii do BehaviorSubject, jest to tożsame z wywołaniem metody next.
Metoda update służy do wprowadzenia nowej wartości utworzonej na podstawie tej aktualnie przechowywanej. Z kolei mutate bezpośrednio modyfikuje aktualną wartość. Jest przydatna w sytuacji, gdy chcemy zmodyfikować tablicę lub obiekt bez zmiany referencji (np. dodać element do tablicy korzystając z Array.prototype.push
).
Wywołanie asReadonly zwraca nowy sygnał, który przechowuje tą samą wartość, ale nie pozwala na jej modyfikację. Wracając do analogii z Subjectem, byłoby to skorzystanie z metody asObservable.
Do utworzenia instancji takiego sygnału służy funkcja:
1 2 3 4 |
function signal<T>( initialValue: T, options?: { equal?: (a: T, b: T) => boolean } ): WritableSignal<T> |
Funkcja equal
pozwala zdefiniować użytkownikowi kiedy dwie wartości (z praktycznego punktu widzenia najbardziej przydatne dla dwóch obiektów) są równe. Domyślna funkcja korzysta z operatora porównania “===” z tym, że obiekty i tablice nigdy nie są równe, co pozwala przechowywanie wartości tych typów i powiadamianie o ich zmianie.
Przykład użycia metod interfejsu WritableSignal:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
interface User { id: string; name: string; age: number; } @Injectable() export class UserService { private _users = signal<User[]>([]); users = this._users.asReadonly(); addUser(newUser: User): void { // bezpośrednia modyfikacja aktualnej wartości this._users.mutate(users => users.push(newUser)); } removeUser(id: string): void { // ustawienie nowej wartości utworzonej na podstawie aktualnej this._users.update(users => users.filter(user => user.id !== id)); } getUser(id: string): User | null { return this._users().find(user => user.id === id) ?? null; } resetUsers(): void { // ustawienie nowej wartości, zastępując dotychczasową this._users.set([]); } } |
Jednym z najczęściej wykorzystywanych operatorów RxJS jest map, który pozwala na stworzenie nowej wartości, zależnej od źródła. W przypadku sygnałów służy do tego funkcja computed
:
1 2 3 4 |
function computed<T>( computation: () => T, options?: {equal?: (a: T, b: T) => boolean} ): Signal<T>; |
Tak stworzony sygnał jest zależny od tych, których wartość odczytujemy wewnątrz funkcji computation
, co oznacza, że jego wartość zmieni się tylko, gdy przynajmniej jedna z zależności zmieni swoją wartość. Funkcja computation
nie może powodować side-effectów, czyli wewnątrz niej możemy dokonywać tylko operacji odczytu. Sprawdzenie czy nowa wartość jest taka sama jak poprzednia działa na tej samej zasadzie, co w przypadku funkcji signal
. Podobnie możemy również przekazać funkcję equal
, która nadpisze standardowy sposób porównania. Pozwala to na poprawienie wydajności poprzez ograniczenie liczby komputacji.
1 2 3 4 5 6 7 8 9 |
const user = signal<User>({ id: ‘70a65491c1d6’, name: ‘John’, age: 35 }); const isAdult = computed(() => user().age >= 18)); //Signal<boolean> const color = computed(() => isAdult() ? ‘green’ : ‘red’); //Signal<’green’ | ‘red’> // isAdult jest obliczane ponownie, ponieważ jego zależność się zmieniła</span> // color nie musi być obliczany, ponieważ isAdult nie zmienia wartości</span> user.set({ id: ‘c37de3232c4d’, name: ‘Andy’, age: 22 });</span> |
Sygnał stworzony za pomocą funkcji computed
dynamicznie śledzi wartości sygnałów, których wartość została odczytana podczas jego ostatniej komputacji.
1 |
const greeting = computed(() => showName() ? `Hello, ${name()}!` : 'Hello!'); |
Sygnał greeting zawsze będzie śledził wartość showName, ale jeżeli wartość showName będzie równa false, nie będzie śledził zmian sygnału name. W takim przypadku zmiana wartości name nie spowoduje zmiany wartości greeting.
W wielu przypadkach przydatne jest wywołanie tzw. side-effect, czyli wywołanie kodu, który zmienia stan poza swoim lokalnym kontekstem, takich jak np. wysłanie żądania http czy synchronizacja dwóch niezależnych modeli danych. Odwołując się do RxJS mamy operator tap
, a w przypadku sygnałów służy do tego funkcja effect
:
1 2 3 4 |
function effect( effectFn: (onCleanup: (fn: () => void) => void) => void, options?: CreateEffectOptions ): EffectRef; |
Zarejestrowana w ten sposób funkcja effectFn
odczytuje wartości sygnałów i jest wykonywana za każdym razem, kiedy któryś z nich zmieni swoją wartość. Jako jej argument można opcjonalnie zarejestrować funkcję “czyszczącą”, która jest wywoływana przed kolejnym wykonaniem effectFn
i ma możliwość anulowania działania rozpoczętego przy poprzednim wywołaniu.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
effect((onCleanup) => { const countValue = this.count(); let secsFromChange = 0; const logInterval = setInterval(() => { console.log( `${countValue} had its value unchanged for ${++secsFromChange} seconds` ); }, 1000); onCleanup(() => { console.log('Clearing and re-scheduling effect'); clearInterval(logInterval); }); }); |
Moment wykonania efektu nie jest ściśle zdefiniowany i zależy od strategii przyjętej przez Angulara, jednak przy pracy z nimi możemy być pewni pewnych pryncypialnych zasad:
- efekt zostanie wywołany przynajmniej raz
- efekt zostanie wywołany po tym jak przynajmniej jeden z sygnałów, od którego jest zależny (czyli odczytuje jego wartość) ulegnie zmianie
- efekt zostanie wywołany minimalną ilość razy, co oznacza, że jeżeli kilka sygnałów, od których efekt jest zależny zmieni swoją wartość w jednym momencie, kod zostanie wykonany tylko raz
Z racji tego że efekt reaguje na zmianę sygnału, od którego zależy, pozostaje zawsze aktywny i gotowy do reakcji na zmiany. Domyślnie jego anulowanie odbywa się automatycznie. W przypadku ustawienia opcji manualCleanup
efekt pozostanie aktywny po zniszczeniu komponentu czy dyrektywy. Aby anulować go ręcznie możemy skorzystać z instancji EffectRef
.
1 2 3 |
const effectRef = effect(() => {...}, { manualCleanup: true }); … effectRef.destroy(); |
Zmiana wartości sygnału wewnątrz efektu jest jest rekomendowane i może prowadzić do niespodziewanych błędów, dlatego domyślnie jest traktowane jako błąd. Aby to zmienić należy ustawić pole allowSignalWrites
w obiekcie options.
Sygnałowe komponenty
Uwaga: Sygnałowe komponenty zostały przedstawione i opisane w RFC dotyczącym sygnałów, jednak nie są jeszcze dostępne w obecnej wersji, a obecne API oraz działanie może jeszcze ulec zmianie.
Na wstępie należy zaznaczyć, że opisane w tym rozdziale funkcjonalności działają zarówno w komponentach jak i dyrektywach, których dla uproszczenia nie będę wymieniał w tekście.
Aby korzystać z sygnałów i powiązanych z nimi funkcjonalności w komponentach oraz korzystać z nowego mechanizmu detekcji zmian (o czym szerzej za chwilę) należy ustawić opcję signals
w dekoratorze @Component:
1 2 3 4 |
@Component({ signals: true, … }) |
Wartość false będzie oznaczała, że korzystamy z dotychczasowego podejścia oraz detekcji zmian opartej na Zone.js. Nie wyklucza to jednak korzystania z obu typów komponentów i ich współistnienia w obrębie jednej aplikacji.
Aby skorzystać z wartości przechowywanej przez sygnał w widoku należy wywołać funkcję getter zwracaną przez signal
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@Component({ signals: true, selector: ‘counter’, template: ` <p>Counter: {{ count() }}</p> <button (click)=”increment()”>+1</button> <button (click)=”decrement()”>-1</button>` }) export class CounterComponent { count = signal(0); increment(): void { this.count.update(value => value + 1); } decrement(): void { this.count.update(value => value - 1); } } |
Dotychczas należało unikać wywoływania funkcji w templacie, ponieważ zwracana wartość była obliczana na nowo przy każdej detekcji zmian, co mogło prowadzić do problemów z wydajnością. W tym przypadku nie jest to dłużej problemem – widok zostanie odświeżony po wykryciu zmiany wartości sygnału.
Dotyczasowe dekorator @Input zastąpiono funkcją input
, która zwraca Signal (czyli tylko do odczytu) przechowujący najnowszą zbindowaną wartość. Jako argumenty przyjmuje ona domyślną wartość oraz obiekt opcji. Jeżeli w komponencie stworzymy efekt, który korzysta z wartości inputu nie zostanie on wykonany, dopóki ta nie będzie dostępna.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Component({ signals: true, selector: ‘user-card’, template: ` <div class=”user-card” [NgClass]=”{ user-card–disabled: disabled() }”> <p>Name: {{ name() }}</p> <p>Role: {{ role() }}</p> </div>` export class UserCardComponent { name = input<string>(); // Signal<string | undefined> role = input(‘customer’); //Signal<string> disabled = input<boolean>(false, { alias: ‘deactivated’ }); //Signal<boolean> } |
Nowym rodzajem inputu, który jest dostępny w tego rodzaju komponentach jest model, który zwraca WritableSignal
, a więc daje możliwość zmiany jego wartości, która propaguje z powrotem do rodzica, zmieniając wartość sygnału, którego referencja została do niego przekazana, tworząc swego rodzaju two-way binding:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@Component({ signals: true, selector: ‘counter’ template: ` <p>Counter: {{ count() }}</p <button (click)=”increment()”>+1</button>` }) export class CounterComponent { count = model(0); //WritableSignal<number> increment(): void { this.count.update(value => value + 1); } } @Component({ signals: true, selector: ‘widget’ template: ` <counter [(count)]=”value” />` )} export class WidgetComponent { value = signal(10); } |
Skoro mamy nowy input, zmianie uległ także output. Wprowadzenie sygnałów nie zmienia sposobu w jaki działają, jednak aby zachować spójność API w miejsce dekoratora @Output dostajemy funkcję output
, która zwraca EventEmmiter
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@Component({ signals: true, selector: ‘user-card’, template: ` <p>{{ user().name }}</p> <button (click)=”edit()”>Edit</button> <button (click)=”remove()”>Remove</button>` }) export class UserCardComponent { user = input<User>(); edit = output<User>(); //EventEmitter<User> remove = output<string>({ alias: ‘delete’ }) //EventEmitter<string> edit(): void { this.edit.emit(this.user()); } remove(): void { this.remove.emit(this.user().id); } } |
Również dekoratory @ViewChild
, @ViewChildren
, @ContentChild
, @ContentChildren
tworzące query elementu(ów) z template zamieniają się na odpowiednie funkcje zwracające Signal:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Component({ signals: true, selector: ‘form-field’, template: ` <field-icon [icon]=”someIcon” /> <field-icon [icon]=someAnotherIcon” /> <input #inputRef />` }) export class FormFieldComponent { icons = viewChildren(FieldIconComponent); //Signal<FieldIconComponent[]> input = viewChild<ElementRef>(‘inputRef’); //Signal<ElementRef> eventHandler(): void { this.input().nativeElement.focus(); } } |
Ze względu na nowy mechanizm detekcji zmian, zmiany dotykają również obszaru lifecycle hooków. Nie są to dłużej metody klasy komponentu, których implementacji wymaga od nas interfejs a funkcje, które jako argument przyjmują callbacki wykonujące się w odpowiednim momencie. Ich aktywowanie polega na wywołaniu odpowiedniej funkcji wewnątrz konstruktora lub innej metody komponentu. Wprowadzono również trzy nowe hooki, które mają wykonywać kod po operacji renderowania widoku:
1 |
function afterNextRender(fn: () => void): void; |
Przekazana funkcja zostanie wykonana po zakończeniu kolejnego cyklu detekcji zmian. Przydatna w przypadku ręcznego odczytywania z lub wpisywania do DOM.
1 |
function afterRender(fn: () => void): { destroy(): void }; |
Przekazana funkcja zostanie wykonana po każdej aktualizacji DOM podczas renderowania.
1 |
function afterRenderEffect(fn: () => void): { destroy(): void }; |
To specjalny rodzaj efektu, który wyzwolony (poprzez zmianę w sygnale z którego odczytuje wartość) zostaje wykonany w tym samym czasie co afterRender.
Z zestawu dotychczasowych hooków pozostawiono charakter dwóch:
ngOnInit
zostaje zastąpione przezafterInit
ngOnDestroy
przezbeforeDestroy
Moment ich wykonania jest taki sam jak poprzedników, czyli odpowiednio: po utworzeniu komponentu i ustawieniu wszystkich inputów oraz przed zniszczeniem komponentu.
Pozostałe hooki nie mają sensu w nowym systemie detekcji zmian i ich działanie może zostać zastąpione poprzez wykorzystanie sygnałów:
ngOnChanges
– służące do reagowania na zmianę inputów – ponieważ teraz sam input jest sygnałem można wykorzystaćcomputed
to utworzenia na jego podstawie nowego sygnału lub zarejestrować odpowiednie operacje wewnątrz effect.ngDoCheck
– reagujący na proces detekcji zmian – jego działanie można przenieść do effect.ngAfterViewInit
– pozwalający na wykonywanie działań po wyrenderowaniu template – to miejsce zajmuje teraz afterNextRender.ngAfterContentInit
,ngAfterViewChecked
,ngAfterContentChecked
– służące do obserwowania wyników queries z widoku – same queries zwracają teraz sygnał, który z natury jest reaktywny
Nowy mechanizm detekcji zmian
Przegląd zmian rozpocznijmy od przypomnienia jak system Change Detection działał do tej pory. Angular śledzi eventy w przeglądarce (takie jak DOM events, wysłanie żądania HTTP czy timery) za pomocą biblioteki Zone.js, która rozszerza i dodaje callback podczas działania (tzw. monkey-patching) do obiektów (window, document) lub proptotypów (HtmlButtonElement, Promise), które mogą prowadzić do zmian w modelu danych.
Kiedy takie zdarzenie wystąpi framework nie wie co konkretnie, i czy wogóle, się zmieniło. Pobiera on nowe dane i aktualizuje widok porównując je z dotychczasowymi, przechodząc przez całe drzewo komponentów, chociaż najczęściej jedynie niewielka część aplikacji wymaga odświeżenia.
Liczbę sprawdzanych komponentów można ograniczyć poprzez zastosowanie strategii OnPush – wtedy sprawdzone zostają komponenty spełniające przynajmniej jeden z warunków (wystąpienie eventu DOM, zmiana inputu lub jawne oznaczenie komponentu jako wymagającego sprawdzenia) oraz wszyscy jego potomkowie. Krótko mówiąc daje to informację kiedy sprawdzać zmiany, ale już nie gdzie.
Takie rozwiązanie zapewnia pewne korzyści, zwłaszcza w mniejszych aplikacjach:
- możliwość bezpośredniego korzystania z JSowych struktur danych
- przechowywanie stanu gdziekolwiek
- możliwość prostej zmiany stanu, bez konieczności stosowania dodatkowego API
W praktyce ma jednak szereg wad, dostrzegalnych szczególnie w przypadku większych systemów:
- inicjalizacja działania Zone.js zużywa czas i zasoby, które dodatkowo rosną wraz z powiększaniem się aplikacji
- konieczność zamiany async/await na Promise, ponieważ dla słów kluczowych niemożliwy jest monkey-patching
- standardowe API przeglądarki jest modyfikowane co może prowadzić do trudnych w diagnozie błędów
- zaburzenie standardowego jednokierunkowego przepływu danych może prowadzić do wystąpienia znanego błędu ExpressionChangedAfterItHasBeenCheckedError
- użycie zewnętrznych bibliotek lub skryptów, które korzystają z API przeglądarki, może prowadzić do dużej liczby niepotrzebnych cykli detekcji zmian
- jest źródłem problemów z wydajnością aplikacji.
Wykorzystanie sygnałów do detekcji zmian zapewnia większą kontrolę oraz granularność, tym samym zapewniając większą wydajność i lepszy developer experience. Sygnałowe komponenty nie podlegają pod globalny system Change Detection, zamiast tego odświeżane są indywidualnie, według podstawowej zasady:
Detekcja zmian nastąpi tylko kiedy sygnał, którego wartość odczytujemy w template, powiadomi Angulara, że ta została zmieniona.
Granularność nowego mechanizmu polega na niezależnym sprawdzaniu każdego widoku, czyli cegiełki, z których zbudowany jest template – statycznego zestawu elementów HTML, dyrektyw czy komponentów – tworząc UI i dając możliwość warunkowego lub powtarzalnego wyświetlania jego części.
Następujący template składa się z jednego widoku:
1 2 3 4 5 6 |
<div> <label>Who: <input name="who"></label> <label>What: <input name="what"></label> </div> Z kolei użycie strukturalnych dyrektyw, takich jak <code>ngFor</code>, <code>ngSwitchCase</code>, czy <code>ngIf</code> tworzy w template więcej niezależnych widoków: <div> <label>Who: <input name="who"></label> <ng-container *ngIf="showWhy"> <label>Why: <input name="why"></label> </ng-container> </div> |
Odświeżanie UI na poziomie widoków jest najbardziej wydajnym rozwiązaniem, ponieważ są to relatywnie niewielkie elementy z niezbyt wielką ilością bindingów, przez co koszt takiej operacji jest niewielki. Większe rozdrobnienie zużywałoby dodatkowe ilości pamięci i czasu na śledzenie wielu zależności. Z kolei większe i dynamiczne struktury naturalnie dzielą się na widoki, które mogą być aktualizowane niezależnie.
Dodatkową optymalizację zapewnia fakt, że inputy są teraz sygnałami – aktualizacja wartości inputu natępuje przed detekcją zmian, a nie w trakcie, a sama detekcja nie występuje, jeżeli input nie jest odczytywany w templacie. Sama zmiana bindowanej do inputa wartości nie powoduje również odświeżenia widoku rodzica.
Integracja z RxJS
Observable, dostępne za przyczyną RxJS, są obecnie szeroko wykorzystywane zarówno w samym Angularze jak i całym ekosystemie. Sygnały przejmą część z tych zastosowań, jednak ze względu na to, że za tymi konstruktami stoją dwie odmienne koncepcje, mogą znakomicie ze sobą współpracować.
Sygnały są synchroniczne, przez co znakomicie nadają się do zarządzania stanem, a także reprezentowania wartości zmieniających się w czasie. Observable z kolei są z natury asynchroniczne i reprezentują strumień danych. RxJS udostępnia również wiele narzędzi, które pozwalają na świetne zarządzanie skomplikowanymi, asynchronicznymi operacjami.
Do zamiany Observable w sygnał służy funkcja toSignal
:
1 2 3 4 5 |
export function toSignal<T, U extends T|null|undefined>( source: Observable<T>, options: { initialValue: U, requireSync?: false }): Signal<T|U>; export function toSignal<T>( source: Observable<T>, options: { requireSync: true }): Signal<T>; |
Funkcja ta subskrybuje do przekazanego jako argument Observable i zmienia wartość zwracanego sygnału za każdym razem, gdy pojawi się nowa wartość. Subskrypcja następuje od razu, aby nie wywoływać niepotrzebnie kodu tworzącego Observable. Gdy zniszczony zostanie kontekst, w którym funkcja została użyta, np. komponent, nastąpi automatyczne odsubskrybowanie.
Dopóki Observable nie wyemituje żadnej wartości, domyślnie sygnał będzie przechowywał undefined, co nie zawsze jest najlepszym wyborem, dlatego obiekt opcji zawiera pole initialValue, którego wartość zainicjuje sygnał.
Niektóre Observable emitują wartości synchronicznie (jak np. BehaviorSubject). W takim przypadku możemy ustawić opcję requireSync, co pozwala pozbyć się obsługi wartości początkowej. Jeżeli jednak opcja ta zostanie ustawiona, a przekazany Observabe będzie tworzony asynchronicznie, funkcja wyrzuci błąd.
Observable oferuje subskrybentowi trzy typy notyfikacji: next, error i complete. Ponieważ sygnał jest zainteresowany jedynie emitowanymi wartościami funkcja toSignal
nie obsługuje błędów i wyrzuci go przy kolejnej próbie odczytania wartości z sygnału. Aby temu zapobiec należy ręcznie obsłużyć błąd za pomocą bloku try/catch lub operatora catchError.
Za konwersję w drugą stronę odpowiada funkcja toObservable
:
1 |
const count: Observable<number> = toObservable(counterObs); |
Kiedy utworzony Observable zostanie zasubskrybowany utworzy ona efekt, wewnątrz którego będzie przekazywać kolejne wartości sygnału do subskrybentów. Wszystkie nowe wartości są emitowane w sposób asynchroniczny, co oznacza że kilkukrotna i synchroniczna zmiana wartości sygnału spowoduje wyemitowanie tylko ostatniej z nich:
1 2 3 4 5 6 7 8 |
const myObservable = toObservable(mySignal); myObservable.subscribe(console.log); mySignal.set(1); mySignal.set(2); mySignal.set(3): //Output: 3 |
Podsumowanie
Wiele zmian, ale także wiele nowych możliwości. Wprowadzenie sygnałów poprawia optymalizację i daje pole do implementacji dalszych zmian, które mogą poprawić developer experience, stanowi świetne uzupełnienie dla RxJS, definiuje nowy sposób zarządzania stanem oraz (w przyszłości) tworzenia komponentów. Mam nadzieję, że ten artykuł stanowił wartościowe wprowadzenie do tematu oraz podstawę do dalszego jego zgłębiania. Zapraszamy do podzielenia się Waszymi opiniami na temat tych nowości.
Świetny artykuł. Brawo.