424 Stimmen

AngularJS : Wie kann man Dienstvariablen überwachen?

Ich habe einen Dienst, sagen wir:

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

  return service;
}]);

Und ich würde gerne foo um eine Liste zu steuern, die in HTML gerendert wird:

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

Damit der Controller erkennen kann, wann aService.foo aktualisiert wird, habe ich dieses Muster zusammengeschustert, bei dem ich einenService zum Controller hinzufüge $scope und verwenden Sie dann $scope.$watch() :

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

  $scope.$watch('aService.foo', function (newVal, oldVal, scope) {
    if(newVal) { 
      scope.foo = newVal;
    }
  });
}

Das fühlt sich langatmig an, und ich habe es in jedem Controller wiederholt, der die Variablen des Dienstes verwendet. Gibt es einen besseren Weg, um die Überwachung gemeinsamer Variablen zu erreichen?

280voto

dtheodor Punkte 4699

Sie können immer das gute alte Beobachtermuster verwenden, wenn Sie die Tyrannei und den Overhead von $watch .

Im Dienst:

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();
  });
});

Und im Controller:

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

  aService.registerObserverCallback(updateFoo);
  //service now in control of updating foo
};

232voto

Matt Pileggi Punkte 6878

In einem Szenario wie diesem, in dem mehrere/unbekannte Objekte an Änderungen interessiert sein könnten, verwenden Sie $rootScope.$broadcast von dem zu ändernden Element.

Anstatt eine eigene Registrierung von Hörern zu erstellen (die bei verschiedenen $destroys aufgeräumt werden müssen), sollten Sie in der Lage sein $broadcast von dem betreffenden Dienst.

Sie müssen noch den Code $on Handler in jedem Listener, aber das Muster ist entkoppelt von mehreren Aufrufen von $digest und vermeidet so das Risiko von Langzeitbeobachtungen.

Auf diese Weise können auch die Zuhörer von der DOM und/oder verschiedene Child-Scopes, ohne dass der Dienst sein Verhalten ändert.

** Update: Beispiele **

Übertragungen sind am sinnvollsten bei "globalen" Diensten, die sich auf unzählige andere Dinge in Ihrer Anwendung auswirken können. Ein gutes Beispiel ist ein Benutzerdienst, bei dem eine Reihe von Ereignissen auftreten können, wie z. B. Anmeldung, Abmeldung, Aktualisierung, Leerlauf usw. Ich glaube, dass hier Broadcasts am meisten Sinn machen, weil jeder Scope auf ein Ereignis hören kann, ohne den Dienst überhaupt zu injizieren, und er muss keine Ausdrücke auswerten oder Ergebnisse zwischenspeichern, um auf Änderungen zu prüfen. Er feuert einfach und vergisst es (stellen Sie also sicher, dass es sich um eine Fire-and-Forget-Benachrichtigung handelt und nicht um etwas, das eine Aktion erfordert).

.factory('UserService', [ '$rootScope', function($rootScope) {
   var service = <whatever you do for the object>

   service.save = function(data) {
     .. validate data and update model ..
     // notify listeners and provide the data that changed [optional]
     $rootScope.$broadcast('user:updated',data);
   }

   // alternatively, create a callback function and $broadcast from there if making an ajax call

   return service;
}]);

Der obige Dienst würde eine Nachricht an jeden Bereich senden, wenn die Funktion save() abgeschlossen und die Daten gültig sind. Alternativ, wenn es eine $Ressource oder eine Ajax-Übermittlung ist, verschieben Sie den Broadcast-Aufruf in den Callback, so dass es feuert, wenn der Server geantwortet hat. Broadcasts eignen sich besonders gut für dieses Muster, da jeder Listener einfach auf das Ereignis wartet, ohne den Bereich für jeden einzelnen $digest überprüfen zu müssen. Der Listener würde wie folgt aussehen:

.controller('UserCtrl', [ 'UserService', '$scope', function(UserService, $scope) {

  var user = UserService.getUser();

  // if you don't want to expose the actual object in your scope you could expose just the values, or derive a value for your purposes
   $scope.name = user.firstname + ' ' +user.lastname;

   $scope.$on('user:updated', function(event,data) {
     // you could inspect the data to see if what you care about changed, or just update your own scope
     $scope.name = user.firstname + ' ' + user.lastname;
   });

   // different event names let you group your code and logic by what happened
   $scope.$on('user:logout', function(event,data) {
     .. do something differently entirely ..
   });

 }]);

Dies hat unter anderem den Vorteil, dass nicht mehr mehrere Uhren benötigt werden. Wenn Sie Felder kombinieren oder Werte wie im obigen Beispiel ableiten würden, müssten Sie sowohl die Eigenschaften vorname als auch nachname überwachen. Das Überwachen der Funktion getUser() würde nur funktionieren, wenn das Benutzerobjekt bei Aktualisierungen ersetzt würde, es würde nicht funktionieren, wenn das Benutzerobjekt lediglich seine Eigenschaften aktualisiert hätte. In diesem Fall müssten Sie eine tiefe Überwachung durchführen, und das ist intensiver.

$broadcast sendet die Nachricht von dem Bereich, in dem es aufgerufen wird, nach unten in alle untergeordneten Bereiche. Der Aufruf von $rootScope wird also in jedem Bereich ausgelöst. Wenn man $broadcast zum Beispiel aus dem Scope des Controllers aufruft, wird es nur in den Scopes ausgelöst, die von dem Controller-Scope erben. $emit geht in die entgegengesetzte Richtung und verhält sich ähnlich wie ein DOM-Ereignis, indem es in der Scope-Kette nach oben blubbert.

Denken Sie daran, dass es Szenarien gibt, in denen $broadcast sehr sinnvoll ist, und dass es Szenarien gibt, in denen $watch eine bessere Option ist - insbesondere wenn es sich um einen isolierten Bereich mit einem sehr spezifischen Watch-Ausdruck handelt.

49voto

Krym Punkte 743

Ich bin mit ähnlichen Ansatz wie @dtheodot aber mit Angular Versprechen anstelle von Callbacks übergeben

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);
    }
})

Dann verwenden Sie einfach überall myService.setFoo(foo) Methode zur Aktualisierung foo im Dienst. In Ihrem Controller können Sie es als verwenden:

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

Die ersten beiden Argumente von then sind Erfolgs- und Fehlerrückrufe, der dritte ist ein Benachrichtigungsrückruf.

Referenz für $q.

43voto

Zymotik Punkte 4676

Ohne Uhren oder Beobachterrückrufe ( http://jsfiddle.net/zymotik/853wvv7s/ ):

JavaScript:

angular.module("Demo", [])
    .factory("DemoService", function($timeout) {

        function DemoService() {
            var self = this;
            self.name = "Demo Service";

            self.count = 0;

            self.counter = function(){
                self.count++;
                $timeout(self.counter, 1000);
            }

            self.addOneHundred = function(){
                self.count+=100;
            }

            self.counter();
        }

        return new DemoService();

    })
    .controller("DemoController", function($scope, DemoService) {

        $scope.service = DemoService;

        $scope.minusOneHundred = function() {
            DemoService.count -= 100;
        }

    });

HTML

<div ng-app="Demo" ng-controller="DemoController">
    <div>
        <h4>{{service.name}}</h4>
        <p>Count: {{service.count}}</p>
    </div>
</div>

Dieses JavaScript funktioniert, da wir ein Objekt vom Dienst zurückgeben und nicht einen Wert. Wenn ein JavaScript-Objekt von einem Dienst zurückgegeben wird, fügt Angular Uhren zu allen seinen Eigenschaften hinzu.

Beachten Sie auch, dass ich 'var self = this' verwende, da ich einen Verweis auf das ursprüngliche Objekt beibehalten muss, wenn der $timeout ausgeführt wird, da sich 'this' sonst auf das Fensterobjekt bezieht.

29voto

NanoWizard Punkte 1949

Ich bin auf diese Frage gestoßen, als ich nach etwas Ähnlichem suchte, aber ich denke, sie verdient eine gründliche Erklärung, was los ist, sowie einige zusätzliche Lösungen.

Wenn ein Angular-Ausdruck wie der, den Sie verwendet haben, im HTML vorhanden ist, richtet Angular automatisch eine $watch für $scope.foo und aktualisiert den HTML-Code immer dann, wenn $scope.foo Änderungen.

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

Der unausgesprochene Punkt ist, dass eines von zwei Dingen Auswirkungen hat aService.foo so dass die Änderungen unentdeckt bleiben. Diese beiden Möglichkeiten sind:

  1. aService.foo wird jedes Mal auf ein neues Array gesetzt, wodurch der Verweis darauf veraltet ist.
  2. aService.foo wird so aktualisiert, dass ein $digest Zyklus wird bei der Aktualisierung nicht ausgelöst.

Problem 1: Veraltete Referenzen

In Anbetracht der ersten Möglichkeit, unter der Annahme einer $digest angewandt wird, wenn aService.foo war immer das gleiche Array, die automatisch eingestellte $watch würde die Änderungen erkennen, wie im folgenden Codeschnipsel gezeigt.

Lösung 1-a: Stellen Sie sicher, dass das Array oder Objekt die gleiches Objekt bei jeder Aktualisierung

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>

Wie Sie sehen können, ist das ng-repeat, das angeblich an aService.foo wird nicht aktualisiert, wenn aService.foo ändert, aber das ng-repeat, das an aService2.foo tut . Dies liegt daran, dass unser Verweis auf aService.foo ist veraltet, aber unser Verweis auf aService2.foo ist nicht. Wir haben einen Verweis auf das ursprüngliche Array mit $scope.foo = aService.foo; die dann vom Dienst bei der nächsten Aktualisierung verworfen wurde, was bedeutet $scope.foo nicht mehr auf das gewünschte Array verweist.

Es gibt zwar mehrere Möglichkeiten, um sicherzustellen, dass der ursprüngliche Verweis beibehalten wird, aber manchmal kann es notwendig sein, das Objekt oder Array zu ändern. Oder was ist, wenn die Diensteigenschaft auf ein Primitivum wie ein String oder Number ? In diesen Fällen können wir uns nicht einfach auf eine Referenz verlassen. Was also kann wir tun?

Mehrere der bereits gegebenen Antworten enthalten bereits einige Lösungen für dieses Problem. Ich persönlich bin jedoch für die Anwendung der einfachen Methode, die von Jinthetallweeks in den Kommentaren:

referenzieren Sie einfach aService.foo in der html-Auszeichnung

Lösung 1-b: Fügen Sie den Dienst dem Bereich hinzu und verweisen Sie auf {service}.{property} in der HTML-Datei.

Das heißt, tun Sie es einfach:

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>

Auf diese Weise kann die $watch wird aufgelöst aService.foo an jedem $digest , die den korrekt aktualisierten Wert erhält.

Das ist in etwa das, was Sie mit Ihrer Umgehungslösung erreichen wollten, aber auf eine viel weniger umständliche Weise. Sie fügten eine unnötige $watch im Controller, der explizit die foo über die $scope wenn sie sich ändert. Sie brauchen dieses Extra nicht $watch wenn Sie die aService anstelle von aService.foo zum $scope und binden Sie explizit an aService.foo im Markup.


Das ist alles schön und gut, wenn man davon ausgeht, dass ein $digest Zyklus angewandt wird. In meinen obigen Beispielen habe ich Angulars $interval Dienst, um die Arrays zu aktualisieren, was automatisch eine $digest Schleife nach jeder Aktualisierung. Was aber, wenn die Service-Variablen (aus welchem Grund auch immer) nicht innerhalb der "Angular-Welt" aktualisiert werden? Mit anderen Worten, wir nicht haben eine $digest Zyklus automatisch aktiviert wird, wenn sich die Diensteigenschaft ändert?


Problem 2: Fehlen $digest

Viele der hier vorgestellten Lösungen werden dieses Problem lösen, aber ich stimme zu mit Code-Flüsterer :

Der Grund, warum wir ein Framework wie Angular verwenden, ist, dass wir uns keine eigenen Beobachtermuster ausdenken müssen.

Daher würde ich es vorziehen, weiterhin die aService.foo Referenz im HTML-Markup, wie im zweiten Beispiel oben gezeigt, und müssen nicht einen zusätzlichen Callback innerhalb des Controllers registrieren.

Lösung 2: Verwenden Sie einen Setter und Getter mit $rootScope.$apply()

Ich war überrascht, dass noch niemand die Verwendung eines Setzergetter . Diese Fähigkeit wurde mit ECMAScript5 eingeführt und ist somit schon seit Jahren bekannt. Das bedeutet natürlich, dass diese Methode nicht funktioniert, wenn Sie, aus welchem Grund auch immer, wirklich alte Browser unterstützen müssen, aber ich finde, dass Getter und Setter in JavaScript viel zu wenig genutzt werden. In diesem speziellen Fall könnten sie sehr nützlich sein:

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>

Hier habe ich eine "private" Variable in der Servicefunktion hinzugefügt: realFoo . Diese wird aktualisiert und abgerufen mit der get foo()set foo() Funktionen jeweils auf den service Objekt.

Beachten Sie die Verwendung von $rootScope.$apply() in der Set-Funktion. Dies stellt sicher, dass Angular über alle Änderungen an service.foo . Wenn Sie "inprog"-Fehler erhalten, siehe diese nützliche Referenzseite , oder wenn Sie Angular >= 1.3 verwenden, können Sie einfach $rootScope.$applyAsync() .

Seien Sie auch vorsichtig, wenn aService.foo sehr häufig aktualisiert wird, da dies die Leistung erheblich beeinträchtigen könnte. Wenn die Leistung ein Problem wäre, könnten Sie ein Beobachter-Muster ähnlich wie die anderen Antworten hier mit dem Setter einrichten.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X