binpress

AngularJS: Looking Under the Hood [Part 2]

angular-js

Last week I published an article that looked under the hood of AngularJS. We covered $scopecreation and how scope inheritance works, Dependency Injection (DI) annotation, Factory versus Service, the $digest cycle and how Angular implements a clever polyfill method using function closures and feature detection.

We’ve got another five secrets from the source code this week, let’s dive in, learn some more about Angular’s internal workings and things I’ve learned along the way that have helped me become a better developer. All code examples are from the Angular 1.3.0-rc.5 release, unless specified.

1) $rootScope closure unbinding

Angular’s core ships with a fantastic easy events API, which mirrors a jQuery-like syntax making it very easy to publish and subscribe to events around your application. Typically, we’d bind a listener, such as $scope.$on(‘myCustomEvent’[, callback]); and fire events with $scope.$emit(‘myCustomEvent’[, data]);. We can also use $broadcast to fire an event. Using $emit will dispatch the event up the scope, whereas $broadcast dispatches downwards.

One thing you might come across is communicating application-wide, which you can’t always use the local $scope. Due to it’s parent/child/sibling relationship we might need to use $rootScope, which ships with the same APIs. While we’re not going to look into how and why these are used, I loved the way Angular thinks about developer-friendly ways to ship APIs.

If you’re using $rootScope, you’ll know that it’s the core scope, it’s the parent, the owner of all further scopes. This means it’s persistent throughout the application’s lifecycle. Unlike $scopeObjects, the $rootScope is only destroyed when a user closes the application or refreshes the page, $scope could be created/destroyed when navigating different Views, where Controllers are instantiated and destroyed.

Throughout this create/destroy process, Angular fires a $destroy event, which automatically unbinds $scope.$on listeners. When it comes to $rootScope this isn’t the case, we have to manually unbind our listeners or we’ll bump into problems. This is more of a special use case, but we need to tell Angular when to $destroy our listeners.

Let’s checkout the $on method in the source code:

  1. $on: function(name, listener) {
  2.   var namedListeners = this.$$listeners[name];
  3.   if (!namedListeners) {
  4.     this.$$listeners[name] = namedListeners = [];
  5.   }
  6.   namedListeners.push(listener);
  7.  
  8.   var current = this;
  9.   do {
  10.     if (!current.$$listenerCount[name]) {
  11.       current.$$listenerCount[name] = 0;
  12.     }
  13.     current.$$listenerCount[name]++;
  14.   } while ((current = current.$parent));
  15.  
  16.   var self = this;
  17.   return function() {
  18.     namedListeners[namedListeners.indexOf(listener)] = null;
  19.     decrementListenerCount(self, 1, name);
  20.   };
  21. }

We’re not going to look at the internal workings here, but essentially Angular creates an Array of listeners for a particular event name. The line to note here is return function() {} at the bottom. This removes that particular listener and decrements the count once the returned closure is called. This means that $on actually returns a function, and is automatically called for $scope.$onlisteners, but for $rootScope.$on listeners we need to manually do this. I’ll use a variable called unbind to describe how it works.

  1. var unbind = $rootScope.$on(‘myCustomEvent’, function (event, data) {
  2.   // run some code, access to `event` and `data` arguments
  3. });

At this point, our unbind variable will contain a function reference to the returned closure from $on, should we call this, the event listener will be unbound. We’ll need to setup the current $scopeso that when it’s destroyed (typically a Controller) we can unbind our $rootScope listener at the same time.

  1. var unbind = $rootScope.$on(‘myCustomEvent’, function (event, data) {
  2.   // run some code, access to event and data arguments
  3. });
  4.  
  5. $scope.$on(‘$destroy’, unbind);

And that’s it, I love this simple but effective way of using a function closure to give a developer a really easy API and utmost control.

2) $scope.$watch arguments magic

Inside Angular Controllers, we can bind local $scope observers that watch for model changes, which could be a String, Array, Object or anything else we like.

If you’re using $scope.$watch, you’re probably doing something like this:

  1. $scope.$watch(‘foo’, function (newVal, oldVal) {
  2.   // do things when `foo` changes
  3. });

Let’s take a look at the $watch method in the source code.

  1. $watch: function(watchExp, listener, objectEquality) {
  2.   var get = $parse(watchExp);
  3.  
  4.   if (get.$$watchDelegate) {
  5.     return get.$$watchDelegate(this, listener, objectEquality, get);
  6.   }
  7.   var scope = this,
  8.       array = scope.$$watchers,
  9.       watcher = {
  10.         fn: listener,
  11.         last: initWatchVal,
  12.         get: get,
  13.         exp: watchExp,
  14.         eq: !!objectEquality
  15.       };
  16.  
  17.   lastDirtyWatch = null;
  18.  
  19.   if (!isFunction(listener)) {
  20.     watcher.fn = noop;
  21.   }
  22.  
  23.   if (!array) {
  24.     array = scope.$$watchers = [];
  25.   }
  26.   // We use unshift since we use a while loop in $digest for speed.
  27.   // The while loop reads in reverse order.
  28.   array.unshift(watcher);
  29.  
  30.   return function deregisterWatch() {
  31.     arrayRemove(array, watcher);
  32.     lastDirtyWatch = null;
  33.   };
  34. }

If you check the method’s function arguments, the first is watchExp. Many developers assume we can only pass a single $scope property into a $watch, such as $watch(‘foo’);, however looking at the first line of the function we can see:

  1. var get = $parse(watchExp);

Angular’s internal $parse method will parse whatever is passed in, which means we could pass in an expression, such as $watch(‘foo && bar’);.

This is pretty awesome! What’s even more insane is that we don’t even need to pass a function in as the second argument, we can pass assignment to another $scope Object, here’s an example:

  1. $watch(‘foo && bar’, fooBar = foo + bar’);

Interestingly, $watch also returns a function closure as we saw in our first example in this article, so we can unbind the $watch at our disposal.

3) ng-switch change event

The ng-switch Directive allows us to conditionally swap DOM in and out based on Model data. If you’ve used ng-if (which adds/removes DOM based on truthy/falsy values), then think of ng-switch as ng-if on steroids. Like a true switch statement, we provide cases which Angular will evaluate, it’s an interesting paradigm for this kind of thinking in HTML.

Angular’s removed this change event in 1.3.x releases, but the 1.2.x releases have this undocumented event available – the change event! It’s really useful to know when an ng-switch has fired and made the switch.

A simple example:

  1. <div ng-switch on="vm.signin">
  2.   <div ng-switch-when="false">Sign in</div>
  3.   <div ng-switch-when="true">Sign out</div>
  4. </div>

In the source code powering this Directive, we can take a look what’s happening:

  1. var ngSwitchDirective = ['$animate', function($animate) {
  2.   return {
  3.     restrict: 'EA',
  4.     require: 'ngSwitch',
  5.  
  6.     // asks for $scope to fool the BC controller module
  7.     controller: ['$scope', function ngSwitchController() {
  8.      this.cases = {};
  9.     }],
  10.     link: function(scope, element, attr, ngSwitchController) {
  11.       var watchExpr = attr.ngSwitch || attr.on,
  12.           selectedTranscludes = [],
  13.           selectedElements = [],
  14.           previousElements = [],
  15.           selectedScopes = [];
  16.  
  17.       scope.$watch(watchExpr, function ngSwitchWatchAction(value) {
  18.         var i, ii;
  19.         for (i = 0, ii = previousElements.length; i < ii; ++i) {
  20.           previousElements[i].remove();
  21.         }
  22.         previousElements.length = 0;
  23.  
  24.         for (i = 0, ii = selectedScopes.length; i < ii; ++i) {
  25.           var selected = selectedElements[i];
  26.           selectedScopes[i].$destroy();
  27.           previousElements[i] = selected;
  28.           $animate.leave(selected, function() {
  29.             previousElements.splice(i, 1);
  30.           });
  31.         }
  32.  
  33.         selectedElements.length = 0;
  34.         selectedScopes.length = 0;
  35.  
  36.         if ((selectedTranscludes = ngSwitchController.cases['!' + value] || ngSwitchController.cases['?'])) {
  37.           scope.$eval(attr.change);
  38.           forEach(selectedTranscludes, function(selectedTransclude) {
  39.             var selectedScope = scope.$new();
  40.             selectedScopes.push(selectedScope);
  41.             selectedTransclude.transclude(selectedScope, function(caseElement) {
  42.               var anchor = selectedTransclude.element;
  43.  
  44.               selectedElements.push(caseElement);
  45.               $animate.enter(caseElement, anchor.parent(), anchor);
  46.             });
  47.           });
  48.         }
  49.       });
  50.     }
  51.   };
  52. }];

The undocumented method that you can add on <div ng-switch> is change, which Angular passes into the current $scope using scope.$eval(attr.change);. It grabs the changeattribute and passes it into $eval, this will then fire each time the ng-switch changes. This means we could bind a $scope method to it, note vm.doSomethingOnChange():

  1. <div ng-switch on="vm.signin" change=”vm.doSomethingOnChange()”>
  2.   <div ng-switch-when="false">Sign in</div>
  3.   <div ng-switch-when="true">Sign out</div>
  4. </div>

Remember though, this has recently been removed in 1.3.x releases.

4) Comments are super valuable

When we first start out as developers, we get carried away writing loads of code and coming back to it forgetting what it all does. I’ve been so heavily inspired by the Angular team’s commenting of their code — which is self-documenting and generated for their website — that it’s helped me dramatically improve my coding standards.

We spend more time reading source code than probably writing it, so we should definitely invest time in ensuring other developers and colleagues can jump in and dig into our code with ease.

Angular currently use a custom comment syntax, where they have annotations such as @ngdocwhich parse out the relevant documentation.

There are two things I’ve learned from the Angular comments, the first being a decent description and code examples. Instead of putting functions together and being descriptive with their naming, we can ease off a little and ensure our comments are the means to understanding. Of course, this doesn’t mean we should create vague variable names, though. Our descriptions inside our comments should contain any bitesize code examples that another developer can reference.

Second to code examples in descriptions, comes argument commenting, type notation and a brief description. At first this seems a very tedious process, and we litter our code with annotations such as @param, but these helped me write better code in many ways. For example, understanding what my methods really did better, that they shouldn’t provide multiple roles, and that I want my functions as minimal as possible. Enforcing yourself to stick to a commenting standard will not only make you your team’s favorite developer, you’ll thank yourself when you return to that same piece of code. I highly recommend reading Angular’s source here, but here’s a short example of their tremendous commenting dedication:

  1. /**
  2.  * @ngdoc directive
  3.  * @name ngInit
  4.  * @restrict AC
  5.  *
  6.  * @description
  7.  * The `ngInit` directive allows you to evaluate an expression in the
  8.  * current scope.
  9.  *
  10.  * <div class="alert alert-error">
  11.  * The only appropriate use of `ngInit` is for aliasing special properties of
  12.  * {@link ng.directive:ngRepeat `ngRepeat`}, as seen in the demo below. Besides this case, you
  13.  * should use {@link guide/controller controllers} rather than `ngInit`
  14.  * to initialize values on a scope.
  15.  * </div>
  16.  * <div class="alert alert-warning">
  17.  * **Note**: If you have assignment in `ngInit` along with {@link ng.$filter `$filter`}, make
  18.  * sure you have parenthesis for correct precedence:
  19.  * <pre class="prettyprint">
  20.  *   <div ng-init="test1 = (data | orderBy:'name')"></div>
  21.  * </pre>
  22.  * </div>
  23.  *
  24.  * @priority 450
  25.  *
  26.  * @element ANY
  27.  * @param {expression} ngInit {@link guide/expression Expression} to eval.
  28.  *
  29.  * @example
  30.    <example module="initExample">
  31.      <file name="index.html">
  32.    <script>
  33.      angular.module('initExample', [])
  34.        .controller('ExampleController', ['$scope', function($scope) {
  35.          $scope.list = [['a', 'b'], ['c', 'd']];
  36.        }]);
  37.    </script>
  38.    <div ng-controller="ExampleController">
  39.      <div ng-repeat="innerList in list" ng-init="outerIndex = $index">
  40.        <div ng-repeat="value in innerList" ng-init="innerIndex = $index">
  41.           <span class="example-init">list[ {{outerIndex}} ][ {{innerIndex}} ] = {{value}};</span>
  42.        </div>
  43.      </div>
  44.    </div>
  45.      </file>
  46.      <file name="protractor.js" type="protractor">
  47.        it('should alias index positions', function() {
  48.          var elements = element.all(by.css('.example-init'));
  49.          expect(elements.get(0).getText()).toBe('list[ 0 ][ 0 ] = a;');
  50.          expect(elements.get(1).getText()).toBe('list[ 0 ][ 1 ] = b;');
  51.          expect(elements.get(2).getText()).toBe('list[ 1 ][ 0 ] = c;');
  52.          expect(elements.get(3).getText()).toBe('list[ 1 ][ 1 ] = d;');
  53.        });
  54.      </file>
  55.    </example>
  56.  */
  57. var ngInitDirective = ngDirective({
  58.   priority: 450,
  59.   compile: function() {
  60.     return {
  61.       pre: function(scope, element, attrs) {
  62.         scope.$eval(attrs.ngInit);
  63.       }
  64.     };
  65.   }
  66. });

It’s totally okay, in my opinion, to have as much (or even more) comment blocks than the code itself. You’ll notice you improve how you section your code and become a better code manager in doing so.

5) How <script type=”text/ng-template”> works

If you’ve ever used a Directive, you’ll know there are two different ways to specify a template. The first, as a template property on the Directive’s Object, the second is templateUrl, which Angular will fetch via GET to a server resource. This second way of specifying a template however comes with another option of using <script type=”text/ng-template”> tags to denote the resource, instead of fetching it from a server, improving Directive load performance.

I’ve omitted the comments here, and it’s a very simple Directive. Angular simply checks all <script>tags to see if type=”text/ng-template” is present. If it’s present, Angular then grabs the content of that <script> tag and dumps it straight into the $templateCache. This cache serves a resource point after Directive templates are fetched, and they’re simply looked up again in the cache to enhance performance in the case the Directive is used elsewhere.

  1. var scriptDirective = ['$templateCache', function($templateCache) {
  2.   return {
  3.     restrict: 'E',
  4.     terminal: true,
  5.     compile: function(element, attr) {
  6.       if (attr.type == 'text/ng-template') {
  7.         var templateUrl = attr.id,
  8.             // IE is not consistent, in scripts we have to read .text but in other nodes we have to read .textContent
  9.             text = element[0].text;
  10.         $templateCache.put(templateUrl, text);
  11.       }
  12.     }
  13.   };
  14. }];

Summing up

Learning from the source code isn’t always about learning fancy tricks (which could change at any time as they’re undocumented). We can become better developers by learning from other teams and projects, and adopting some of their practices.

Angular has an enormous code base, and it has many more hidden talents. I encourage you to look through and find some of them for yourself. It’ll open your eyes to understanding bigger code bases and how Angular really works.

Author: Todd Motto

Scroll to Top