1

I am trying to setup a watch on the return value of an asynchronous function, but keep running into infinite digest loops. Here's my $watch

  $scope.$watch(function(){
    var friendsCount = PublicUserData().then(function(data){
      console.log(data.length);
      return data.length;
    });
    return friendsCount;
  }, function(newValue, oldValue) {
    console.log("change"+ newValue);
  });

I can see in the chrome console that both console.log get called, but the second (on the callback) gets called far more often than the first.

PublicUserData uses $q:

.factory('PublicUserData', function($http, $q){
  return function(){
    var defer = $q.defer();
    $http.get('/api/v1/users/').then(function(data){
      defer.resolve(data.data.users);
    })
    return defer.promise;
  }
})

I've tried a few things, like setting the watch expression as a $scope outside of my $watch, but that ends up returning the factory's code, rather than the factory's return value.

Is it possible to use $watch with a function that implements a promise? Or should I be using something else?

user2936314
  • 1,734
  • 4
  • 18
  • 32

3 Answers3

6

You really really really really don't want to be doing AJAX requests from a $watch. Really.

Angular uses dirty-checking and your $watch function will be invoked repeatedly every single time anything on the $scope changes. If you could get it to work it would completely kill the performance of your application while spamming the server.

It looks like what you're trying to do is to have a counter or something that shows how many friends are logged in? That problem is better solved with either server events or a polling mechanism, polling being the easiest to pull off.

Consider something like this instead:

$interval(function() {
    PublicUserData().then(function(data) {
        $scope.data.friendsCount = data.length;
    });
}, 1000);

That will poll the server every second (which is probably overkill, but far less frequent than trying to do it in a $watch) and update the $scope with the correct value.

ivarni
  • 17,658
  • 17
  • 76
  • 92
  • 1
    ah I see. I couldn't figure out why it was causing digest loops. You gave me an idea for another way to approach this, cheers! – user2936314 Jul 18 '14 at 13:27
  • 2
    @user2936314 check out solutions which will let you provide websockets or longpooling communication into your application. It will let you push information from server to ui, instead of polling. For example we're using `Atmosphere` for that in our Java based app. – akn Jul 18 '14 at 13:34
2

If you already have a promise why do you do $watch?

PublicUserData().then(function(data){
  $scope.friendsCount = data
});

if you need to run it in interval best performance gives a $timeout

callForFriendsCount = function () {
  PublicUserData().then(function(data){
    $scope.friendsCount = data
    $timeout(callForFriendsCount, 1000)
  });
}

$timeout is better idea over an interval because if in any case older call will take longer than 1000 ms it will overlap the newest call, with $timeout you will avoid that

maurycy
  • 8,455
  • 1
  • 27
  • 44
2

Just to offer another similar solution to ivarni's excellent answer. If you want to poll (whether using timeout or interval) you can do this in a factory and just use a simple scope variable:

var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope, PublicUserData) {

  $scope.pollData = PublicUserData.pollData;

});


app.factory('PublicUserData', function($http, $q, $interval){


  var srv = {
    pollFn: function() {
      srv.stopInterval = $interval(function() {

        srv.pollData.friendCount++;
        console.log('polled');

      }, 1000);
    },
    pollData: {friendCount: 0, other: {}},
    stop: function() {
       $interval.cancel(srv.stopInterval);
    }

  }

  srv.pollFn();

   return srv; 
});

Demo: http://plnkr.co/edit/0467sypyeg0Wpdr4302k?p=preview

lucuma
  • 18,247
  • 4
  • 66
  • 91