AngularJS: Watchers and the Digest Cycle

AuthorMáximo Mussini
·4 min read

AngularJS might not be hip anymore, but it's still a useful framework to create interactive web apps. In this post we will take a brief tour through Angular's internals: watchers and the digest cycle.

Bindings and Watchers

One of the most useful features in AngularJS are data-bindings, which allow us to bind a model or property to a view; whenever the model changes, the view is updated automatically.

<input ng-model="user.name" type="text" />
<h1>{{ user.name }}</h1>

How does that work? When you write an expression like {{ user.name }}, Angular creates a watcher that observes the user.name property, which will be triggered whenever the model changes, allowing Angular to update the view content.

We can also create watchers manually and execute arbitrary code when the model changes:

$scope.$watch('user.name', function(newName, oldName) {
  console.log('user.name changed from', oldName, 'to', newName);
});

The first argument passed to $watch is known as the model, and the second argument is the listener function, which is called whenever the model (or more precisely, value of the watched expression) changes.

Now that we know what watchers can do for us, let's dig a bit deeper and learn how they work. How does Angular figure out when user.name changed in order to call the listener?

The Watch List and Dirty Checking

Each time a watcher is created, Angular adds the expression to a watch list to track changes. Angular will walk down the watch list from time to time and resolve each watcher through a process called dirty checking:

  • Keep the last value for each watched expression.
  • Evaluate the expression: if the value is the same than the last one continue down the watch list.
  • If the value is different the expression is dirty, so propagate the change by calling each listener with the old and the new value.
  • Once the change has been synchronized across the app, replace the last value with the new value.
  • Continue to the next expression in the watch list.

For every UI element that is bound to a $scope object, a watch is created and added to the watch list, which is checked on every digest loop.

🔁 The Digest Cycle

And it's in the $digest cycle where every watcher in the watch list is evaluated, and the changes propagated to the listeners.

This cycle starts as a result of a call to $scope.$digest(), which often happens as a result of an action performed by the user. For example, clicking an element with the ng-click directive will explicitly call $scope.$digest() and start the loop.

Once the cycle starts, it will go through the watch list, propagating changes to listeners as needed.

There are many Angular directives and services that will automatically trigger a digest cycle, such as ng-model and $timeout.

👑 Keep Calm and Digest

Once Angular has run through the entire watch list, if any value changed, it will start a new digest cycle until no model is changed and no watchers are triggered.

Why does it run the loop all over again? Because any $watch listener could change the value of an expression that was evaluated earlier in the digest loop, so Angular wouldn't be able to detect and propagate that change.

Remember that Angular uses dirty-checking as a way to determine if the watched expression changed, so the only way to guarantee all changes are propagated is to go through the watch list again and check that no values were changed during the previous digest cycle.

This means that the digest loop will run a minimum of two times, even when listeners don’t change any models.

Minimize changes to watched models when inside listener functions, each change could trigger an extra digest loop.

If the loop runs ten times or more, Angular will throw an exception to prevent a possible infinite loop, which would make the app unusable.

Conclusion

Because of the nature of Angular's internals, it's very important to minimize the amount of watchers in order to keep the digest cycle fast.

At the same time, it's important to ensure that our application doesn't trigger more digest cycles than necessary, since each loop requires evaluating every watcher in the list.

While watchers are a very useful feature, the digest cycle implementation takes a brute force approach, which makes it almost magical at times, but is inefficient and can cause performance problems in complex applications.

In the next post, we will take a look at some techniques that help to reduce the amount of watchers and improve the performance of our app. Stay tuned! 😃