Friday, November 28, 2014

The Top 5 Mistakes AngularJS Developers Make Part 2: Abusing $watch

This is the second part in a five-part series that covers common AngularJS mistakes. To recap, the top five mistakes I see people make are:

  1. Heavy reliance on $scope (not using controller as) 
  2. Abusing $watch
  3. Overusing $broadcast and $emit
  4. Hacking the DOM
  5. Failing to Test

When I posted the first article, one comment suggested that using controller as is fine for smaller controllers but large, complex controllers with dependencies might not work out as well. I disagree, and you’ll see why later in this series. I’ll get more into dependencies in the next post, but for now I’d like to take the concept of $scope one step further to discuss the second mistake.

Abusing $watch

There are plenty of reasons why you want to watch for changes to certain properties and respond. It is quite common on a complex form to render information or process an algorithm based on a selection. To keep the example simple I created a contrived scenario, but bear with me because I think you’ll see how it extrapolates to common, real world examples.

Let’s assume you are rendering a drop-down that enables the user to select gender. In a real world example you might have some specific algorithms to run or text to display based on the selection, but for our example we’ll simply display some different text based on the gender they choose.

<div ng-app="myApp">
    <div ng-controller="badCtrl">
        <select ng-options="g as g for g in genders"
                ng-model="selectedGender"></select>
        It's a {{genderText}}!
    </div>
</
div>

The script simply keeps track of two lists and watches for changes. If the gender changes, a property is updated to show the corresponding label.

(function (app) {
 
    var genders = ['Male', 'Female'],
        labels = ['boy', 'girl'];
    function BadController($scope) {
        $scope.genders = genders;
        $scope.selectedGender = genders[0];
        $scope.$watch('selectedGender', function () {
            $scope.genderText =
                $scope.selectedGender === genders[0]
                ? labels[0] : labels[1];
        });
    }

    app.controller('badCtrl', BadController);
})(angular.module('myApp', []));

I’ve had to build the controller with $scope because I have to $watch for the changes and the only way to $watch is by having a reference to $scope, right? Perhaps. When we run this example the watch tree looks like this:

watchtree1

For performance the majority of time is spent with the model watch, and the least amount of time on the watch for the property selectedGender that we added explicitly. That time can grow, however, and add complexity as I’ll get to in a moment. Is it even possible to “watch” for a change using controller as? Here is some new HTML:

<div ng-app="myApp">
    <div ng-controller="goodCtrl as ctrl">
        <select ng-options="g as g for g in ctrl.genders"
                ng-model="ctrl.selectedGender"></select>
        It's a {{ctrl.genderText}}!
    </div>
</
div>

Here is the watch tree when running the new example:

watchtree2

As you can see, there is one less watch. Performance-wise the existing watches are about the same but we’ve completely eliminated the overhead of the explicit watch. Although it was only milliseconds on a desktop browser, if we extrapolate to a complex controller with dozens of watches you can imagine it adds up quickly and will be noticeable on a mobile device.

In this case, the combined watches on the bad controller averaged about 5.264ms of overhead, while the combined watches on the good controller averaged about 4.312ms of overhead. Doesn’t seem like much? The second approach averaged a 20% improvement for just one property. Consider controllers with multiple watches and eventually you will see tangible differences in the response time of your application. This is also testing in a browser; the overhead becomes amplified when you are running Angular on a mobile device.

Speaking of mobile: not only does this approach improve performance, but it also reduces memory overhead because AngularJS doesn’t have to keep track of as many references. So how did I achieve this? Here is the code for the second example:

(function (app) {
 
    var genders = ['Male', 'Female'],
        labels = ['boy', 'girl']
    function GoodController() {
        this.genders = genders;
        this._selectedGender = genders[0];
        this.genderText = labels[0];
    }
    Object.defineProperty(GoodController.prototype,
        "selectedGender", {
        enumerable: true,
        configurable: false,
        get: function () {
            return this._selectedGender;
        },
        set: function (val) {
            if (val !== this._selectedGender) {
                this._selectedGender = val;
                this.genderText =
                    val === this.genders[0]
                    ? labels[0] : labels[1];
            }
        }
    });
    app.controller('goodCtrl', GoodController);
})(angular.module('myApp', []));

Instead of using a $watch I’m taking advantage of properties that were introduced in ECMAScript 5. The property manages the selection and when the selection changes, updates the corresponding label. The reason this works is because of the way Angular handles data-binding. Angular operates on a digest loop. When the model is mutated (i.e. by the user changing a selection), Angular automatically re-evaluates other properties to determine if the UI needs to be updated. Angular is already doing the work, and when you add a $watch you are simply plugging into the loop so you can react yourself.

The problem is that Angular must now hold a reference to your watch. If the model mutates, Angular will re-evaluate the expression in your watch and call your function if it changes. Of course, because your code might mutate the model further, Angular must then re-evaluate all expressions again to make sure there aren’t further dependent changes. This can exponentially add to the overhead of your application.

On the other hand, using properties makes it simple. When the user mutates the model by changing the gender, Angular will automatically re-evaluate properties like the gender text to see if it needs to update the UI. Because the gender selection updated the property, Angular will recognize the change and refresh the UI. In this approach, you allow Angular to do the work instead of having to plug into the digest loop yourself and add overhead to the entire process.

There are a few more lines of code with this approach, but even that can be simplified tremendously if you use a tool like TypeScript to create the property definitions. It also enables you to build pure JavaScript objects and even test for the updates without involving Angular. That keeps your tests simple and ensures they run quickly with little overhead (i.e. “Given controller when the selected gender changes to male then the gender text should be updated to boy.”)

There is one more advantage. This approach allows Angular to handle the watches, and Angular is great about managing those watches appropriately. If you add the $watch yourself, you are now responsible for de-registering the $watch when it is no longer needed. If you don’t, it will continue to add overhead.

The full source code is available in this jsFiddle of the two controllers running side-by-side.

Keep this technique in mind because I see another practice that adds overhead and doesn’t have to when it comes to communicating between controllers. Have you seen a model where a controller does a $watch and then uses $broadcast or $emit to notify other controllers something has changed? Once again, this approach forces you to depend on the $scope and adds another layer of complexity by relying on the messaging mechanism of $scope to communicate between controllers. In my next post, I’ll show you how to avoid this third common mistake.

signature[1]

12 comments:

  1. Awesome . I started using approach after i saw this article. I don't how to resolve in the screen take a look at my property
    Object.defineProperty(Auto.prototype, "basePrice", {
    get: function () {
    return this._basePrice;
    },
    set: function (value) {
    if (value <= 0)
    throw 'price must be >= 0';
    this._basePrice = value;
    },
    enumerable: true,
    configurable: true
    });

    When i bound to the screen it nicely stops the editing part if it is less than zero.
    How can i capture the error and display the error message . Other words how do i pass the error to ngMessage

    ReplyDelete
    Replies
    1. I use the getter/setter for data-binding. For validation, I would use a validation directive - for example: http://www.benlesh.com/2012/12/angular-js-custom-validation-via.html

      Delete
  2. Why not use a function call? http://jsfiddle.net/n0cc2jqg/1/

    ReplyDelete
    Replies
    1. That's also an approach - technically this is like exposing the property getter, only you are just explicitly exposing a function rather than the property. The main idea is to avoid explicit watches when you don't need them.

      Delete
    2. Actually, there are some expression that require a settable property, not a function, for example ng-model.

      To solve this issue, I used a native ES5 property, which also allowed me to remove a ng-change directive too.

      Delete
    3. With ng-model you can use ng-model-options="{ getterSetter: true }" instead
      https://docs.angularjs.org/api/ng/directive/ngModelOptions

      Delete
  3. This is pretty good!!! Thanks for this..

    ReplyDelete
  4. "If you add the $watch yourself, you are now responsible for de-registering the $watch when it is no longer needed. If you don’t, it will continue to add overhead."

    Is this true? It's my understanding that Angular automatically deregisters watchers when the scope is destroyed.

    ReplyDelete
  5. Hello , I have been providing AngularJS courses in Chennai for the past 6 months, and at times, I have used your blog as reference for my students in the class. It has been so much useful. Thank you, keep writing more :)

    ReplyDelete
  6. This was seriously helpful! Thanks Man!

    ReplyDelete
  7. I have on value that lives on every component (angular v1.5.X), so I have set that on $rootScope. How can you make this work with $rootScope.$watch()?

    ReplyDelete
    Replies
    1. I would use a service to host the value and then architect it so that if it is a value that is tied to the UI, a getter or setter will automatically refresh when it changes. If it is a value that affects the backend, provide a call back or event mechanism so consumers can subscribe to the service and get notified when it changes.

      Delete