I'm reading Build Your Own AngularJS and have a decent understanding of how $scopes
, $watch
and $digest
work. I understand how it works when you add your own $watches
and and call your own $digests
. However, I'm confused about what exactly is happening by default.
What gets added to $scope.$$watchers
by default? Everything you put on $scope
? Everything you assign an ng-model
to? Both? Something else?
And when exactly do $digests
get triggered by default? Input field changes? Input fields with ng-models
? Other?
Some of the common directives that use $watch
/ $watchCollection
/ $watchGroup
internally:
- ng-model
- ng-bind / {{ }}
- ng-show & ng-hide
- ng-class
- ng-repeat
- ng-if
- ng-switch
- ng-include
Note that the only one that sets up a two-way binding is ng-model
(scope -> view & view -> scope).
The others set up a one-way binding (scope -> view).
Simply exposing something on for example a controller´s $scope
will not add a watcher.
For example, the following will not result in a watcher being added:
angular.module('myApp', []).controller('Controller', function MyCtrl($scope) {
$scope.value = 1;
});
Together with:
<body ng-app="myApp" ng-controller="Controller">
</body>
But if you replace the HTML with the following one watcher will be added:
<body ng-app="myApp" ng-controller="Controller">
<div>{{value}}</div>
</body>
Some common scenarios when the digest cycle is triggered:
- When
ng-click
is evaluated
- When
ng-model
changes (for example when typing in an input)
- By the
$http
service
- In
$timeout
and $interval
Note that there is one big difference between $apply
and $digest
:
Calling scope.$digest()
will execute the watchers only on that scope and its children.
Calling scope.$apply()
will trigger $digest
on the $rootScope
, which means all the scopes will be traversed and all watchers executed.
$apply
also accepts an expression as an argument. This expression will be evaluated inside a try-catch statement and any exception will be passed on to the $exceptionHandler
service.
$digest
does not accept any arguments.
Usually you only call $digest
instead of $apply
when you are chasing micro optimizations and really know what you are doing.
It's my understanding that any two-way bindings get a $watch
in their scope, however, if it is added by angular internals, you don't get that hook, because, for example, ngModelController
has the callback, so you can't use that callback, I think what is on scope does not get a watch unless it is bound to the view.
$digest
is not used on model binding from what I found in the source code - but I found plenty of uses of $apply
. In fact I found very few uses of $digest
in the angular code at all. I didn't look in every file, but I did find it used in the expression parser here. Which I find interesting, but to answer your question, $digest
in not called often, and $apply
is called on only a few occasions with model binding, most notably on $commitViewValue()
in the ngModelController
. It also calls apply when an input is "touched"(ng-touched
). I also found that $evalAsync
calls $digest
as well.
What I found for certain on apply/digest:
//apply method in rootScope
$apply: function(expr) {
try {
beginPhase('$apply');
return this.$eval(expr);
} catch (e) {
$exceptionHandler(e);
} finally {
clearPhase();
try {
$rootScope.$digest();
} catch (e) {
$exceptionHandler(e);
throw e;
}
}
}
As you can see, apply is really just a safe wrap around $digest
, so one might argue to use $apply over digest. According to the source $digest does some crazy while-loop $scope
traversal.
TLDR;
The notes from the angular team on digest:
Processes all of the {@link ng.$rootScope.Scope#$watch watchers} of
the current scope and
its children. Because a {@link ng.$rootScope.Scope#$watch watcher}'s listener can change
the model, the $digest()
keeps calling the {@link ng.$rootScope.Scope#$watch watchers}
until no more listeners are firing. This means that it is possible to get into an infinite
loop. This function will throw 'Maximum iteration limit exceeded.'
if the number of
iterations exceeds 10.
Usually, you don't call $digest()
directly in
{@link ng.directive:ngController controllers} or in
{@link ng.$compileProvider#directive directives}.
Instead, you should call {@link ng.$rootScope.Scope#$apply $apply()} (typically from within
a {@link ng.$compileProvider#directive directive}), which will force a $digest()
.
If you want to be notified whenever $digest()
is called,
you can register a watchExpression
function with
{@link ng.$rootScope.Scope#$watch $watch()} with no listener
.