AngularJS : How to watch service variables?

后端 未结 21 1389
盖世英雄少女心
盖世英雄少女心 2020-11-22 09:12

I have a service, say:

factory(\'aService\', [\'$rootScope\', \'$resource\', function ($rootScope, $resource) {
  var service = {
    foo: []
  };

  return          


        
相关标签:
21条回答
  • 2020-11-22 09:31

    You can always use the good old observer pattern if you want to avoid the tyranny and overhead of $watch.

    In the service:

    factory('aService', function() {
      var observerCallbacks = [];
    
      //register an observer
      this.registerObserverCallback = function(callback){
        observerCallbacks.push(callback);
      };
    
      //call this when you know 'foo' has been changed
      var notifyObservers = function(){
        angular.forEach(observerCallbacks, function(callback){
          callback();
        });
      };
    
      //example of when you may want to notify observers
      this.foo = someNgResource.query().$then(function(){
        notifyObservers();
      });
    });
    

    And in the controller:

    function FooCtrl($scope, aService){
      var updateFoo = function(){
        $scope.foo = aService.foo;
      };
    
      aService.registerObserverCallback(updateFoo);
      //service now in control of updating foo
    };
    
    0 讨论(0)
  • 2020-11-22 09:36

    I'm using similar approach as @dtheodot but using angular promise instead of passing callbacks

    app.service('myService', function($q) {
        var self = this,
            defer = $q.defer();
    
        this.foo = 0;
    
        this.observeFoo = function() {
            return defer.promise;
        }
    
        this.setFoo = function(foo) {
            self.foo = foo;
            defer.notify(self.foo);
        }
    })
    

    Then wherever just use myService.setFoo(foo) method to update foo on service. In your controller you can use it as:

    myService.observeFoo().then(null, null, function(foo){
        $scope.foo = foo;
    })
    

    First two arguments of then are success and error callbacks, third one is notify callback.

    Reference for $q.

    0 讨论(0)
  • 2020-11-22 09:36

    I have found a really great solution on the other thread with a similar problem but totally different approach. Source: AngularJS : $watch within directive is not working when $rootScope value is changed

    Basically the solution there tells NOT TO use $watch as it is very heavy solution. Instead they propose to use $emit and $on.

    My problem was to watch a variable in my service and react in directive. And with the above method it very easy!

    My module/service example:

    angular.module('xxx').factory('example', function ($rootScope) {
        var user;
    
        return {
            setUser: function (aUser) {
                user = aUser;
                $rootScope.$emit('user:change');
            },
            getUser: function () {
                return (user) ? user : false;
            },
            ...
        };
    });
    

    So basically I watch my user - whenever it is set to new value I $emit a user:change status.

    Now in my case, in the directive I used:

    angular.module('xxx').directive('directive', function (Auth, $rootScope) {
        return {
            ...
            link: function (scope, element, attrs) {
                ...
                $rootScope.$on('user:change', update);
            }
        };
    });
    

    Now in the directive I listen on the $rootScope and on the given change - I react respectively. Very easy and elegant!

    0 讨论(0)
  • 2020-11-22 09:37

    You can watch the changes within the factory itself and then broadcast a change

    angular.module('MyApp').factory('aFactory', function ($rootScope) {
        // Define your factory content
        var result = {
            'key': value
        };
    
        // add a listener on a key        
        $rootScope.$watch(function () {
            return result.key;
        }, function (newValue, oldValue, scope) {
            // This is called after the key "key" has changed, a good idea is to broadcast a message that key has changed
            $rootScope.$broadcast('aFactory:keyChanged', newValue);
        }, true);
    
        return result;
    });
    

    Then in your controller:

    angular.module('MyApp').controller('aController', ['$rootScope', function ($rootScope) {
    
        $rootScope.$on('aFactory:keyChanged', function currentCityChanged(event, value) {
            // do something
        });
    }]);
    

    In this manner you put all the related factory code within its description then you can only rely on the broadcast from outside

    0 讨论(0)
  • 2020-11-22 09:38

    I am late to the part but I found a nicer way to do this than the answer posted above. Instead of assigning a variable to hold the value of the service variable, I created a function attached to the scope, that returns the service variable.

    controller

    $scope.foo = function(){
     return aService.foo;
    }
    

    I think this will do what you want. My controller keeps checking the value of my service with this implementation. Honestly, this is much simpler than the selected answer.

    0 讨论(0)
  • 2020-11-22 09:39

    I stumbled upon this question looking for something similar, but I think it deserves a thorough explanation of what's going on, as well as some additional solutions.

    When an angular expression such as the one you used is present in the HTML, Angular automatically sets up a $watch for $scope.foo, and will update the HTML whenever $scope.foo changes.

    <div ng-controller="FooCtrl">
      <div ng-repeat="item in foo">{{ item }}</div>
    </div>
    

    The unsaid issue here is that one of two things are affecting aService.foo such that the changes are undetected. These two possibilities are:

    1. aService.foo is getting set to a new array each time, causing the reference to it to be outdated.
    2. aService.foo is being updated in such a way that a $digest cycle is not triggered on the update.

    Problem 1: Outdated References

    Considering the first possibility, assuming a $digest is being applied, if aService.foo was always the same array, the automatically set $watch would detect the changes, as shown in the code snippet below.

    Solution 1-a: Make sure the array or object is the same object on each update

    angular.module('myApp', [])
      .factory('aService', [
        '$interval',
        function($interval) {
          var service = {
            foo: []
          };
    
          // Create a new array on each update, appending the previous items and 
          // adding one new item each time
          $interval(function() {
            if (service.foo.length < 10) {
              var newArray = []
              Array.prototype.push.apply(newArray, service.foo);
              newArray.push(Math.random());
              service.foo = newArray;
            }
          }, 1000);
    
          return service;
        }
      ])
      .factory('aService2', [
        '$interval',
        function($interval) {
          var service = {
            foo: []
          };
    
          // Keep the same array, just add new items on each update
          $interval(function() {
            if (service.foo.length < 10) {
              service.foo.push(Math.random());
            }
          }, 1000);
    
          return service;
        }
      ])
      .controller('FooCtrl', [
        '$scope',
        'aService',
        'aService2',
        function FooCtrl($scope, aService, aService2) {
          $scope.foo = aService.foo;
          $scope.foo2 = aService2.foo;
        }
      ]);
    <!DOCTYPE html>
    <html>
    
    <head>
      <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
      <link rel="stylesheet" href="style.css" />
      <script src="script.js"></script>
    </head>
    
    <body ng-app="myApp">
      <div ng-controller="FooCtrl">
        <h1>Array changes on each update</h1>
        <div ng-repeat="item in foo">{{ item }}</div>
        <h1>Array is the same on each udpate</h1>
        <div ng-repeat="item in foo2">{{ item }}</div>
      </div>
    </body>
    
    </html>

    As you can see, the ng-repeat supposedly attached to aService.foo does not update when aService.foo changes, but the ng-repeat attached to aService2.foo does. This is because our reference to aService.foo is outdated, but our reference to aService2.foo is not. We created a reference to the initial array with $scope.foo = aService.foo;, which was then discarded by the service on it's next update, meaning $scope.foo no longer referenced the array we wanted anymore.

    However, while there are several ways to make sure the initial reference is kept in tact, sometimes it may be necessary to change the object or array. Or what if the service property references a primitive like a String or Number? In those cases, we cannot simply rely on a reference. So what can we do?

    Several of the answers given previously already give some solutions to that problem. However, I am personally in favor of using the simple method suggested by Jin and thetallweeks in the comments:

    just reference aService.foo in the html markup

    Solution 1-b: Attach the service to the scope, and reference {service}.{property} in the HTML.

    Meaning, just do this:

    HTML:

    <div ng-controller="FooCtrl">
      <div ng-repeat="item in aService.foo">{{ item }}</div>
    </div>
    

    JS:

    function FooCtrl($scope, aService) {
        $scope.aService = aService;
    }
    

    angular.module('myApp', [])
      .factory('aService', [
        '$interval',
        function($interval) {
          var service = {
            foo: []
          };
    
          // Create a new array on each update, appending the previous items and 
          // adding one new item each time
          $interval(function() {
            if (service.foo.length < 10) {
              var newArray = []
              Array.prototype.push.apply(newArray, service.foo);
              newArray.push(Math.random());
              service.foo = newArray;
            }
          }, 1000);
    
          return service;
        }
      ])
      .controller('FooCtrl', [
        '$scope',
        'aService',
        function FooCtrl($scope, aService) {
          $scope.aService = aService;
        }
      ]);
    <!DOCTYPE html>
    <html>
    
    <head>
      <script data-require="angular.js@1.4.7" data-semver="1.4.7" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.js"></script>
      <link rel="stylesheet" href="style.css" />
      <script src="script.js"></script>
    </head>
    
    <body ng-app="myApp">
      <div ng-controller="FooCtrl">
        <h1>Array changes on each update</h1>
        <div ng-repeat="item in aService.foo">{{ item }}</div>
      </div>
    </body>
    
    </html>

    That way, the $watch will resolve aService.foo on each $digest, which will get the correctly updated value.

    This is kind of what you were trying to do with your workaround, but in a much less round about way. You added an unnecessary $watch in the controller which explicitly puts foo on the $scope whenever it changes. You don't need that extra $watch when you attach aService instead of aService.foo to the $scope, and bind explicitly to aService.foo in the markup.


    Now that's all well and good assuming a $digest cycle is being applied. In my examples above, I used Angular's $interval service to update the arrays, which automatically kicks off a $digest loop after each update. But what if the service variables (for whatever reason) aren't getting updated inside the "Angular world". In other words, we dont have a $digest cycle being activated automatically whenever the service property changes?


    Problem 2: Missing $digest

    Many of the solutions here will solve this issue, but I agree with Code Whisperer:

    The reason why we're using a framework like Angular is to not cook up our own observer patterns

    Therefore, I would prefer to continue to use the aService.foo reference in the HTML markup as shown in the second example above, and not have to register an additional callback within the Controller.

    Solution 2: Use a setter and getter with $rootScope.$apply()

    I was surprised no one has yet suggested the use of a setter and getter. This capability was introduced in ECMAScript5, and has thus been around for years now. Of course, that means if, for whatever reason, you need to support really old browsers, then this method will not work, but I feel like getters and setters are vastly underused in JavaScript. In this particular case, they could be quite useful:

    factory('aService', [
      '$rootScope',
      function($rootScope) {
        var realFoo = [];
    
        var service = {
          set foo(a) {
            realFoo = a;
            $rootScope.$apply();
          },
          get foo() {
            return realFoo;
          }
        };
      // ...
    }
    

    angular.module('myApp', [])
      .factory('aService', [
        '$rootScope',
        function($rootScope) {
          var realFoo = [];
    
          var service = {
            set foo(a) {
              realFoo = a;
              $rootScope.$apply();
            },
            get foo() {
              return realFoo;
            }
          };
    
          // Create a new array on each update, appending the previous items and 
          // adding one new item each time
          setInterval(function() {
            if (service.foo.length < 10) {
              var newArray = [];
              Array.prototype.push.apply(newArray, service.foo);
              newArray.push(Math.random());
              service.foo = newArray;
            }
          }, 1000);
    
          return service;
        }
      ])
      .controller('FooCtrl', [
        '$scope',
        'aService',
        function FooCtrl($scope, aService) {
          $scope.aService = aService;
        }
      ]);
    <!DOCTYPE html>
    <html>
    
    <head>
      <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
      <link rel="stylesheet" href="style.css" />
      <script src="script.js"></script>
    </head>
    
    <body ng-app="myApp">
      <div ng-controller="FooCtrl">
        <h1>Using a Getter/Setter</h1>
        <div ng-repeat="item in aService.foo">{{ item }}</div>
      </div>
    </body>
    
    </html>

    Here I added a 'private' variable in the service function: realFoo. This get's updated and retrieved using the get foo() and set foo() functions respectively on the service object.

    Note the use of $rootScope.$apply() in the set function. This ensures that Angular will be aware of any changes to service.foo. If you get 'inprog' errors see this useful reference page, or if you use Angular >= 1.3 you can just use $rootScope.$applyAsync().

    Also be wary of this if aService.foo is being updated very frequently, since that could significantly impact performance. If performance would be an issue, you could set up an observer pattern similar to the other answers here using the setter.

    0 讨论(0)
提交回复
热议问题