The Digest Loop and $apply

Let’s take a peek at how Angular works underneath the hood. How do we get this magical data binding to work in only a few lines of code? It’s important that we understand how the $digest loop works and how to use the $apply() method.

In the normal browser flow, a browser executes callbacks that are registered with an event when that event occurs (e.g., clicking on a link).

Events are fired when the page loads, when an $http request comes back, when the mouse moves or a button is clicked, etc.

When an event is fired/triggered, JavaScript creates an event object and executes any functions listening for the specific events with this event object. This callback method then runs inside the JavaScript function, which returns to the browser, potentially updating the DOM.

No two events can run at the same time. The browser waits until one event handler finishes before the next handler is called.

In non-Angular JavaScript, we can attach a function callback to the click event on a div. Any time that a click event is found on an element, the function callback runs:

var div = document.getElementById("clickDiv");
div.addEventListener("click", 
  function(evt) { 
    console.log("evt", evt); 
  });

Open the Chrome developer tools, and copy and paste the previous code inside of any web page and click around the page.

Any time the browser detects a click, the browser calls the function registered with the addEventListener on the document.

When we mix Angular into the flow, it extends this normal browser flow to create an Angular context. The Angular context refers specifically to code that runs inside the Angular event loop, referred to as the $digest loop. To understand the Angular context, we need to look at exactly what goes on inside of it. There are two major components of the $digest loop:

$watch List

Every time we track an event in the view, we are registering a callback function that we expect to be called when an event happens in the page. Recall our first example:

<!DOCTYPE html>
<html ng-app>
<head>
  <title>Simple app</title>
  <script 
    src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.js">
    </script>
</head>
<body>
  <input ng-model="name" type="text" placeholder="Your name">
  <h1>Hello {{ name }}</h1>
</body>
</html>

Any time a user updates the input field, {{ name }} changes in the UI. This change happens because we bind the input field in the UI to the $scope.name property. In order to update the view, Angular needs to track the change. It does so by adding a watch function to the $watch list.

Properties that are on the $scope object are only bound if they are used in the view. In the case above, we’ve added a single function to the $watch list.

Remember: For all UI elements that are bound to a $scope object, a $watch is added to the $watch list.

These $watch lists are resolved in the $digest loop through a process called dirty checking.

Dirty Checking

Dirty checking is a simple process that boils down to a very basic concept: It checks whether a value has changed that hasn’t yet been synchronized across the app.

The dirty checking strategy is commonly used in plenty of different applications, beyond Angular. Game engines, database engines, and Object Relational Mappers (ORMs) are some examples of such systems.

Our Angular app keeps track of the values of the current watches (in the watch object, for those who are curious). Angular walks down the $watch list, and, if the updated value has not changed from the old value, it continues down the list. If the value has changed, the app records the new value and continues down the $watch list.

Digest loop

Once Angular has run through the entire $watch list, if any value changed, the app will fall back into the $watch loop until it detects that nothing has changed.

Why run the loop all over again? If we update a value in the $watch list that updates another value, Angular won’t detect the update unless we rerun the loop.

If the loop runs ten times or more, our Angular app throws an exception, and the app dies. If Angular doesn’t throw this exception, our app could launch into an infinite loop, with bad results.

In future versions of Angular, the framework will use the native browser specification Object.observe(), which will speed up the dirty checking process considerably.

$watch

The $watch method on the $scope object sets up a dirty check on every call to $digest inside the Angular event loop. The Angular $digest loop always returns if it detects changes on the expression.

The $watch function itself takes two required arguments and a third optional one:

The watchExpression can either be a property of a scope object or a function. It runs on every call to $digest in the $digest loop.

If the watchExpression is a string, Angular evaluates it in the context of the $scope. If it is a function, then Angular expects it to return the value that should be watched.

The callback listener function is only called when the current value of the watchExpression and the previous value of the expression are not equal (except during initialization on the first run).

The objectEquality parameter is a comparison boolean that tells Angular to check for strict equality.

The $watch function returns a deregistration function for the listener that we can call to cancel Angular’s watch on the value.

// ...
var unregisterWatch =
  $scope.$watch('newUser.email', 
    function(newVal, oldVal) {
      if (newVal === oldVal) return; // on init
});
// ... 
// later, we can unregister this watcher
// by calling
unregisterWatch();

If we are done watching the newUser.email in this example, we can clean up our watcher by calling the deregistration function it returns.

For instance, let’s say we want to parse an input field value from a full name to split on spaces and find a simple first and last name. Given that our view looks like:

<input type="text" ng-model="full_name" placeholder="Enter your full name" />

We should never use $watch in a controller: It makes it difficult to test the controller. We’re making an allowance here for the sake of illustration, and we’ll move these watches into services later.

We want to set up a $watch listener on the full_name property and detect any changes to the value. We also want to set the $watch function on the full_name property.

angular.module("myApp")
.controller("MyController", ['$scope', function($scope) {
  $scope.$watch('full_name', function(newVal, oldVal, scope) {
    // the newVal of the full_name will be available here
    // while the oldVal is the old value of full_name
  });
}]);

In our example, we’re setting an AngularJS expression that tells our Angular app to “watch the full_name property for any potential changes on it, and run the function if you detect any changes”.

The listener function is called once on initialization, so the first time around, the value of newVal and oldVal will be undefined (and will be equal). That being the case, it’s generally good to check inside the expression if we’re in the initialization phase or if there is an update to the previous value. We can easily accomplish this check inside the function, like so:

$scope.$watch('full_name', 
  function(newVal, oldVal, scope) {
    if (newVal === oldVal) {
      // This will run only on the initialization of the watcher
    } else {
      // A change has occurred after initialization
    }
  });

The $scope.$watch() function sets up a watchExpression on the $scope for ‘full_name’.

$watchCollection

Angular also allows us to set shallow watches for object properties or elements of an array and fire the listener callback whenever the properties change.

Using $watchCollection allows us to detect when there are changes on an object or array, which gives us the ability to determine when items are added, removed, or moved in the object or array. $watchCollection works just like the normal $watch works in the $digest loop, and we can treat it as a normal $watch.

 
This page is a preview of ng-book.
Get the rest of this chapter plus 600 pages of the best Angular content on the web.

 

Ready to master AngularJS?

  • What if you could master the entire framework – with solid foundations – in less time without beating your head against a wall? Imagine how quickly you could work if you knew the best practices and the best tools?
  • Stop wasting your time searching and have everything you need to be productive in one, well-organized place, with complete examples to get your project up without needing to resort to endless hours of research.
  • You will learn what you need to know to work professionally with ng-book: The Complete Book on AngularJS or get your money back.
Get it now