What gets added to $scope.$$watchers by default in

2019-01-27 10:02发布

问题:

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.

  1. What gets added to $scope.$$watchers by default? Everything you put on $scope? Everything you assign an ng-model to? Both? Something else?

  2. And when exactly do $digests get triggered by default? Input field changes? Input fields with ng-models? Other?

回答1:

Some of the common directives that use $watch / $watchCollection / $watchGroup internally:

  1. ng-model
  2. ng-bind / {{ }}
  3. ng-show & ng-hide
  4. ng-class
  5. ng-repeat
  6. ng-if
  7. ng-switch
  8. 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:

  1. When ng-click is evaluated
  2. When ng-model changes (for example when typing in an input)
  3. By the $http service
  4. 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.



回答2:

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.