Understanding the AngularJs Digest Loop


When you first start working with a large monolithic framework like AngularJS, you may not want to or care to understand the inner workings of the framework. Once you start to get more comfortable, you'll naturally wonder how things actually work under the hood and how you can write better code.

If you have yet to understand the $digest loop, don't worry, I'll cover it in great detail in this post


Two Way Data-binding - The Wizardry

To truly understand how two way data-binding works within AngularJS, we need to start collecting a whole array of concepts and tie them together.

I'll warn you, if you aren't already familiar with JavaScript or the basics behind AngularJS, you might have some trouble following along. As a prerequisite, I urge you to review the fundamental posts on my blog and the AngularJS documentation.

AngularJS Documentation


Wizard lol


Browser Event Handling

Your browser is constantly executing callbacks based on various events such as click, keypress, focus, change, scroll, and blur events, etc.

You can tie into these events via JavaScript addEventListener function. Here is an example:



    function modifyText(newValue) {
        var someRandomPageElement = document.getElementById("someRandomPageElement");
        someRandomPageElement.firstChild.nodeValue = newValue;
    }

    var element = document.getElementById("someOuterDivElement");

    element.addEventListener("click", function eventHandler() {
        modifyText("bar")
    });

Nothing magical here, just plain ol' JavaScript.

There are many more JavaScript functions that you can use to kick off events, attach events and set listeners. The short list as follows:

  • EventTarget.addEventListener()
  • EventTarget.detachEvent()
  • EventTarget.dispatchEvent()
  • EventTarget.fireEvent()
  • EventTarget.removeEventListener()

$digest Loop

The Angular context includes the Angular event loop, which ties directly into the above browser event flow. There are various components to the Angular event loop, and we'll start the discussion with $watch and $watch list.


digest loop by angularjs


$watch and the $watch list

Every model binding on the UI within the Angular context is given the $watch function and is added to the $watch list. The list of $watch functions are resolved during the $digest loop via dirty checking. The beauty here is that even if $scope does not have that property, it will still be registered with a $watch function and added to the $watch list.


        <textarea ng-model="lifeStory" rows="10" cols="50">
        </textarea>

        <span>{{lifeStory}}</span>
    

In the above code there was a single $watch function that was registered with the $watch list which will be resolved through the $digest loop.

Here is an inside look into the $watch function that you can create yourself:


    //$watch(watchExpression, listener, [objectEquality])

    scope.numberOfTimesExecuted = 0;
    scope.EverInitialized = false;

    scope.$watch("lifeStory", function(newValue, oldValue) {
        if(newValue ==== oldValue){
            scope.EverInitialized = true;
        }
        scope.numberOfTimesExecuted = scope.numberOfTimesExecuted + 1;
});

You don't need to setup any of this manually, all the heavy lifting has already been done for you by the Angular framework. But how does Angular know when to check for changes and how to update the bindings? That is where the concept of dirty checking comes into play!

NOTE: The concept of dirty checking isn't some new coding phenomenon that the Angular founder imagined, it's widely used in technologies such as EntityFramework, NHibernate, etc.



Dirty Checking

We know that the Angular context keeps track of all of the UI bindings with a $watch function and adds it to the $watch list. Dirty Checking in essence walks down the $watch list and compares the oldValue to the newValue.

If there are no changes to the specific binding being watched, we proceed by moving on to the next item in the $watch list. If there is a change, we update the value, then continue on.


digest loop by angularjs

So once we traversed all of the $watch functions in the $watch list, are we done with the $digest loop? No, we go through the list ONE more time and verify that nothing has changed. We do this because there could have been a change to one of the values when another $watch item updated it. We continue through this loop until no changes are present in any of the values.

If we go through this loop more than 10 times in a single $digest loop, the application will die. This is necessary to stop you from blowing up the browser by going into an infinite loop.


Dirty Checking 24/7?

No, not realy, but sort of. Let's see what's happening with our previous example with a textarea and an submit button:


        <textarea ng-model="lifeStory" rows="10" cols="50">
        </textarea>
        <span>{{lifeStory}}</span>
        <input type="submit" ng-click="activateReader()" value="blast off" />
    

The $digest loop runs as you type in the textarea box. Once you are done and the $digest loop has ran through the $watch list(recall there is a final loop) and nothing has updated, it exits.

Remember how we talked about the browser event flow? The Angular context ties directly into this flow and performs the $digest loop. But how is the $digest loop triggered off of the browser events?

The $digest loop is triggered by calling the $apply() function within the angular directive ngClick.


Using $apply()

Simply put, $apply() executes an expression inside Angular when the call site is outside of the Framework, think jQuery. $apply() is necessary for DOM events, setTimeout, XHR(http), or any kind of 3rd party libraries that aren't part of Angular. Most 3rd party libraries built ontop of angular such as ui-router or ui-utils will call $apply() for us, so you won't need to do it explicitly.

$apply can optionally take a function or string. If you pass a function, it will be executed on the scope thats passed to the function. For instance:

        $scope.$apply(function(scope){
            scope.lifeStory = "Once upon a time I woke up in a forrest..."
        });
    

Calling$scope.$apply() forces a $digest loop. When third party libraries interact with an element that has a binding, the Angular context doesn't know anything about this unless it is notified.

Because Angular knows nothing about this interaction, it has no chance to update the values via the $digest loop. Be careful when using 3rd party libraries with Angular.


scope apply lol

Do not call $apply() in a controller because you will have a hell of a time testing that controller later, refactor this to a service that handles the logic for you.


Angular Performing the Heavy Lifting

Almost every single Angular directive internally will call $apply() for us. For instance, lets look at ngClick:

//Source Code Above
        forEach(
  'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
  function(eventName) {
    var directiveName = directiveNormalize('ng-' + eventName);
    ngEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse, $rootScope) {
      return {
        restrict: 'A',
        compile: function($element, attr) {
          // We expose the powerful $event object on the scope that provides access to the Window,
          // etc. that isn't protected by the fast paths in $parse.  We explicitly request better
          // checks at the cost of speed since event handler expressions are not executed as
          // frequently as regular change detection.
          var fn = $parse(attr[directiveName], /* interceptorFn */ null, /* expensiveChecks */ true);
          return function ngEventHandler(scope, element) {
            element.on(eventName, function(event) {
              var callback = function() {
                fn(scope, {$event:event});
              };
              if (forceAsyncEvents[eventName] && $rootScope.$$phase) {
                scope.$evalAsync(callback);
              } else {
                scope.$apply(callback);
              }
            });
          };
        }
      };
    }];
  }
);
//Source Code Below
    

Angular JS ngEvent Directives

Notice $evalAsync and $apply being called.


$evalAsync List

There is a key aspect that we forgot to mention. Actually, we didn't forget, it just seems much more appropriate to talk about it now that we covered the "basics".

Along with the $watch list, the the $digest loop runs through the $asyncQueue. We can tie functions or strings into $evalAsync to execute after the function that scheduled the evaluation.


Hopefully that gets you a little more familiar with the $digest loop and helps clear up some of the magic behind the scenes. You're now one step closer to truly understanding AngularJS!


I know AngularJS lol

[Date Edited: 10/9/2016 5:20:16 AM ]

Leave a Comment