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:
- The $watch list
- The
$evalAsync
list
$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
.
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).
- objectEquality (optional)
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
.
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