Hackviking He killed Chuck Norris, he ruled dancing so he took up a new hobby…

29Feb/160

AngularJS: Directive for checking # of watchers in scope

Performance is key in web development. Many developers forget this since there machines usually are more powerful then the average user machine. This is really important to take into account when building client side applications. In AngularJS the ng-repeat can be a source of bad performance, it's super convenient to use but can create both digest loops and performance issues. Most of the time the performance can improve if you cut back on the two-way bindings. Every two-way binding creates a watcher that fires if something is changed, since Angular dosen't know what changed it checks every watcher in the current scope of the change.

So if you are only displaying data with ng-repeat without any editing function there is no reason to use a two-way binding like {{whateverdata}}, instead you should use a one way binding like {{::whateverdata}}. This approach will save the number of watchers. When I develop in Angular I use a simple directive to double check my scopes for the number of watchers created. It gives me quick access to the figure and I can see if changes to the application affects the number of watchers.

.directive('scopewatchers', function($timeout) {
  return {
    restrict: 'AE',
    replace: 'true',
    template: '<h2 ng-click="wcount()"># of watchers (click to update):{{watchers || "0"}}</h2>',
    controller: function($scope, $element) {
      $scope.wcount = function() {
        $timeout(function() {
          $scope.watchers = $scope.watchersContainedIn($scope);
        });
      };

      $scope.watchersContainedIn = function(scope) {
        var watchers = (scope.$$watchers) ? scope.$$watchers.length : 0;
        var child = scope.$$childHead;
        while (child) {
          watchers += (child.$$watchers) ? child.$$watchers.length : 0;
          child = child.$$nextSibling;
        }
        return watchers;
      };
    }
  };
})

Then I can easaly pop it into any scoop I want to check the number of watchers currently active in that scope.

<scopewatchers></scopewatchers>

I put together a Plunker demo of this built on the Digest Loop Demo I did a while back. With the amount of data in this demo it doesn't really do any difference if I have 500+ watchers or 2 watchers but the principal is the same. For larger datasets it can be a huge difference when ng-repeat inserts new items to the DOM or changes order of the items.

https://plnkr.co/edit/SlBYfp?p=info

8Feb/160

AngularJS: $digest loop when filtering array of objects

Ran into trouble trying to use a filter to sort an array of objects in AngularJS. Basic issue is because the array of objects is passed to the filter by reference. This is not unique to AngularJS, javascript always passed arrays by reference. Since AngularJS watches for changes in the array the sort will fire a digest cycle for every change. This will end up with an error similar to this:

Error: $rootScope:infdig
Infinite $digest Loop

10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: [[{"msg":"fn: function (c,e,f,g){f=d&amp;amp;amp;&amp;amp;amp;g?g[0]:a(c,e,f,g);
return b(f,c,e)}","newVal":139,"oldVal":137},
{"msg":"fn: function (c,e,f,g){f=d&amp;amp;amp;&amp;amp;amp;g?g[0]:a(c,e,f,g);return b(f,c,e)}","newVal":141,"oldVal":139}],
[{"msg":"fn: function (c,e,f,g){f=d&amp;amp;amp;&amp;amp;amp;g?g[0]:a(c,e,f,g);return b(f,c,e)}","newVal":141,"oldVal":139},
{"msg":"fn: function (c,e,f,g){f=d&amp;amp;amp;&amp;amp;amp;g?g[0]:a(c,e,f,g);return b(f,c,e)}","newVal":143,"oldVal":141}],
[{"msg":"fn: function (c,e,f,g){f=d&amp;amp;amp;&amp;amp;amp;g?g[0]:a(c,e,f,g);return b(f,c,e)}","newVal":143,"oldVal":141},
{"msg":"fn: function (c,e,f,g){f=d&amp;amp;amp;&amp;amp;amp;g?g[0]:a(c,e,f,g);return b(f,c,e)}","newVal":145,"oldVal":143}],
[{"msg":"fn: function (c,e,f,g){f=d&amp;amp;amp;&amp;amp;amp;g?g[0]:a(c,e,f,g);return b(f,c,e)}","newVal":145,"oldVal":143},
{"msg":"fn: function (c,e,f,g){f=d&amp;amp;amp;&amp;amp;amp;g?g[0]:a(c,e,f,g);return b(f,c,e)}","newVal":147,"oldVal":145}],
[{"msg":"fn: function (c,e,f,g){f=d&amp;amp;amp;&amp;amp;amp;g?g[0]:a(c,e,f,g);return b(f,c,e)}","newVal":147,"oldVal":145},
{"msg":"fn: function (c,e,f,g){f=d&amp;amp;amp;&amp;amp;amp;g?g[0]:a(c,e,f,g);return b(f,c,e)}","newVal":149,"oldVal":147}]]

Background

So the data structure looks something like this:

{
"feed": {
    "entry": [
    {
        "gphoto$id": {
            "$t": "1000000482404626"
            },
        "updated": {
            "$t": "2016-01-31T19:15:32.739Z"
        },
        "title": {
            "$t": "Auto Backup",
            "type": "text"
        },
        "gphoto$numphotos": {
            "$t": 4223
        },
        "published": {
            "$t": "2016-01-31T17:44:14.000Z"
        }
    },
    {
        "gphoto$id": {
            "$t": "6056987966618281201"
        },
        "updated": {
            "$t": "2015-09-04T15:47:26.370Z"
        },
        "title": {
            "$t": "Norge 2014-09",
            "type": "text"
        },
        "gphoto$numphotos": {
            "$t": 368
        },
        "published": {
            "$t": "2014-09-09T08:46:46.000Z"
        }
    },
    {
        "gphoto$id": {
            "$t": "5980880175059657089"
        },
        "updated": {
            "$t": "2016-01-23T02:50:47.003Z"
        },
        "title": {
            "$t": "Testalbum2",
            "type": "text"
        },
        "gphoto$numphotos": {
            "$t": 3
        },
        "published": {
            "$t": "2014-02-16T06:29:40.000Z"
        }
    }],
    "xmlns": "http://www.w3.org/2005/Atom",
    "xmlns$gphoto": "http://schemas.google.com/photos/2007"
},
"version": "1.0",
"encoding": "UTF-8"
}

We store this array in a $scope variable called List. When we want to run through this array in a simple ng-repeat like

ng-repeat="album in List | BasicFilter:Field:Reverse track by album.gphoto$id.$t"

It will call the filter every time the List variable is changed. If we use this filter:

.filter('BasicFilter', function() {
    return function(items, field, reverse) {
        items.sort(function(a, b){
            var aValue = a[field].$t.toLowerCase();
            var bValue = b[field].$t.toLowerCase();
            
            if(reverse)
            {
                return ((aValue &amp;gt; bValue) ? -1 : ((aValue &amp;lt; bValue) ? 1 : 0));
            } else {
                return ((aValue &amp;lt; bValue) ? -1 : ((aValue &amp;gt; bValue) ? 1 : 0));
            }
        });
        
        return items;
    };
})

Why?

This filter takes in the array as items and sort it. Without going in to to much detail the function supplied to .sort will fire a lot of times. Since the array items is passed by reference it will end up updating the $scope.List that Angular watches and fire a digest each time. There for the fail safe kicks in and you get the error from before.

Solution

So how do we get around this? We need to sort the array without updating the original to prevent the digest from running each time. So we modify our filter to:

.filter('SlicedFilter', function() {
    return function(input, field, reverse) {
      var items = input.slice();
        items.sort(function(a, b){
            var aValue = a[field].$t.toLowerCase();
            var bValue = b[field].$t.toLowerCase();
            
            if(reverse)
            {
                return ((aValue > bValue) ? -1 : ((aValue < bValue) ? 1 : 0));
            } else {
                return ((aValue < bValue) ? -1 : ((aValue > bValue) ? 1 : 0));
            }
        });
        
        return items;
    };
})

By renaming the items variable to input and then create items as an internal variable and copy the original array with .slice() we will get around this issue. Slice will create a shallow copy of the object array. That means that all simple types like strings and numbers will be copied while objects inside the array will be by reference. This means that if any of the objects is updated in the original array the changes will reflect in our copy as well. Then we sort our new copy and when we are done, running the function passed to sort many times, we return the array. When the filter finally return the sorted copy that will trigger the digest and DOM update. So instead of triggering it to many times we only trigger it when we are done with our sorting returning the final product.

But what if the user decides to change back to a sort order we already used? We will sort once again and return it, that's not really efficient! So let's cache the sorted list:

.filter('CachedFilter', function() {
    return _.memoize(function(input, field, reverse) {
      var items = input.slice();
        items.sort(function(a, b){
            var aValue = a[field].$t.toLowerCase();
            var bValue = b[field].$t.toLowerCase();
            
            if(reverse)
            {
                return ((aValue > bValue) ? -1 : ((aValue < bValue) ? 1 : 0));
            } else {
                return ((aValue < bValue) ? -1 : ((aValue > bValue) ? 1 : 0));
            }
        });
        
        return items;
    }, function(items, field, reverse) {
      return items.length + field + reverse;
    });
})

This is a function from underscoreJS that will cache output from whatever function you give it. The second function we pass in calculates the key for the cache. In our case that would be the number of items in the array, field we filtered on and if it's ascending or descending. So the next time we request the same sort we will get the cached version which increases performance.

I put together a demo on Plunker for this issue...

Please share, comment and add suggestions below!