$watch(),$digest() & $apply()

These are the top 3 names which every Angular developer will definitely come across in the life cycle of playing with $scope or just simply say Angular Js  :p when I started learning Angular Js and implementing it these things were super alien to me.

So, I initially ignored their importance and was moving on with rest of the stuff.But when the time came for me to write a directive which uses isolate binding then I was scratching my head with following 3 questions ( there were lot but chuck them :p) :

  • How to know my scope object has changed ?
  • Why  my scope model is not getting updated ?
  • How should I tell to angular that something in my parent scope has changed,so reflect it ?

These question’s led me find out what’s happening under the hood when a scope is created and until it’s destroyed.Let’s find out how we can answer these questions with these three magic terms( $watch(),$digest() & $apply() ) that are important to us.

$watch()


The name itself says ‘to watch’,yes exactly this function can be used to find out if a scope variable is changed from its previous state or not.Basically there are three types of watching functions we can use to keep track of scope variables.They are :

a) $watch(watchExpression, listener, [objectEquality]); 

The intake parameter’s are explained as below:

  • watchExpression : The scope variable/expression basically which you want to watch.
  • listener: Basically a function that is called when the value of the watchExpression changes to a new value. If the watchExpression is not changed then listener will not be called.
  • objectEquality: By default set to false,if we want to check the changed value then set it to true,with that it uses  angular.equals to check  object’s equality.

A small example on how to use this :

var myapp = angular.module("myapp", []);
var Ctrl1= myapp.controller("Ctrl1", function($scope) {
$scope.name = 'Angular';
$scope.$watch('name', function(newValue, oldValue) {
$scope.name= 'Name has changed to' +newValue;
},true);
});

b) $watchGroup(watchExpressions, listener);

  • One more type in $watch with this we can watch a collection,if any variable in collection changes then listener is executed.


$scope.angularUsers = 20;
$scope.emberUsers = 5;
$scope.$watchGroup(['angularUsers', 'emberUsers'], function(newVal, oldVal) {
if (newVal[0] > newVal[1]) {
$scope.popularFW = 'Angular Js is most used framework';
} else if (newVal[1] > newVal[0]) {
$scope.popularFW = 'Ember Js is most used framework';
}
});

C) $watchCollection(obj, listener);

  • This function is used to watch the properties of an object(arrays) and listener is executed whenever any of the properties change. It takes an object as the first parameter and watches the properties of the object.


$scope.jsFrameWorks = ['angular', 'ember', 'meteor', 'knockout'];
$scope.frameWorksCount = 4;
$scope.$watchCollection('jsFrameWorks', function(newVal, oldVal) {
$scope.frameWorksCount = newVal.length;
});

$digest()


  • It processes all of the watcher’s of the current scope and its children,as  watcher’s listener can change the model,
  • The $digest()keeps calling the watcher’s until no more listeners are firing. Although there is a restriction of maximum 10 iteration’s to avoid infinite loop,after that it throw’s an error of ‘Maximum iteration limit exceeded’.

Basically,if you are using something like this in your controller:


$scope.showDate = new Date();
$scope.changeDate = function () {
$scope.showDate = new Date();
};
//when this click event is captured new changed date will not be reflected..
document.getElementById("someClick").addEventListener('click', function () {
$scope.showDate = new Date();
});

What’s happening in above code is we are trying to change scope variable outside angular context which it fails to update.In this scenario as mentioned if  we want to reflect our changes in the current and children scope we will call $digest().


document.getElementById("someClick").addEventListener('click', function () {
$scope.showDate = new Date();
//update the scope
$scope.$digest();
});

Note:Calling $digest in your controller or directive is a bad practice as it doesn’t can’t handle errors via $exceptionHandler service,which makes us to handle the exeption explicitly.

$apply()


  • It is used to execute an expression in angular from outside of the angular framework. (For example from browser DOM events, setTimeout, XHR or third-party libraries).
  • To perform proper scope life cycle of exception handling and executing watcher’s.
  • As mentioned in the Angular doc’s $apply has proper exception handling as shown below with pseudo code:
    function $apply(expr) {
      try {
        return $eval(expr);
      } catch (e) {
        $exceptionHandler(e);
      } finally {
        $root.$digest(); //$digest() is called by $apply()
      }
    }
  • What exactly happens in $apply() is it calls the $eval() method to execute the expression and if any error occurs it handles it through $exceptionHandler service then later call’s $digest to update entire scope.

Let’s take our  previous example and see how it’s written with $apply()

document.getElementById("updateTimeButton").addEventListener('click', function() {
$scope.$apply(function() {
$scope.showDate = new Date();
});
});

So as we can see it’s a good practice to call $apply() instead of $digest() to update scope variable as it’s handled much better than later one and updates entire scope chain.

Now,coming back to our three questions which we were trying to find solution are finally answered:

  • How to know my scope object has changed ? —> $watch()
  • Why  my scope model is not getting updated ? —> $digest()
  • How should I tell to angular that something in my parent scope has changed,so reflect it ? —> $apply()

Hope this article helped you understanding things happening under the hood  with angular js in much better way.For any queries or questions feel free to comment and share your feedback. Cheers  !! 🙂

Advertisements