If you want to get notified whenever $digest() is called, you can set up a watcher without any listener function. The first and only argument to $scope.$watch() should be the function whose return value you want to monitor. This function gets called in every digest cycle. That is why the second argument to $watch() is optional.What you need to do is pass a simple function as the first argument to $scope.$watch(), as shown below:

$scope.$watch(function(){
  //do something here
  console.log('called in a digest cycle');
  return;
});

The function passed to $watch() gets called in each digest cycle. As you know digest cycle continues to run (a maximum of 10 times, of course) until the scope models stabilize. So, your function might get called multiple times in a digest cycle.

If the $apply() call is required to fire a digest cycle, how do directives like ng-model and ng-click tell Angular about the model mutations? The answer is that, internally, ng-click and ng-model wrap the code that changes the models inside an $apply() call. As a result digest cycle runs and watchers are called as usual. So, here’s what happens when you write <input type="text" ng-model="name"/>:

  1. The directive ng-model registers a keydown listener with the input field. When the input field text changes a keydown event is fired and the corresponding listener is called to handle it.
  2. Inside the keydown listener the directive assigns the new value of input field to the scope model specified in ng-model. In our case the model name is updated with the new value. This code is wrapped inside $apply() call.
  3. After $apply() ends the digest cycle starts in which the watchers are called. If you have an expression {{name}} in the view it has already set up a watcher on scope model name. So, in digest cycle this watcher gets called and the listener function executes with the newValue and oldValue as arguments. The job of this listener is to update the DOM with the newValue probably using innerHTML.
  4. The overall result is that you see the {{name}} updated with whatever you type into the input field instantly.

Tip: Using $apply()

Use $apply() when you want to make the transition from the non-angularjs world to the Angular world and need a way to say “Hey, Angular, I am mutating some models, and now it’s your job to fire the watchers!”

What about an AJAX call? If you make XHRs through Angular’s built-in service $http (which you will do 99% of the time) the model mutation code is implicitly wrapped within the $apply() call, so you don’t need any additional steps. But if, for some reason, you’re writing XHRs manually with plain JavaScript, you need to mutate the models inside $apply().

Tip: $digest Gets Triggered Automatically

In most scenarios you don’t need to call $digest() yourself. Calling $apply() will automatically trigger a $digest on $rootScope which subsequently visits all the child scopes calling the watchers.

Warning: It’s Preferable to Call $apply with a Function as an Argument

You can call $scope.$apply() either with no arguments or a function as an argument. If a function (which changes models) is passed as an argument to $apply() it is evaluated and then $rootScope.$digest() is fired.

You can also change the models as usual and in the end just call $apply() to trigger a $digest cycle. But the former method ($apply() with argument) is the preferred approach and should always be used. This is because when you pass a function to $apply() the code will be wrapped in try/catch and any exceptions that occur will be passed to $exceptionHandler service.

You should also note that if you skip $apply() after changing some models, the changes won’t reflect in the view. The changes will be reflected only if $apply() is called and $digest() runs.