Event-Handling in AngularJS
In vielen Sprachen, Frameworks oder APIs gibt es Konstrukte, die dem Observer-Pattern (Beobachter) entsprechen. Ein Listener wird an einem Subjekt registriert und aufgerufen, sobald ein bestimmtes Event (Ereignis) auftritt. Damit das Subjekt den Listener aufrufen kann, muss dieser eine vorgegebene Schnittstelle implementieren. Abgesehen von dieser Schnittstelle besitzt das Subjekt keinerlei Wissen über den Listener, so dass eine recht lose Kopplung zwischen den Objekten besteht. Hierin liegt der Vorteil dieses Entwurfsmusters.
In AngularJS wird dieses Konzept umgesetzt, in dem die Scopes die Funktion eines Event-Bus einnehmen. Listener können sich an Scopes (egal ob $rootScope oder $scope) per $on-Methode registrieren und werden aufgerufen, sobald ein Event per $emit-Methode an den Scope gesendet wird. AngularJS sendet eigene Events, die mit $ beginnen, z.B. $destroy, $routeChangeStart etc., Anwendungsentwickler können aber beliebige eigene Events über die Scopes schicken und empfangen.
Problematik
Die Gefahr eines Memory Leaks besteht, wenn Objekte von kurzer Lebensdauer einen Listener an Objekten mit längerer Lebensdauer registrieren, nämlich wenn die Listener im Speicher bestehen bleiben, nachdem die registrierenden Objekte bereits entfernt wurden.
In AngularJS könnte das wie folgt ablaufen: Im Konstruktor eines Controllers wird am $rootScope ein Listener (Callback) registriert. Der Listener hat über einen Closure-Scope Zugriff auf den $scope. Wenn nun Controller und $scope beispielsweise durch Navigation in einen anderen Anwendungsteil ihren Dienst erfüllt haben und aus dem Speicher entfernt werden könnten, ist das nicht möglich solange der $rootScope Listener enthält, der auf den $scope zugreift. Untenstehendes Codebeispiel verdeutlich das etwas anschaulicher.
Navigiert ein Benutzer mehrfach innerhalb der Anwendung und wiederholt sich dieser Sachverhalt, wächst der belegte Speicher weiter an und es kann zu Problemen bis hin zum Absturz des Browsers kommen. Dieselbe Problematik besteht auch in weiteren Frameworks wie Backbone.js.
Code-Beispiel mit JSFiddle
Das JSFiddle http://jsfiddle.net/bjoerne/LM3gt/ zeigt die Problematik, aber auch die Lösung dieses Sachverhalts. Diese Mini-Anwendung wechselt automatisch zwischen zwei AnguarJS-Routen hin und her. Mit jedem Wechsel wird ein Controller erzeugt, der ein Array mit Zufallszahlen im $scope gesetzt. Das Speicherverhalten wird über eine JavaScript-API abgefragt (performance.memory.usedJSHeapSize), die aber standardmäßig nicht zur Verfügung steht. Startet man Google Chrome mit dem --enable-memory-info Parameter funktioniert die Abfrage des benötigten Speichers.
Zu Beginn des JavaScript-Teils werden Konstanten festgelegt. Diese können genutzt werden, um verschiedene Konfigurationen auszuprobieren.
app.value('constants', { 'numOfRandomNumbers': 1000000, 'shouldRegister': true, 'shouldUnregister': true, 'autoSwitchTimeout': 1000 });
Variante 1: Keine Registrierung von Listenern
Mit shouldRegister: false wird bewirkt, dass lediglich die Controller und der Zufallszahlen-Array erzeugt werden, aber keine Listener am $rootScope erzeugt werden. Es lässt sich erkennen, dass AngularJS aufräumt, nachdem ein $scope beendet wird. Der Speicherverbrauch bleibt langfristig auf einem Niveau.
Variante 2: Registrierung am $rootScope und keine Unregistrierung
Anders verhält es sich, wenn der Controller einen Listener am $rootScope registriert, der Zugriff auf den $scope hat (shouldRegister: true und shouldUnregister: false).
$rootScope.$on('someEvent', function() { $scope.eventOccurred = true; });
Der Speicherverbrauch nimmt stetig zu. Die Anwendung hat ein Memory Leak, da der $scope nicht aus dem Speicher entfernt werden kann.
Variante 3: Registrierung am $rootScope und Unregistrierung
Mit shouldRegister: true und shouldUnregister: false wird zwar ein Listener registriert, dieser wird aber mit dem $destroy des Scopes wieder entfernt. In AngularJS funktioniert das so, dass der Rückgabewert der $on-Funktion die Funktion zum Unregistrieren ist.
var unregister = $rootScope.$on('someEvent', function() { $scope.eventOccurred = true; }); if (constants.shouldUnregister) { $scope.$on('$destroy', function() { unregister(); }); }
Auch nach vielen Scope-Wechseln bleibt der Speicherverbrauch stabil.
Fazit
Das Registrieren von Event-Listenern am $rootScope ist eine saubere Sache sofern man ein paar Dinge beachtet. Ist die Lebenszeit des Listeners analog zur Lebenszeit eines Scopes, so muss der Listener mit dem Beenden des Scopes manuell entfernt werden, da sonst ein Memory Leak entsteht.
Lust auf mehr? Als Experte für AngularJS und individuelle JavaScript-Entwicklung kann ich Sie in Ihren Projekten unterstützen. Melden Sie sich gerne bei mir.
Hinterlassen Sie einen Kommentar