AngularJS: Looking under the hood [Part 1]

1 TM Todd Motto Oct 7, 2014

The more AngularJS you write, the more you marvel at its magic. I got pretty curious about some of the fantastic things Angular does and decided to dive into the source code and see if I could reveal some of its secrets. I’ve documented some of the things I’ve found really useful that explain some of the more advanced (and hidden!) side of Angular in its 23,000+ lines of source code!

1) Dependency Injection annotation process

Dependency Injection (DI) is a way of asking for dependencies rather than having our code fetch or build it. Simply put, dependencies are passed to us as an injection. Angular allows us to use DI in our applications through methods like Controllers and Directives. We can create our own dependencies and allow Angular to inject them where we ask for them.

One of the most common dependencies we might ask for in Angular is $scope. This might be injected like so:

function MainCtrl ($scope) {
  // access to $scope
}
angular
  .module(‘app’)
  .controller(‘MainCtrl’, MainCtrl);

To a JavaScript developer who’s never used Dependency Injection the way Angular does, this looks like a locally scoped variable name. In fact, it’s just a placeholder for a dependency name that we’re asking for. Angular looks at these placeholders and passes the real dependencies to us via DI, let’s take a closer look.

Function Arguments

These work perfectly until we minify our app. When you minify your code, your function declarations will have letters for each parameter instead of words - meaning Angular will not be able to find what you want! Angular uses a function that we can call on functions: toString(). This will return the function as a string! That then gives us access to what parameters are being requested. Angular does this via four different Regular Expressions (RegExps), and each plays a special role in the way dependencies are fetched from the arguments of our functions.

var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;

The first thing Angular does is convert a function to a String, which is a really powerful feature of the JavaScript language. This would give us our function as a String like so:

‘function MainCtrl ($scope) {...}’

From here, Angular removes any comments between the function () parenthesis.

fnText = fn.toString().replace(STRIP_COMMENTS, '');

Angular then separates the parameters from the rest of the function to fetch the parts it really needs.

argDecl = fnText.match(FN_ARGS);

Angular then uses .split() to remove any whitespace, and we have an array of the requested parameters, in order — perfect! Angular uses an internal forEach function to iterate over this Array of matched argument names and pushes them into an $inject Array.

forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) {
  arg.replace(FN_ARG, function(all, underscore, name) {
    $inject.push(name);
  });
});

This is an expensive process as you can imagine. Four RegExp lookups and a lot of parsing for every function - that’s going to give us a performance hit. As we end up with an $inject Array here that Angular has abstracted, we can cut straight to the chase and populate the $inject Array to save Angular all this hard work and costly performance time.

$inject

We can specify the dependencies ourselves by adding an $inject property to our function, which, if present, Angular uses for DI annotations. It’s easily the most readable syntax. Here's an example:

function SomeCtrl ($scope) {

}

SomeCtrl.$inject = ['$scope'];

angular
  .module('app', [])
  .controller('SomeCtrl', ['$scope', SomeCtrl]);

This saves Angular a lot of work - instead of having to check function arguments, or manipulate an Array (see next section on Array arguments), it just returns and runs with the $inject array we've already given it. Simple, and very performant!

Ideally we would use a task runner such as Grunt.js or Gulp.js to automate the $inject task or Array syntax, as it becomes costly in our own time as well as Angular’s parsing time.

Note: This doesn't instantiate any Services that you pass it through. All Angular does is annotate the relevant names — another piece of the framework takes care of the Object injection.

Array Arguments

The final example uses the commonly seen Array syntax in which the index of each Array item has to correspond to the function argument order. For example, [‘$scope’, function ($scope) {}].

This order is important as the function arguments need to follow the same order to avoid dependencies being incorrectly instantiated as other ones and likely throwing errors.

function SomeCtrl ($scope, $rootScope) {

}

angular
  .module('app', [])
  .controller('SomeCtrl', ['$scope', ‘$rootScope’, SomeCtrl]);

This is neat - all we need to do is pass in the function as the last item of the Array and Angular removes it and loops over the Array of annotated names as if it were an $inject property we created above. When Angular annotates a function, it checks to see if it is an Array, and if so, the function is the last item, and the dependencies are others.

} else if (isArray(fn)) {
  last = fn.length - 1;
  assertArgFn(fn[last], 'fn');
  $inject = fn.slice(0, last);
}

2) Factory vs Service

Factory and Services are very similar and often difficult to understand for developers.

When a .service() is instantiated, new Service() is called under the hood, giving us a new instance. Essentially, the .service() acts as a Constructor function.

A service is basically a factory, however it is instantiated on creation, therefore you’ll need to use this to register variables and functions in the service, instead of returning an object like you would in the factory.

Factories are very close to the object-oriented “factory pattern.” When you inject the factory, you are given the whole function, allowing you to create new instances as you please - essentially an Object for creating new Objects.

You can see the internal workings in the Angular source code here:

  function factory(name, factoryFn) { return provider(name, { $get: factoryFn }); }

  function service(name, constructor) {
    return factory(name, ['$injector', function($injector) {
      return $injector.instantiate(constructor);
    }]);
  }

3) New $scope creation from $rootScope

Scopes in Angular all descend from the $rootScope. The $rootScope is created via new Scope(), and further child scopes are created using $scope.$new().

var $rootScope = new Scope();

Inside the $new method, Angular sets up a Prototype chain to allow scopes to reference their parent, its children's tails (for the digest cycle), and previous sibling scope.

From the code below, if you request an isolate scope, it creates a new Scope() - else it creates a child scope that directly inherits from the parent.

I've omitted some of the unnecessary code below, but here’s the gist of it:

  $new: function(isolate) {
    var child;

    if (isolate) {
      child = new Scope();
      child.$root = this.$root;
    } else {
      // Only create a child scope class if somebody asks for one,
      // but cache it to allow the VM to optimize lookups.
      if (!this.$$ChildScope) {
        this.$$ChildScope = function ChildScope() {
          this.$$watchers = null;
        };
        this.$$ChildScope.prototype = this;
      }
      child = new this.$$ChildScope();
    }
    child['this'] = child;
    child.$parent = this;
    return child;
  }

This is also great to understand for testing purposes as you need to create $scope when testing Controllers using $scope.$new(). This helped clear things up a little for me on how Angular creates these new scopes and why they’re imperative to test driven development with the Angular mocks module.

4) Digest Cycle

The digest cycle, often seen as $digest, is what powers Angular’s two way data-binding. It runs whenever a model value is expected to update, checks the last known value for it, and if it is different, fires the appropriate listener. These are the basics of dirty checking — it checks all the possible values against their old values, and if they're dirty, it fires the listener associated with it, until it is no longer dirty (matches the old value). Let's have a quick look at how it works.

$scope.name = 'Todd';

$scope.$watch(function() {
    return $scope.name;
}, function (newValue, oldValue) {
    console.log('$scope.name was updated!');
} );

When you call $scope.$watch, you register two things. The first parameter is a function that returns a value that you want to watch (when you provide a string, Angular converts it to a function). This way when the digest cycle runs, the watch parameter can be called, returning any value you want. The second parameter is what function will run when the first parameters value gets changes. Let's have a look at Angular registering the watch.

  $watch: function(watchExp, listener, objectEquality) {
    var get = $parse(watchExp);

    if (get.$$watchDelegate) {
      return get.$$watchDelegate(this, listener, objectEquality, get);
    }
    var scope = this,
        array = scope.$$watchers,
        watcher = {
          fn: listener,
          last: initWatchVal,
          get: get,
          exp: watchExp,
          eq: !!objectEquality
        };

    lastDirtyWatch = null;

    if (!isFunction(listener)) {
      watcher.fn = noop;
    }

    if (!array) {
      array = scope.$$watchers = [];
    }
    // we use unshift since we use a while loop in $digest for speed.
    // the while loop reads in reverse order.
    array.unshift(watcher);

    return function deregisterWatch() {
      arrayRemove(array, watcher);
      lastDirtyWatch = null;
    };
  }

This function pushes the parameters you pass into the $$watchers array in the scope, and returns a function for when you want to stop watching.

Then, the digest cycle runs whenever $scope.$apply or $scope.$digest runs. $scope.$apply is a wrapper for a digest on the rootScope, which then propagates down the scopes, whereas $scope.$digest only runs in the current scope (and downward from there).

  $digest: function() {
    var watch, value, last,
        watchers,
        asyncQueue = this.$$asyncQueue,
        postDigestQueue = this.$$postDigestQueue,
        length,
        dirty, ttl = TTL,
        next, current, target = this,
        watchLog = [],
        logIdx, logMsg, asyncTask;

    beginPhase('$digest');

    lastDirtyWatch = null;

    do { // "while dirty" loop
      dirty = false;
      current = target;

      traverseScopesLoop:
      do { // "traverse the scopes" loop
        if ((watchers = current.$$watchers)) {
          // process our watches
          length = watchers.length;
          while (length--) {
            try {
              watch = watchers[length];
              // Most common watches are on primitives, in which case we can short
              // circuit it with === operator, only when === fails do we use .equals
              if (watch) {
                if ((value = watch.get(current)) !== (last = watch.last) &&
                    !(watch.eq
                        ? equals(value, last)
                        : (typeof value === 'number' && typeof last === 'number'
                           && isNaN(value) && isNaN(last)))) {
                  dirty = true;
                  lastDirtyWatch = watch;
                  watch.last = watch.eq ? copy(value, null) : value;
                  watch.fn(value, ((last === initWatchVal) ? value : last), current);
                  if (ttl < 5) {
                    logIdx = 4 - ttl;
                    if (!watchLog[logIdx]) watchLog[logIdx] = [];
                    logMsg = (isFunction(watch.exp))
                        ? 'fn: ' + (watch.exp.name || watch.exp.toString())
                        : watch.exp;
                    logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last);
                    watchLog[logIdx].push(logMsg);
                  }
                } else if (watch === lastDirtyWatch) {
                  // If the most recently dirty watcher is now clean, short circuit since the remaining watchers
                  // have already been tested.
                  dirty = false;
                  break traverseScopesLoop;
                }
              }
            } catch (e) {
              $exceptionHandler(e);
            }
          }
        }

      } while ((current = next));

      // `break traverseScopesLoop;` takes us to here

      if((dirty || asyncQueue.length) && !(ttl--)) {
        clearPhase();
        throw $rootScopeMinErr('infdig',
            '{0} $digest() iterations reached. Aborting!\n' +
            'Watchers fired in the last 5 iterations: {1}',
            TTL, toJson(watchLog));
      }

    } while (dirty || asyncQueue.length);

    clearPhase();

    while(postDigestQueue.length) {
      try {
        postDigestQueue.shift()();
      } catch (e) {
        $exceptionHandler(e);
      }
    }
  }

The code here is genius - whilst I'm not going to get into how it propagates down scopes, in a general gist it loops over the $$watchers in the scope, executes the watcher (the first parameter you pass through to $scope.$watch) and if the value it returns differs from the last known value, it will run the listener (the second parameter you pass through). And ta-da, we have a notification as soon as a variable is updated. The thing is, how do we know when a value may update? Quite simple, the digest cycle is run when a value is expected to be updated (even though it might not necessarily be!). For example, ng-model on an input runs the digest cycle on every keydown event!

$scope.$apply

When you do anything outside of the Angular framework, such as a setTimeout function, you need to let Angular know that you've possibly changed a model value. You'd do this using $scope.$apply, which, if you pass in a function, will execute that function in the context of the scope, and then run the $digest cycle on the $rootScope. This will then propagate down all the scopes, meaning that all the listeners/watchers you have registered will be checked. This means that you can update any scope variable, and Angular will know that you have!

5) Polyfilling by feature detection and closures

The way Angular does Polyfilling is superb. It doesn’t just hit the Prototype of an Object directly, such as patching Function.prototype.bind, for example. Angular creates a function that works out whether that method is supported in the browser (basic feature detection) and then serves it back to us if it exists. If it doesn’t exist, Angular will have a small patch that silently polyfills it for us.

This is a really safe way of doing things, as extending the Prototype directly can conflict with other libraries or frameworks (or even our own code) that do it directly. Closures also give us the safety of function scope to store temporary variables to make calculations, which if the native method exists, Angular uses. This is a great performance enhancement as native methods are usually blazingly fast.

Function feature detects

Angular supports (at the time of writing, with 1.2.x) IE8+, which means it needs to polyfill the features that the older browsers do not have. We'll look at indexOf in this example.

function indexOf(array, obj) {
  if (array.indexOf) return array.indexOf(obj);

  for (var i = 0; i < array.length; i++) {
    if (obj === array[i]) return i;
  }
  return -1;
}

Instead of calling array.indexOf directly, it just creates its own indexOf function that it uses everywhere, which of course if the browser supports it, returns the native indexOf function. Otherwise, it uses a polyfill. Simple!

This means that it doesn't hack around with changing the Prototype of the Object, instead it uses feature detection to be smart and performance-oriented.

Closures

The closure method uses an immediately-invoked function expression (IIFE), which is called by the JavaScript engine at runtime. In this isArray example, the closure is called and if the feature doesn’t exist, Angular returns a function that looks like a proper Array.isArray method, but is really just a small patch that does the same thing. If Array.isArray is a function, Angular will return Array.isArray and we can use the native implementation — another performance gain. The IIFE serves as a great way to encapsulate things at runtime and return the function we need.

var isArray = (function() {
  if (!isFunction(Array.isArray)) {
    return function(value) {
      return toString.call(value) === '[object Array]';
    };
  }
  return Array.isArray;
})();

That’s it for part one of my look at Angular’s source code. Part two will arrive next week and cover the second batch of nuggets I've dug up. Sign up below to receive alerts about new tutorials!

AngularJS: Looking under the hood continues in Part 2.

1 comment


Or enter your name and Email
  • L Ludek 3 years ago
    Hey Todd. In the section on $inject property you are using $inject AND array arguments together in your example. I was under the impression that this would be sufficient: function SomeCtrl ($scope) {} SomeCtrl.$inject = ['$scope']; angular .module('app', []) .controller('SomeCtrl', SomeCtrl); Is there any reason for your usage of array annotation together with $inject or is just result of copy/paste error when creating examples?