Event-Handling in AngularJS und ein potentielles Memory Leak

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.

angular_memory_leak_jsfiddle_init

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.

angular_memory_leak_jsfiddle_scenario_1

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.

angular_memory_leak_jsfiddle_scenario_2

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.

angular_memory_leak_jsfiddle_scenario_3

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.

 

bjoerne_com_bjoern_weinbrenner_softwareentwickler_icon_leistungen_02

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.

2017-01-28T13:52:41+00:00 01.03.2014|Tags: , , , |0 Kommentare

Hinterlassen Sie einen Kommentar