How to Do Dirty Checking in AngularJS

Rana Hasnain Khan Feb 02, 2024
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 Hasnain Khan avatar Rana Hasnain Khan avatar

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