How to Do Dirty Checking in AngularJS
We will introduce how to do dirty checking in AngularJs.
Implement Dirty Checking in AngularJS
AngularJS carries out dirty checking on $scope
variables for two-way data binding. Ember.js
performs two-way data binding by programmatically fixing up the setters and getters, but in Angular, dirty checking permits it to look for variables that might be available or not.
# AngularJs
$scope.$watch( wExp, listener, objEq );
$scope.$watch
function is used when we want to see when a variable is altered. We have to choose three arguments to apply this function: wExp
is what we want to watch, listener
is what we want it to do when it’s updated, and whether we want to check a variable or an object.
We can skip this while using this function when we want to check a variable. Let’s go through an example as shown below.
#AngularJs
$scope.newName = 'Subhan';
$scope.$watch( function( ) {
return $scope.name;
}, function( newVal, oldVal ) {
console.log('$scope.newName is updated!');
} );
Angular will save your watcher function in the $scope
. You can check this by logging the $scope
to the console.
In $watch
, we can also utilize a string in place of a function. This will also work the same as a function works.
When we add a string in the Angular source code, the code will be:
#AngularJs
if (typeof wExp == 'string' && get.constant) {
var newFn = watcher.fn;
watcher.fn = function(newVal, oldVal, scope) {
newFn.call(this, newVal, oldVal, scope);
arrayRemove(array, watcher);
};
}
By applying this code, wExp
will be set as a function. This will designate our listener with the variable with our chosen name.
the $watchers
Function in AngularJS
All the selected watchers will be stored in the $$watchers
variable in $scope
. You will find an array of objects when we check $$watchers
, as shown below.
#AngularJs
$$watchers = [
{
eq: false,
fn: function( newVal, oldVal ) {},
last: 'Subhan',
exp: function(){},
get: function(){}
}
];
unRegWatch
function is returned by the $watch
function. This shows that if we want to allot the initial $scope.$watch
to a variable, we can easily attain this by ordering it to stop watching.
Just confirm we have opened and looked at the first $scope
logged before disconnecting the watcher.
the $scope.$apply
Function in AngularJS
When we try to run controller/directive/etc.
, Angular runs a function within which we call $scope.$watch
. Before administering the $digest
function in the rootScope
, the $apply
function will run a function we have chosen.
The Angular $apply
function is as follows:
#AngularJs
$apply: function(expr) {
try {
beginPhase('$apply');
return this.$eval(expr);
} catch (e) {
$exceptionHandler(e);
} finally {
clearPhase();
try {
$rootScope.$digest();
} catch (e) {
$exceptionHandler(e);
throw e;
}
}
}
the expr
Argument in AngularJS
When using $scope.$apply
, sometimes Angular or we would pass through a function, the expr
argument is that function.
While using this function, we won’t find a need to use $apply
most frequently. Let’s see what happens when ng-keydown
uses $scope.$apply
.
To submit the directive, we use the following code.
#AngularJs
var EventNgDirective = {};
forEach(
keydown keyup keypress submit focus blur copy cut paste'.split(' '),
function(newName) {
var newDirectiveName = newDirectiveNormalize('ng-' + newName);
EventNgDirective[newDirectiveName] = ['$parse', function($parse) {
return {
compile: function($element, attr) {
var fn = $parse(attr[directiveName]);
return function ngEventHandler(scope, element) {
element.on(lowercase(newName), function(event) {
scope.$apply(function() {
fn(scope, {$event:event});
});
});
};
}
};
}];
}
);
This code will loop across the contrasting types of events that can be fired, generating a new directive called ng(eventName)
. When the compile function of the directive is concerned, an event handler is recorded on the element, where the event contains the directive’s name.
When this event is being fired, $scope.$apply
will be run by Angular, offering it a function to run.
That’s how the $scope
value will be updated with the element value, but this binding is just one-way binding. The reason for this is we’ve applied ng-keydown
, which allows us to change only when this event is applied.
As a result, a new value is obtained!
Our main goal is to attain two-way data binding. To achieve this goal, we can use the ng-model
.
For ng-model
to function, it utilizes both $scope.$watch
and $scope.$apply
.
The input we’ve given will join the event handler by ng-model
. At this point, $scope.$watch
is called!
$scope.$watch
is called in the directive’s controller.
#AngularJs
$scope.$watch(function ngModelWatch() {
var value = ngModelGet($scope);
if (ctrl.$modelValue !== value) {
var formatters = ctrl.$formatters,
idx = formatters.length;
ctrl.$modelValue = value;
while(idx--) {
value = formatters[idx](value);
}
if (ctrl.$viewValue !== value) {
ctrl.$viewValue = value;
ctrl.$render();
}
}
return value;
});
When just one argument is used while setting $scope.$watch
, the function we have chosen will be applied even though it is updated or not. The function present in the ng-model
examines whether the model and view are in sync.
If it is out of sync, the function will give the model a new value and update it. This function allows us to know what is the new value by returning the new value when we run this function in $digest
.
Why the Listener Is Not Fired
Let’s go back to the point in the example where we have unenrolled the $scope.$watch
function in the similar function as we have described it. We may now acknowledge the reason for not getting notified about updating $scope.name
even when we have updated it.
As we know, $scope.$apply
is run by Angular on each directive controller function. The $scope.$apply
function will run $digest
only on one condition when we have estimated the directive’s controller function.
This means that we have unenrolled the $scope.$watch
function before it could have been able to perform, so there is a minimum to no chance of it being called.
the $digest
Function in AngularJS
You can call this function on the $rootScope
by $scope.$apply
. We can run the digest cycle on the $rootScope
, and then it will pass over the scopes and instead run the digest cycle.
The digest cycle will fire all our wExp
functions in the $$watchers
variable. It will check them against the last known value.
If the results are negative, the listener will be fired.
In the digest cycle, when it runs, it loops twice. Once, it will loop across the watchers and then loops again until the cycle is not dirty
anymore.
When the wExp
and last known value are not equivalent, we say the cycle is dirty. Usually, this cycle will run one time and give an error if it runs more than ten times.
Angular can run $scope.$apply
on anything that carries a possibility of having a model value changing.
You have to run $scope.$apply();
when we have updated $scope
outside Angular. This will notify Angular that the scope has been updated.
Let’s design a basic version of dirty checking.
All the data we wish to save will be in the Scope
function. We’ll expand the object on a function to duplicate $digest
and $watch
.
Because we don’t need to assess any functions in relation to Scope
, we will not use $apply
. We will directly use $digest
.
This is how our Scope
will look like:
#AngularJs
var Scope = function( ) {
this.$$watchers = [];
};
Scope.prototype.$watch = function( ) {
};
Scope.prototype.$digest = function( ) {
};
There are two parameters, wExp
and listener
, that $watch
should adopt. We can set the values in Scope
.
When $watch
is called, we send these into the $$watcher
value previously stored in Scope
.
#AngularJs
var Scope = function( ) {
this.$$watchers = [];
};
Scope.prototype.$watch = function( wExp, listener ) {
this.$$watchers.push( {
wExp: wExp,
listener: listener || function() {}
} );
};
Scope.prototype.$digest = function( ) {
};
Here we’ll notice that listener
is set to an empty function.
You can easily register a $watch
for all variables when no listener
is offered by following this example.
Now we will operate $digest
. We can inspect whether the old value and the new value are equivalent.
We can also fire the listener if it isn’t already fired. To attain this, we will then loop till they are equivalent.
The dirtyChecking
variable has helped us attain this goal, no matter whether the values are equal.
#AngularJs
var Scope = function( ) {
this.$$watchers = [];
};
Scope.prototype.$watch = function( wExp, listener ) {
this.$$watchers.push( {
wExp: wExp,
listener: listener || function() {}
} );
};
Scope.prototype.$digest = function( ) {
var dirtyChecking;
do {
dirtyChecking = false;
for( var i = 0; i < this.$$watchers.length; i++ ) {
var newVal = this.$$watchers[i].wExp(),
oldVal = this.$$watchers[i].last;
if( oldVal !== newVal ) {
this.$$watchers[i].listener(newVal, oldVal);
dirtyChecking = true;
this.$$watchers[i].last = newVal;
}
}
} while(dirtyChecking);
};
Now, we will design a new example of our scope. We’ll assign this to $scope
; then, we will register a watch function.
Then we will update it and digest it.
#AngularJs
var Scope = function( ) {
this.$$watchers = [];
};
Scope.prototype.$watch = function( wExp, listener ) {
this.$$watchers.push( {
wExp: wExp,
listener: listener || function() {}
} );
};
Scope.prototype.$digest = function( ) {
var dirtyChecking;
do {
dirtyChecking = false;
for( var i = 0; i < this.$$watchers.length; i++ ) {
var newVal = this.$$watchers[i].wExp(),
oldVal = this.$$watchers[i].last;
if( oldVal !== newVal ) {
this.$$watchers[i].listener(newVal, oldVal);
dirtyChecking = true;
this.$$watchers[i].last = newVal;
}
}
} while(dirtyChecking);
};
var $scope = new Scope();
$scope.newName = 'Subhan';
$scope.$watch(function(){
return $scope.newName;
}, function( newVal, oldVal ) {
console.log(newVal, oldVal);
} );
$scope.$digest();
We have successfully implemented dirty checking. You will observe its logs when we spectate the console.
#AngularJs
Subhan undefined
Formerly, $scope.newName
was not defined. We have fixed it for Subhan
.
We will connect the $digest
function to a keyup
event on an input. By doing this, we don’t have to designate it ourselves.
This signifies that we can attain two-way data binding too.
#AngularJs
var Scope = function( ) {
this.$$watchers = [];
};
Scope.prototype.$watch = function( wExp, listener ) {
this.$$watchers.push( {
wExp: wExp,
listener: listener || function() {}
} );
};
Scope.prototype.$digest = function( ) {
var dirty;
do {
dirtyChecking = false;
for( var i = 0; i < this.$$watchers.length; i++ ) {
var newVal = this.$$watchers[i].wExp(),
oldVal = this.$$watchers[i].last;
if( oldVal !== newVal ) {
this.$$watchers[i].listener(newVal, oldVal);
dirtyChecking = true;
this.$$watchers[i].last = newVal;
}
}
} while(dirtyChecking);
};
var $scope = new Scope();
$scope.newName = 'Subhan';
var element = document.querySelectorAll('input');
element[0].onkeyup = function() {
$scope.newName = element[0].value;
$scope.$digest();
};
$scope.$watch(function(){
return $scope.newName;
}, function( newVal, oldVal ) {
console.log('Value updated In Input - The new value is ' + newVal);
element[0].value = $scope.newName;
} );
var updateScopeValue = function updateScopeValue( ) {
$scope.name = 'Butt';
$scope.$digest();
};
With this technique, we can easily update the input’s value, as seen in the $scope.newName
. You can also call updateScopeValue
, and the input’s value will show that.
Rana is a computer science graduate passionate about helping people to build and diagnose scalable web application problems and problems developers face across the full-stack.
LinkedIn