$apply in AngularJS

AngularJS bietet Datenbindung für beliebige JavaScript-Objekte. Wenn Daten jedoch aktualisiert werden, ohne dass AngularJS das bemerkt, muss sich der Entwickler selbst darum kümmern, AngularJS zu benachrichtigen. Hierzu dient die Funktion $apply.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 5 Min.
Von
  • Golo Roden
Inhaltsverzeichnis

Eine ausgesprochen hilfreiche Funktion in AngularJS ist die Datenbindung, die Steuerelemente der Webseite mit JavaScript-Objekten verbindet und deren jeweilige Werte aktualisiert, ohne dass der Entwickler explizit Code hierfür schreiben müsste.

Anders als beispielsweise Knockout benötigt AngularJS hierfür keine besonderen Objekte, sondern kann beliebige JavaScript-Objekte nutzen. Auf scheinbar magische Weise bemerkt AngularJS eine Aktualisierung und überträgt diese in das angebundene Steuerelement beziehungsweise Objekt.

Die unausgesprochene Frage lautet: Wie und warum funktioniert das?

Die zu Grunde liegende Idee ist verhältnismäßig einfach: AngularJS kennt innerhalb eines jeden Controllers die Funktion $apply, die am jeweiligen Scope zur Verfügung steht. Diese gleicht die Werte in den Steuerelementen mit jenen des Scopes ab. Damit die Datenbindung funktioniert, muss AngularJS also lediglich sicherstellen, dass jede Änderung eines Werts den Aufruf dieser Funktion bewirkt.

Eine derartige Veränderung kann prinzipiell zwei Ursachen haben. Entweder wurde sie vom Anwender ausgelöst oder der Scope wurde programmatisch aktualisiert:

  • Im ersten Fall löst der Anwender durch seine Interaktion in der Regel eine Direktive wie beispielsweise ng-model, ng-click oder ng-submit aus. Da diese von AngularJS bereitgestellt wird, kann AngularJS eingreifen und $apply aufrufen.
  • Ähnlich verhält es sich im zweiten Fall: Sobald beispielsweise eine über den $http-Dienst versendete AJAX-Anfrage zurückkehrt, kümmert sich AngularJS um den notwendigen Aufruf von $apply. Dies gilt nicht nur für $http, sondern für alle von AngularJS bereitgestellten Dienste, wie auch für $timeout.

Heikel wird es allerdings, wenn sich der Zustand des Systems auf einem Weg verändert, den AngularJS nicht überwachen kann: Da AngularJS die Änderung nicht bemerkt, ruft es infolge dessen auch die Funktion $apply nicht auf. Dementsprechend greift die Datenbindung nicht, weshalb weder die Steuerelemente noch die zugehörigen Objekte aktualisiert werden.

Dieses Problem tritt häufiger auf als man zunächst vermuten könnte: Ereignisse in der UI, die nicht über eine Direktive von AngularJS behandelt werden, zählen ebenso zu den Verursachern wie sämtliche Ereignisse im JavaScript-Code, die nicht über einen Dienst von AngularJS ausgelöst werden.

Hierzu zählen unter anderem die Funktionen setTimeout und setInterval des Webbrowsers und mit jQuery versendete AJAX-Anfragen. Auch das Eintreffen von Daten über eine Websocket-Verbindung erfolgt unterhalb des Radars von AngularJS.

Aus diesem Grund bewirkt die Änderung der Variablen foo durch den Callback im folgenden Controller keine Aktualisierung der Webseite:

angular.controller('fooController', [ '$scope', function ($scope) {
$scope.foo = 23;

setTimeout(function () {
$scope.foo = 42;
}, 5 * 1000);
} ]);

In all diesen Fällen obliegt es dem Entwickler, die $apply-Funktion von Hand aufzurufen. Tritt das Ereignis innerhalb eines Controllers auf, muss man $apply an der $scope-Variablen aufrufen. Tritt es jedoch innerhalb eines Dienstes auf, steht kein Scope zur Verfügung. In diesem Fall muss man auf $rootScope ausweichen.

Packt man den Inhalt des Callbacks also in einen Aufruf von $apply, bemerkt AngularJS die Änderung und aktualisiert die Webseite entsprechend:

angular.controller('fooController', [ '$scope', function ($scope) {
$scope.foo = 23;

setTimeout(function () {
$scope.$apply(function () {
$scope.foo = 42;
});
}, 5 * 1000);
} ]);

Gelegentlich begegnet man im Web dem Aufruf der $apply-Funktion, ohne dass dieser ein Callback übergeben würde. Dies funktioniert auf den ersten Blick zwar scheinbar auch, wird aber nicht empfohlen. $apply ruft den Callback nämlich beispielsweise innerhalb eines try/catch-Blocks auf, sodass eine geworfene Ausnahme in jedem Fall behandelt wird, wie Jim Hoskins in seinem Blog-Eintrag "AngularJS and scope.$apply" beschreibt.

Die Versuchung liegt nahe, sämtlichen Code in einen Aufruf von $apply zu verpacken. Dies funktioniert allerdings nicht, da $apply nicht aufgerufen werden kann, wenn AngularJS $apply bereits ausführt. AngularJS quittiert diesen Versuch mit der folgenden Fehlermeldung:

$apply already in progress

Als Ausweg wird manchmal empfohlen, den Aufruf von $apply von dem aktuellen Wert der Variablen $scope.$$phase abhängig zu machen oder direkt auf eine entsprechende Funktion wie beispielsweise safeApply auszuweichen.

Dadurch macht man sich allerdings von einer internen, undokumentierten Variable von AngularJS abhängig: Dieses Vorgehen ist also eher als Workaround denn als Lösung anzusehen. Die einzig saubere Lösung besteht darin, $apply dann (und nur dann) auszulösen, wenn der Kontext es tatsächlich erfordert.

Erhält man die zuvor genannte Fehlermeldung häufiger, deutet dies in der Regel auf ein tiefer liegendes Problem hin – in der Regel auf einen Fehler in der Struktur des Codes.

Unter Umständen begegnet man im Zusammenhang mit der Funktion $apply einer weiteren Funktion namens $digest. Obwohl beide Funktionen thematisch zusammenhängen, ruft man $digest im Normalfall niemals direkt auf. Dies geschieht innerhalb von $apply automatisch.

Der Hintergrund ist einfach erklärt: $apply führt zunächst den ihr übergebenen Callback aus und ruft anschließend $digest auf, die sich um das Aktualisieren der Werte kümmert. Dieses Vorgehen ist unter anderem deshalb sinnvoll, da $apply die Ausführung von $digest garantiert, auch im Fehlerfall innerhalb des Callbacks.

tl;dr: AngularJS bietet Datenbindung für beliebige JavaScript-Objekte. Wenn Daten jedoch aktualisiert werden, ohne dass AngularJS dias bemerkt, muss sich der Entwickler selbst darum kümmern, AngularJS zu benachrichtigen. Hierzu dient die Funktion $apply. ()