Angular JS : $digest loop and $apply

We know that angular has this 2 way data binding feature that greatly simplify things for us. Data binding means that when you change something in the view, the scope model automagically updates. Similarly, whenever the scope model changes, the view updates itself with the new value. So how does angular does this?
We know that the event loop in JS is where all the events are thrown, we just need to listen to these events. Using javascript or Jquery we manually attach code to these events. AngularJS keeps tracks of these events to do things for us. In order to track all the variables and functions angular add Watchers. There is a big watch list. Every time you put a variable on to the $scope and put it in the view, angular automatically adds a watcher to the watch list, i.e keeping track of the original value and the new value. To check if the value got changed, angular has a Digest loop. 

concepts-runtime

The $digest loop is fired when the browser receives an event that can be managed by the angular context. This loop is made up of two smaller loops. One processes the $evalAsync queue and the other one processes the $watch list. The $digest loop keeps iterating until the $evalAsync queue is empty and the $watch list does not detect any changes in the model. These watches are resolved in the $digest loop through a process called dirty checking. The dirty checking is a process which checks whether a value has changed, if the value has changed, it set the $scope as dirty. If the $scope is dirty, another $digest loop is triggered.

Lets see this by an example – index.html:


<!DOCTYPE html>
<html lang="en-us" ng-app="angularApp">
<head>
<title>AngularJS Demo</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" />
<script src="https://code.angularjs.org/1.4.8/angular.min.js"></script>
<script src="../js/main.js"></script>
</head>
<body>

<header>
<nav class="navbar navbar-default">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand">AngularJS Demo</a>
</div>
</div>
</nav>
</header>
<div class="container">
<div ng-controller="mainController">

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

</div>
</div>
</body>
</html>

main.js:


var myApp = angular.module('angularApp', []);

myApp.controller('mainController', ['$scope', function($scope) {

$scope.name = '';

$scope.$watch('name', function(newValue, oldValue) {
console.info('Changed!');
console.log('Old:' + oldValue);
console.log('New:' + newValue);

});

}]);

digestloop1

But what if something outside angular context changes the value. Lets see this by an example. Changing main.js


var myApp = angular.module('angularApp', []);

myApp.controller('mainController', ['$scope', function($scope) {

$scope.name = '';

$scope.$watch('name', function(newValue, oldValue) {
console.info('Changed!');
console.log('Old:' + oldValue);
console.log('New:' + newValue);

});

setTimeout(function() {

$scope.name = 'setTimeout';
console.log('Scope changed by setTimeout!');

}, 2000);

}]);

digestloop2

As you can see, setTimeout log came but nothing happens in the DOM. This is because setTimeout is outside angular context and angular didn’t realize that the value changed. To handle this we have to call $apply() manually, which triggers a $digest cycle. 


var myApp = angular.module('angularApp', []);

myApp.controller('mainController', ['$scope', function($scope) {

$scope.name = '';

$scope.$watch('name', function(newValue, oldValue) {
console.info('Changed!');
console.log('Old:' + oldValue);
console.log('New:' + newValue);

});

setTimeout(function() {
$scope.$apply(function(){
$scope.name = 'setTimeout';
});
console.log('Scope changed by setTimeout!');

}, 2000);


}]);

digestloop3

We could use angular provided services too, to keep things in angular context. Angular provides a $timeout service which does the same thing and keep things in angular contex (by using apply only).


var myApp = angular.module('angularApp', []);

myApp.controller('mainController', ['$scope','$timeout', function($scope,$timeout) {

$scope.name = '';

$scope.$watch('name', function(newValue, oldValue) {
console.info('Changed!');
console.log('Old:' + oldValue);
console.log('New:' + newValue);

});

$timeout(function() {
$scope.name = 'setTimeout';
console.log('Scope changed by setTimeout!');
}, 2000);

}]);

This will produce the same result. So these watchers/digest loop is what binds the our view and model. Just remember if we are going outside angular context like say ajax call using normal js or jquery just wrap the callback with $scope.$apply so that angular could keep track of it, or use a equivalent angular service. 

Advertisements
%d bloggers like this: