February 14, 2024
Angular Signals Teil 1 – How-to Guide für Angular Signals
Go here for the english version of this blog post.
Verbesserung der Leistung in Angular 17 mit Signals: Eine kluge Lösung
In der Weiterentwicklung von Angular wird mit der Einführung von Signals ein einfacherer und effizienterer Ansatz zur Erkennung von Änderungen eingeführt, der die Leistung und Reaktivität von Anwendungen grundlegend verbessert. Signals wurden bereits in Angular Version 16 als Developer Preview eingeführt. Mit Angular 17 wird das Feature in einer stabilen Version veröffentlicht. Im Gegensatz zur umfassenden Scanning-Strategie von Zone.js, die den gesamten Komponentenbaum auf Änderungen prüft, setzen Signals eine gezieltere Methode ein. Sie aktualisieren direkt nur die Komponenten, die von den Zustandsänderungen betroffen sind, was den unnötigen Leistungsaufwand erheblich reduziert.
Dieser Wechsel optimiert nicht nur die Change Detection von Angular, sondern entspricht auch einem intuitiveren Modell der Reaktivität, das sowohl die Entwicklungserfahrung als auch die Reaktionsfähigkeit des Frameworks verbessert. Durch Signals erhalten Angular-Entwickler Zugang zu einem präziseren und effizienteren Toolset für die Verwaltung von Zustandsänderungen, was einen bedeutenden Schritt nach vorne bei der Fähigkeit des Frameworks darstellt, dynamische und komplexe Anwendungen mit Leichtigkeit zu handhaben.
Durch die Bereitstellung eines effizienteren, gezielteren und vereinfachten Modells der Reaktivität ermöglichen Signals Angular-Anwendungen ein höheres Leistungsniveau und bieten Entwicklern ein schlankeres und effektiveres Toolset für die Erstellung dynamischer, reaktionsfähiger Anwendungen.
Im folgenen Blog erfährst du wie du mit Angular Signals effiziente Anwendungen entwickeln kannst. Wenn Du alles über Signals im Detail lernen und alle Tipps und Tricks erfahren möchtest, besuche einen unserer Angular-Kurse:
- Angular Basic Schulung: https://www.thecodecampus.de/schulungen/angular
- Advanced Schulung: https://www.thecodecampus.de/schulungen/angular-advanced
- Aktuelle Schulungstermine
Welches Problem wird durch Signals gelöst?
Angular Signals wurden entwickelt, um die Laufzeitleistung von Anwendungen zu verbessern, indem Zone.js ersetzt wird. Traditionell spielte Zone.js eine entscheidende Rolle bei der Aktivierung der Change Detection und der Aktualisierung der Benutzeroberfläche, wenn sich der Zustand der Anwendung änderte. Diese Methode erforderte allerdings das Scannen des gesamten Komponentenbaums, um relevante Zustandsänderungen zu identifizieren. Durch die redundante Prüfung inaktiver Komponenten kam es häufig zu Performanceproblemen.
Mit der Einführung von Signals werden die umfassenden Scans des Komponentenbaums überflüssig. Jetzt werden nur noch die Komponenten aktualisiert, die direkt von einer Änderung betroffen sind, wodurch der DOM-Aktualisierungsprozess erheblich vermindert wird. Diese fokussierte Strategie minimiert nicht nur den Systemaufwand, sondern steigert auch die Gesamteffizienz der Anwendung. So lassen sich mit Angular Signals effizientere Anwendungen entwickeln als zuvor.
Neben der Leistungsverbesserung vereinfachen die Angular Signals auch das State Management und bieten eine intuitivere Alternative zu RxJS, insbesondere beim Ersetzen von subjektbasierten Zuständen. RxJS kann, mit seinen leistungsstarken, aber komplexen Tools für die reaktive Programmierung, für Entwickler eine Herausforderung darstellen, insbesondere wenn es um die Verwaltung des Anwendungsstatus durch Subjects geht. Subjects in RxJS sind zwar flexibel, erfordern aber ein tieferes Verständnis von Konzepten der reaktiven Programmierung, wie Observables und Subscriber, was sie für Anfänger oder einfachere Anwendungen weniger zugänglich macht.
Im Gegensatz dazu bieten Signals einen unkomplizierten Mechanismus für das State Management. Sie vermeiden die steile Lernkurve, die mit RxJS verbunden ist, indem sie einen einfacheren, direkteren Ansatz bieten. Entwickler können einen State definieren und ihn direkt mit den UI-Komponenten verknüpfen, wobei die Komplexität von Observable Streams umgangen wird. Diese direkte Verknüpfung vereinfacht den Prozess der Aktualisierung der Benutzeroberfläche als Reaktion auf Zustandsänderungen, da keine Subscriptions verwaltet oder Datenströme verarbeitet werden müssen. Mit Signals geht es beim State Management eher darum, Zustandsänderungen zu definieren und darauf zu reagieren, als mit den Feinheiten reaktiver Programmierparadigmen zu jonglieren.
Signals alias Reaktive Primitives
Signals, auch bekannt als Reactive Primitives, sind ein System, das die Verwendung und die Abhängigkeiten von Zuständen bzw. States innerhalb einer Anwendung verfolgt und es Angular ermöglicht, Rendering-Updates zu optimieren. Mit Signals identifiziert Angular genau, wo welcher State verwendet wird und welche Abhängigkeiten bestehen. Dies ermöglicht ein gezieltes Re-Rendering von Komponenten, wodurch die Abhängigkeit zur Change Detection entfällt, weil eine umfassende Überprüfungen aller Komponenten nicht notwendig ist. Im Gegensatz zu Observables benötigen Signals keine Subscriptions und haben immer einen Anfangswert, was das State Management vereinfacht, da asynchrone Operationen, wie z.B. die async-Pipe, nicht mehr gebraucht werden.
Signals sind durchgängig typisiert und können vom Typ Number oder String oder sogar ein komplexer Typ sein. Sie können writeable oder readonly sein und man kann immer ein readonly Signal aus einem writeable Signal mit .asReadonly()
erzeugen. Readonly Signals können auch von writable Signales abhängen, aber darauf werden wir später noch eingehen.
Es ist auch sehr einfach, Signals zu exportieren, um sie in mehreren Komponenten zu verwenden.
1 |
export const name = signal('Angular'); |
1 |
import { name } from "main"; |
Wie man mit Signals interagiert
Nachdem wir nun die Vorteile von Signals bei der Bewältigung gängiger Herausforderungen in der Angular-Entwicklung kennengelernt haben, ist es an der Zeit, tiefer zu gehen. Lass uns herausfinden, wie man sie effektiv nutzen kann und die verschiedenen Methoden untersuchen, die uns im Umgang mit Signals zur Verfügung stehen.
signal()
Durch den Aufruf der signal
Funktion kann man ein writeable Signal erstellen. In unserem Fall ist das ein Zähler.
1 |
const counter = signal(0); |
Wir können auf den Wert mit dem Variablennamen und runden Klammern zugreifen. Dies funktioniert auch im Template, wenn wir den Aufruf in einer Expression verwenden.
1 |
console.log('New counter value', this.counter()); |
1 |
Count: {{ counter() }} |
set()
Mit set
kann man dem Signal einen neuen Wert geben.
1 |
this.counter.set(5); |
Damit können wir den Zähler auf Null zurücksetzen.
1 2 3 |
reset() { this.counter.set(0); }; |
update()
Durch die Verwendung von update
kann man auch den Wert des Signals ändern, aber jetzt hat man auch Zugriff auf den aktuellen Wert. Man kann also einen neuen Wert basierend auf dem alten Wert setzen.
In unserem Beispiel können wir dies für eine Inkrement- oder Dekrementfunktion verwenden.
1 2 3 |
increment() { this.counter.update((currentValue) => currentValue + 1); }; |
Die update
Funktion muss nicht unbedingt ein Einzeiler sein. Solange man einen Rückgabewert angibt, können verschiedene Operationen durchgeführt werden.
1 2 3 4 5 6 7 |
decrement() { this.counter.update((currentValue) => { console.log('Old value', currentValue); const newValue = currentValue - 1; return newValue; }); }; |
computed()
Um ein Signal zu erzeugen, das auf einem anderen Signal basiert oder von diesem abhängig ist, kann computed
verwendet werden. Diese Funktion erzeugt ein readonly Signal, das sich selbst aktualisiert, wenn sich der Wert des Signals, von dem es abhängt, ändert.
Als Beispiel werden wir eine Variable erstellen, die prüft, ob der aktuelle Zähler gerade oder ungerade ist. isOdd
kann nicht durch die set
oder update
Funktion geändert werden. Da Angular aber weiß, dass es eine Abhängigkeit zwischen den beiden Signals gibt, wird jedes Mal, wenn sich das counter
Signal ändert, die Callback-Funktion von isOdd
erneut ausgeführt.
1 |
const isOdd = computed(() => this.counter() % 2 !== 0); |
Natürlich funktioniert dies auch mit zwei oder mehr Signals, von denen es abhängt. Jetzt wird combined
jedes Mal aktualisiert, wenn sich entweder firstLetter
oder secondLetter
ändert.
1 2 3 4 |
const firstLetter = signal('a') const secondLetter = signal('b') const combined = computed(() => this.firstLetter().concat(this.secondLetter())); |
Wie die update
Funktion muss computed
kein Einzeiler sein. Jedoch muss man hier beachten, dass die Abhängigkeiten eines computed Signals nicht nur durch seinen Rückgabewert bestimmt werden. Im folgenden Beispiel verwendet combined
nur noch firstLetter
im return statement, wird aber immer noch aktualisiert, wenn sich der Wert von secondLetter
ändert, da das Signal in der Callback-Funktion von combined
verwendet wird.
1 2 3 4 |
const combined = computed(() => { console.log('Second letter changed', this.secondLetter()); return this.firstLetter().concat('c') }); |
effect()
Mit effect
kann man festlegen, was passieren soll, wenn sich der Wert eines Signals ändert, oder anders gesagt, welche Seiteneffekte dadurch ausgelöst werden. Das kann das Loggen des Wertes eines Signals sein, das Exportieren des Wertes in den localStorage oder das transparente Speichern des Wertes in die Datenbank.
In unserem Fall wollen wir nur den neuen Zählerwert auf der Konsole ausgeben.
Standardmäßig erfordert die Registrierung eines neuen Effekts mit der effect
Funktion einen Injektionskontext (Zugriff auf die inject
Funktion). Am einfachsten ist es, effect
innerhalb eines Komponenten-, Direktiven- oder Servicekonstruktors aufzurufen. Alternativ kann der Effekt auch einer Variablen zugewiesen werden (wodurch er auch einen beschreibenden Namen erhält).
1 2 3 4 |
constructor() { effect(() => { console.log('New counter value', this.counter()); }); } |
Wie die computed
Funktion kann ein Effekt von mehreren Signals abhängig sein.
1 |
const letterEffect = effect(() => console.log('Log letters', this.firstLetter(), this.secondLetter())); |
untracked()
Wenn man Signals in einer reaktiven Funktion wie computed
oder effect
lesen will, ohne eine Abhängigkeit zu schaffen, kann man das Signal mit der untracked
Funktion aufrufen, um zu verhindern, dass ein Signal verfolgt wird.
Nehmen wir den letterEffect
von oben als Beispiel. Im Moment loggt der Effekt die aktuellen Buchstaben, wenn sich einer der beiden Signalwerte ändert. Wenn der Effekt nur ausgelöst werden soll, wenn sich firstLetter
ändert, aber nicht, wenn sich secondLetter
ändert, können wir das Folgende schreiben:
1 |
const specialEffect = effect(() => console.log('Special effect', this.firstLetter(), untracked(this.secondLetter))); |
Erfahrung ist der beste Lehrer: Hands-On mit Signals
Bist Du bereit, die Theorie in die Praxis umzusetzen? In diesem interaktiven StackBlitz-Beispiel haben wir eine Beispielanwendung erstellt, die die besprochenen Signal-Konzepte beinhaltet. Tauche ein und experimentiere, um Signals in Aktion zu erleben!
Anwendungsfälle um mit Angular Signals effiziente Anwendungen zu entwickeln.
Signals sind gut geeignet, um einen synchronen Zustand in den Komponenten zu verwalten. Sie eignen sich nicht für Events und andere asynchrone Operationen. Daher sind Signals eine Erweiterung zu RxJS und kein Ersatz. Sie können eine Ablösung für async-Pipes und OnPush-Komponenten sein, da Angular von selbst merkt, wenn sich etwas in der Komponente geändert hat.
Mehr über die Verwendung von Signals und RxJS wird im zweiten Teil unserer Artikelserie zu Angular Signals behandelt.
Wenn ein tieferes Verständnis für die inneren Mechanismen und deren Nutzung von Angular signals zu erfahren, empfehlen wir die Videos von Deborah Kurata. Sie erklärt extrem gut wie man mit Signals in Angular umgeht und was zu beachten ist.