Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Max Pappas
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Max Pappas@maxian )

Using $rootScope.$emit() As A Performance Optimization In AngularJS

By Ben Nadel on

Yesterday, I looked at how to create a simple modal window system in AngularJS. In that exploration, I used the $rootScope to bridge the gap between the modal Service and the modal directive. In the past, I've used the same approach to bridge the gap between a global uploader service and its Plupload directive. As it turns out, the scope tree acts as a wonderful pub/sub (Publish and Subscribe) mechanism; and, in certain edge-cases we can optimize our events by using the $rootScope.$emit() method.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

When it comes to scope tree events, there are two event broadcasting methods: scope.$broadcast() and scope.$emit(). The $broadcast() method will send events down through the scope tree's descendants. The $emit() method will send events up through the scope tree's ancestors. When you bind to an event using the scope.$on() method, your handler will be invoked regardless of how the original event was triggered (ie, broadcast vs emit).

In most cases, the $broadcast() method makes the most sense as you cannot be sure which components in the application will want to know about your event. But, in certain edge-cases, you need to use events to create communication between two very specific components (such as a modal window Service and a modal window Directive). In these cases, using $rootScope.$emit() can provide a small performance optimization over $rootScope.$broadcast().

NOTE: I picked this idea up from Nicolas Bevacqua's excellent article on the AngularJS internals.

The optimization is a byproduct of the scope tree structure. Since the $rootScope has no parent (ancestors), an event, $emit()'d event on the $rootScope, has no where to go. As such, it triggers the $rootScope-bound handlers and then ends its lifecycle.


 
 
 

 
 Using $rootScope.$emit() as a possible performance optimization in AngularJS. 
 
 
 

If a service needs to communicate with a directive, they can both use the $rootScope as their pub/sub beacon and the overhead of the event is entirely eliminated. To see what this might look like, I've tried to boil it down to a very simple demo in which a directive needs to listen for events triggered by the service layer:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Using $rootScope.$emit() As A Performance Optimization In AngularJS
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body>
  •  
  • <h1>
  • Using $rootScope.$emit() As A Performance Optimization In AngularJS
  • </h1>
  •  
  • <ul>
  • <li
  • ng-repeat="i in [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]"
  • bn-toggle>
  • {{ i }} - CSS class will toggle based on event.
  • </li>
  • </ul>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.3.15.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I manage some service functionality, detached from any concept of the DOM.
  • // --
  • // NOTE: We are passing in the $rootScope object. From a service-layer
  • // standpoint, the $rootScope is nothing more than a publish and subscribe
  • // (PUB / SUB) mechanism that the service layer can use to broadcast events.
  • app.factory(
  • "someService",
  • function( $interval, $rootScope ) {
  •  
  • var intervalCount = 0;
  •  
  • // Kick off an interval that will emit events.
  • $interval( operator, 1000 );
  •  
  • // Return the public API.
  • return({
  • count: count
  • });
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I return the number of intervals that have passed.
  • function count() {
  •  
  • return( intervalCount );
  •  
  • }
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I execute the interval operation.
  • function operator() {
  •  
  • // When the interval runs, we are going to EMIT an event on the
  • // $rootScope. By using $emit(), instead of $broadcast(), it will
  • // isolate the event landscape to a single scope - the $rootScope.
  • // Since $emit() goes "up" in the scope tree, and there's nowhere to
  • // go "up" from the $rootScope, this will limit event handler
  • // invocation to those handlers that have specifically bound to the
  • // $rootScope (and not the relevant "local" scope).
  • $rootScope.$emit( "interval.changed", ++intervalCount );
  •  
  • };
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I render the toggle component.
  • // --
  • // NOTE: We are injecting the $rootScope to act as our pub-sub mechanism.
  • app.directive(
  • "bnToggle",
  • function( $rootScope, someService ) {
  •  
  • // Return the directive configuration.
  • return( link );
  •  
  •  
  • // I bind the JavaScript events to the local scope.
  • function link( scope, element, attributes ) {
  •  
  • // Listen for the interval event on the $rootScope. Since the original
  • // event is being announced via $emit(), it will never show up on the
  • // local directive-scope (which is a descendant of $rootScope).
  • // --
  • // NOTE: We could have accomplished the same thing by doing something
  • // like binding a $watch() on the .count() method result; however,
  • // this would have required a check on every single digest, which is
  • // less than optimal (considering that we have a delineated event).
  • // --
  • // CAUTION: This event handler WILL NOT BE AUTOMATICALLY DEREGISTERED
  • // when the local scope is destroyed. As such, if you ever expect this
  • // scope to be destroyed, you MUST explicitly invoke the deregister()
  • // function returned by the event binding.
  • var deregister = $rootScope.$on(
  • "interval.changed",
  • function() {
  •  
  • console.log( "Responding to event %s.", someService.count() );
  •  
  • if ( Math.random() > .5 ) {
  •  
  • element.addClass( "on" );
  •  
  • } else {
  •  
  • element.removeClass( "on" );
  •  
  • }
  •  
  • }
  • );
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, the service object uses $rootScope.$emit() to announce interval events and the directive uses $rootScope.$on() to listen for those events. The directive can't use scope.$on(), in this case, because the emitted event never comes down through the scope tree.

The obvious downside to this is that the $rootScope-bound event listener will not automatically unbind itself when the directive scope is destroyed. Therefore, if your directive is ever destroyed, you have to be sure to explicitly unbind the event handler; otherwise, it will just stick around forever, causing a memory leak at best and unexpected behavior at worst.

Now, as it turns out, the $broadcast() algorithm, in AngularJS, is heavily optimized; AngularJS will only traverse areas of the scope tree that are known to have bound-listeners for a certain event type. So, while the $rootScope.$emit() approach is faster, it may also be unnecessary in recent releases of AngularJS. And, considering the complexity of having to manually unbind events, the $rootScope optimization may be more trouble than it's worth. That said, it's definitely an interesting approach - one well worth pondering.




Reader Comments

It's nice to see a great example using $emit because for a while it felt just like a leftover from the early days of Angular. Thanks for putting that together Ben!

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.