AngularJS: Wie werden Dienstvariablen überwacht?


414

Ich habe einen Service, sagen wir:

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

  return service;
}]);

Und ich möchte fooeine Liste steuern, die in HTML gerendert wird:

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

Damit der Controller aService.fooerkennt, wann aktualisiert wird, habe ich dieses Muster zusammengeschustert, wobei ich dem Controller einen Service hinzufüge $scopeund dann Folgendes verwende $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;
    }
  });
}

Dies fühlt sich langwierig an und ich habe es in jedem Controller wiederholt, der die Variablen des Dienstes verwendet. Gibt es eine bessere Möglichkeit, gemeinsam genutzte Variablen zu überwachen?


1
Sie können einen dritten Parameter an $ watch übergeben, der auf true gesetzt ist, um aService und alle seine Eigenschaften zu überwachen.
SirTophamHatt

7
$ scope.foo = aService.foo ist ausreichend, Sie können die obige Zeile verlieren. Und was es in $ watch macht, macht keinen Sinn, wenn Sie $ scope.foo einen neuen Wert zuweisen möchten. Tun Sie es einfach ...
Jin

4
Könnten Sie einfach aService.fooim HTML-Markup verweisen ? (wie folgt : plnkr.co/edit/aNrw5Wo4Q0IxR2loipl5?p=preview )
gesamten Wochen

1
Ich habe ein Beispiel ohne Rückrufe oder $ Uhren hinzugefügt, siehe Antwort unten ( jsfiddle.net/zymotik/853wvv7s )
Zymotik

1
@ MikeGledhill, du hast recht. Ich denke, es liegt an der Natur von Javascript, dass Sie dieses Muster an vielen anderen Stellen sehen können (nicht nur in Angular, sondern allgemein in JS). Einerseits übertragen Sie den Wert (und er wird nicht gebunden) und andererseits übertragen Sie ein Objekt (oder den Wert, der auf das Objekt verweist ...), und deshalb werden die Eigenschaften korrekt aktualisiert (wie perfekt) gezeigt im Beispiel von Zymotik oben).
Christophe Vidal

Antworten:


277

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

Im Dienste:

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 in der Steuerung:

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

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

21
@Moo auf das $destoryEreignis im Bereich warten und eine Methode zum Aufheben der Registrierung hinzufügenaService
Jamie

13
Was sind die Vorteile dieser Lösung? Es benötigt mehr Code in einem Dienst und ungefähr die gleiche Menge an Code in einem Controller (da wir auch die Registrierung für $ destroy aufheben müssen). Ich könnte für die Ausführungsgeschwindigkeit sagen, aber in den meisten Fällen spielt es einfach keine Rolle.
Alex Che

6
nicht sicher, wie dies eine bessere Lösung als $ watch ist, der Fragesteller fragte nach einer einfachen Möglichkeit, die Daten zu teilen, es sieht noch umständlicher aus. Ich würde lieber $ Broadcast als dies verwenden
Jin

11
$watchDas vs-Beobachter-Muster entscheidet einfach, ob abgefragt oder gepusht werden soll, und ist im Grunde eine Frage der Leistung. Verwenden Sie es also, wenn es auf die Leistung ankommt. Ich verwende ein Beobachtermuster, wenn ich sonst komplexe Objekte "tief" beobachten müsste. Ich hänge ganze Dienste an den $ -Bereich an, anstatt einzelne Dienstwerte zu beobachten. Ich vermeide Angulars $ watch wie den Teufel, es gibt genug davon in Direktiven und in nativen eckigen Datenbindungen.
Thetheor

107
Der Grund, warum wir ein Framework wie Angular verwenden, besteht darin, keine eigenen Beobachtermuster zu erstellen.
Code Whisperer

230

In einem solchen Szenario, in dem mehrere / unbekannte Objekte möglicherweise an Änderungen interessiert sind, verwenden Sie $rootScope.$broadcastdas zu ändernde Element.

Anstatt eine eigene Registrierung von Listenern zu erstellen (die bei verschiedenen $ destroys bereinigt werden müssen), sollten Sie in der Lage sein, $broadcastüber den betreffenden Dienst zu verfügen .

Sie müssen weiterhin die $onHandler in jedem Listener codieren, aber das Muster ist von mehreren Aufrufen an entkoppelt $digestund vermeidet so das Risiko von lang laufenden Beobachtern.

Auf diese Weise können Zuhörer auch aus dem DOM und / oder verschiedenen untergeordneten Bereichen kommen und gehen, ohne dass der Dienst sein Verhalten ändert.

** Update: Beispiele **

Sendungen sind in "globalen" Diensten am sinnvollsten, die sich auf unzählige andere Dinge in Ihrer App auswirken können. Ein gutes Beispiel ist ein Benutzerdienst, bei dem eine Reihe von Ereignissen stattfinden können, z. B. Anmelden, Abmelden, Aktualisieren, Leerlauf usw. Ich glaube, hier sind Broadcasts am sinnvollsten, da jeder Bereich auf ein Ereignis warten kann, ohne Selbst wenn der Dienst injiziert wird, müssen keine Ausdrücke oder Cache-Ergebnisse ausgewertet werden, um nach Änderungen zu suchen. Es wird nur ausgelöst und vergessen (stellen Sie also sicher, dass es sich um eine Feuer-und-Vergessen-Benachrichtigung handelt, die keine Maßnahmen 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 sendet eine Nachricht an jeden Bereich, wenn die Funktion save () abgeschlossen ist und die Daten gültig sind. Wenn es sich alternativ um eine $ -Ressource oder eine Ajax-Übermittlung handelt, verschieben Sie den Broadcast-Anruf in den Rückruf, damit er ausgelöst wird, wenn der Server geantwortet hat. Broadcasts passen besonders gut zu diesem Muster, da jeder Listener nur auf das Ereignis wartet, ohne den Umfang jedes einzelnen $ Digest überprüfen zu müssen. Der Hörer würde aussehen wie:

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

 }]);

Einer der Vorteile davon ist der Wegfall mehrerer Uhren. Wenn Sie Felder kombinieren oder Werte wie im obigen Beispiel ableiten, müssen Sie sowohl die Eigenschaften Vorname als auch Nachname überwachen. Das Beobachten der Funktion getUser () würde nur funktionieren, wenn das Benutzerobjekt bei Aktualisierungen ersetzt wurde. Es würde nicht ausgelöst, wenn die Eigenschaften des Benutzerobjekts lediglich aktualisiert würden. In diesem Fall müssten Sie eine tiefe Beobachtung machen, und das ist intensiver.

$ Broadcast sendet die Nachricht aus dem aufgerufenen Bereich in untergeordnete Bereiche. Wenn Sie es also von $ rootScope aus aufrufen, wird es in jedem Bereich ausgelöst. Wenn Sie beispielsweise $ Broadcast aus dem Bereich Ihres Controllers senden, wird dieser nur in den Bereichen ausgelöst, die von Ihrem Controller-Bereich erben. $ emit geht in die entgegengesetzte Richtung und verhält sich ähnlich wie ein DOM-Ereignis, indem es die Scope-Kette in die Luft sprudelt.

Denken Sie daran, dass es Szenarien gibt, in denen $ Broadcast sehr sinnvoll ist, und es gibt Szenarien, in denen $ Watch eine bessere Option ist - insbesondere in einem isolierten Bereich mit einem ganz bestimmten Watch-Ausdruck.


1
Das Verlassen des $ Digest-Zyklus ist eine gute Sache, insbesondere wenn die Änderungen, die Sie beobachten, kein Wert sind, der direkt und sofort in das DOM übernommen wird.
XML

Gibt es weg, um die .save () -Methode zu vermeiden. Scheint übertrieben, wenn Sie nur die Aktualisierung einer einzelnen Variablen im sharedService überwachen. Können wir die Variable im sharedService beobachten und senden, wenn sie sich ändert?
JerryKur

Ich habe einige Möglichkeiten ausprobiert, um Daten zwischen Controllern auszutauschen, aber dies ist die einzige, die funktioniert hat. Gut gespielt, Sir.
Abettermap

Ich ziehe dies den anderen Antworten vor, scheint weniger hackig , danke
JMK

9
Dies ist nur dann das richtige Entwurfsmuster, wenn Ihr konsumierender Controller über mehrere mögliche Datenquellen verfügt. Mit anderen Worten, wenn Sie eine MIMO-Situation haben (Multiple Input / Multiple Output). Wenn Sie nur ein Eins-zu-Viele-Muster verwenden, sollten Sie die direkte Objektreferenzierung verwenden und das Angular-Framework die bidirektionale Bindung für Sie ausführen lassen. Horkyze hat dies unten verlinkt und es ist eine gute Erklärung für die automatische bidirektionale
Charles

47

Ich verwende einen ähnlichen Ansatz wie @dtheodot, verwende jedoch ein Winkelversprechen, anstatt Rückrufe weiterzuleiten

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

Verwenden Sie dann überall die myService.setFoo(foo)Methode, um den fooService zu aktualisieren . In Ihrem Controller können Sie es verwenden als:

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

Die ersten beiden Argumente thensind Erfolgs- und Fehlerrückrufe, das dritte ist der Benachrichtigungsrückruf.

Referenz für $ q.


Was wäre der Vorteil dieser Methode gegenüber der von Matt Pileggi unten beschriebenen $ -Sendung?
Fabio

Nun, beide Methoden haben ihre Verwendung. Die Vorteile der Ausstrahlung wären für mich die menschliche Lesbarkeit und die Möglichkeit, mehr Orte derselben Veranstaltung zuzuhören. Ich denke, der Hauptnachteil ist, dass Broadcast Nachrichten an alle untergeordneten Bereiche sendet, sodass dies möglicherweise ein Leistungsproblem darstellt.
Krym

2
Ich hatte ein Problem, bei dem das Ausführen $scope.$watcheiner Dienstvariablen nicht zu funktionieren schien (der Bereich, den ich beobachtete, war ein Modal, von dem geerbt wurde $rootScope) - dies funktionierte. Cooler Trick, danke fürs Teilen!
Seiyria

4
Wie würden Sie mit diesem Ansatz nach sich selbst aufräumen? Ist es möglich, den registrierten Rückruf aus dem Versprechen zu entfernen, wenn der Bereich zerstört wird?
Abris

Gute Frage. Ich weiß es ehrlich gesagt nicht. Ich werde versuchen, einige Tests durchzuführen, wie Sie den Benachrichtigungsrückruf aus dem Versprechen entfernen können.
Krym

41

Ohne Uhren oder Beobachter-Rü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, wenn wir ein Objekt anstelle eines Werts vom Dienst zurückgeben. Wenn ein JavaScript-Objekt von einem Dienst zurückgegeben wird, fügt Angular allen seinen Eigenschaften Überwachungsfunktionen hinzu.

Beachten Sie auch, dass ich 'var self = this' verwende, da ich bei der Ausführung des $ timeout einen Verweis auf das ursprüngliche Objekt behalten muss, andernfalls bezieht sich 'this' auf das Fensterobjekt.


3
Dies ist ein großartiger Ansatz! Gibt es eine Möglichkeit, nur eine Eigenschaft eines Dienstes an den Gültigkeitsbereich anstatt an den gesamten Dienst zu binden? Nur zu tun $scope.count = service.countfunktioniert nicht.
Jvannistelrooy

Sie können die Eigenschaft auch in einem (beliebigen) Objekt verschachteln, sodass es als Referenz übergeben wird. $scope.data = service.data <p>Count: {{ data.count }}</p>
Alex Ross

1
Hervorragender Ansatz! Obwohl es auf dieser Seite viele starke, funktionale Antworten gibt, ist dies bei weitem a) am einfachsten zu implementieren und b) am einfachsten zu verstehen, wenn der Code gelesen wird. Diese Antwort sollte viel höher sein als derzeit.
CodeMoose

Vielen Dank an @CodeMoose, ich habe es heute für diejenigen, die AngularJS / JavaScript noch nicht kennen, noch weiter vereinfacht.
Zymotik

2
Möge Gott dich segnen. Ich habe wie Millionen Stunden verschwendet, würde ich sagen. Weil ich mit 1.5 und Angularjs zu kämpfen hatte, die von 1 auf 2 verschoben wurden, und auch Daten teilen wollte
Amna

29

Ich bin auf diese Frage gestoßen, als ich nach etwas Ähnlichem gesucht habe, aber ich denke, sie verdient eine gründliche Erklärung der Vorgänge sowie einige zusätzliche Lösungen.

Wenn ein Winkelausdruck wie der von Ihnen verwendete im HTML-Code vorhanden ist, richtet Angular automatisch ein $watchfor ein $scope.foound aktualisiert den HTML-Code bei jeder $scope.fooÄnderung.

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

Das unausgesprochene Problem hierbei ist, dass eines von zwei Dingen so wirkt aService.foo , dass die Änderungen nicht erkannt werden. Diese beiden Möglichkeiten sind:

  1. aService.foo wird jedes Mal auf ein neues Array gesetzt, wodurch der Verweis darauf veraltet ist.
  2. aService.foowird so aktualisiert, dass $digestbeim Update kein Zyklus ausgelöst wird.

Problem 1: Veraltete Referenzen

Unter Berücksichtigung der ersten Möglichkeit würde unter der Annahme, dass a $digestangewendet wird, wenn aService.fooimmer dasselbe Array verwendet würde, der automatisch festgelegte $watchdie Änderungen erkennen, wie im folgenden Codeausschnitt gezeigt.

Lösung 1-a: Stellen Sie sicher, dass das Array oder Objekt bei jedem Update dasselbe Objekt ist

Wie Sie sehen können, wird die angeblich angehängte ng-Wiederholung aService.foobei aService.fooÄnderungen nicht aktualisiert , die angehängte ng-Wiederholung aService2.foo jedoch . Dies liegt daran, dass unser Verweis auf aService.fooveraltet ist, unser Verweis auf aService2.foojedoch nicht. Wir haben einen Verweis auf das ursprüngliche Array mit erstellt $scope.foo = aService.foo;, der dann beim nächsten Update vom Dienst verworfen wurde, was bedeutet, dass $scope.foonicht mehr auf das gewünschte Array verwiesen wird.

Obwohl es verschiedene Möglichkeiten gibt, um sicherzustellen, dass die ursprüngliche Referenz erhalten bleibt, kann es manchmal erforderlich sein, das Objekt oder Array zu ändern. Oder was ist, wenn die Serviceeigenschaft auf ein Grundelement wie ein Stringoder verweist Number? In diesen Fällen können wir uns nicht einfach auf eine Referenz verlassen. Was können wir also tun?

Einige der zuvor gegebenen Antworten geben bereits einige Lösungen für dieses Problem. Ich persönlich bin jedoch dafür, die einfache Methode zu verwenden, die Jin und die ganzen Wochen in den Kommentaren vorgeschlagen haben:

Verweisen Sie einfach im HTML-Markup auf aService.foo

Lösung 1-b: Hängen Sie den Dienst an den Bereich an und verweisen Sie ihn {service}.{property}im HTML-Code.

Das heißt, mach einfach das:

HTML:

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

JS:

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

Auf diese Weise der $watchWille Entschlossenheit aService.fooauf jeden $digest, der die korrekt aktualisierte Wert erhalten wird.

Dies ist eine Art von dem, was Sie mit Ihrer Problemumgehung versucht haben, aber auf eine viel weniger runde Art und Weise. Sie hinzugefügt unnötig $watchin der Steuerung , die legt ausdrücklich foodas auf , $scopewenn sie sich ändert. Sie brauchen nicht , dass zusätzliche , $watchwenn Sie legen aServicestatt aService.fooauf das $scope, und binden explizit aService.fooim Markup.


Nun, das ist alles schön und gut, vorausgesetzt, ein $digestZyklus wird angewendet. In meinen obigen Beispielen habe ich Angulars $intervalDienst verwendet, um die Arrays zu aktualisieren, wodurch $digestnach jeder Aktualisierung automatisch eine Schleife ausgelöst wird. Was aber, wenn die Dienstvariablen (aus welchen Gründen auch immer) in der "Angular World" nicht aktualisiert werden? Mit anderen Worten, wir dont haben einen $digestZyklus automatisch aktiviert wird , sobald die Service - Eigenschaft ändert?


Problem 2: Vermisst $digest

Viele der hier aufgeführten Lösungen werden dieses Problem lösen, aber ich stimme Code Whisperer zu :

Der Grund, warum wir ein Framework wie Angular verwenden, besteht darin, keine eigenen Beobachtermuster zu erstellen

Daher würde ich es vorziehen, die aService.fooReferenz im HTML-Markup weiterhin zu verwenden, wie im zweiten Beispiel oben gezeigt, und keinen zusätzlichen Rückruf im Controller registrieren zu müssen.

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

Ich war überrascht, dass noch niemand die Verwendung von Setter und Getter vorgeschlagen hat . Diese Funktion wurde in ECMAScript5 eingeführt und gibt es daher seit Jahren. Das bedeutet natürlich, dass diese Methode nicht funktioniert, wenn Sie aus irgendeinem Grund wirklich alte Browser unterstützen müssen, aber ich denke, dass Getter und Setter in JavaScript stark unterbeansprucht 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;
      }
    };
  // ...
}

Hier habe ich eine 'private' Variable in die Servicefunktion eingefügt : realFoo. Dieses gets aktualisiert und abgerufen die Verwendung get foo()und set foo()Funktionen jeweils auf dem serviceObjekt.

Beachten Sie die Verwendung von $rootScope.$apply()in der eingestellten Funktion. Dadurch wird sichergestellt, dass Angular über Änderungen informiert wird service.foo. Wenn Sie 'inprog'-Fehler erhalten, lesen Sie diese nützliche Referenzseite , oder wenn Sie Angular> = 1.3 verwenden, können Sie einfach verwenden $rootScope.$applyAsync().

Seien Sie auch vorsichtig, wenn aService.foodiese sehr häufig aktualisiert werden, da dies die Leistung erheblich beeinträchtigen kann. Wenn die Leistung ein Problem darstellt, können Sie mit dem Setter ein Beobachtermuster einrichten, das den anderen Antworten hier ähnelt.


3
Dies ist die richtige und einfachste Lösung. Wie @NanoWizard sagt, sucht $ Digest servicesnicht nach Eigenschaften, die zum Service selbst gehören.
Sarpdoruk Tahmaz

28

Soweit ich das beurteilen kann, müssen Sie nichts so Aufwändiges tun. Sie haben Ihrem Bereich bereits foo aus dem Dienst zugewiesen, und da foo ein Array ist (und wiederum ein Objekt, wird es als Referenz zugewiesen!). Alles, was Sie tun müssen, ist etwa Folgendes:

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

 }

Wenn eine andere Variable in derselben Strg von der Änderung von foo abhängt, benötigen Sie eine Uhr, um foo zu beobachten und Änderungen an dieser Variablen vorzunehmen. Aber solange es sich um eine einfache Referenz handelt, ist das Beobachten nicht erforderlich. Hoffe das hilft.


35
Ich habe es versucht und konnte nicht $watchmit einem Primitiven arbeiten. Stattdessen habe ich eine Methode für den Dienst definiert, die den primitiven Wert zurückgibt: somePrimitive() = function() { return somePrimitive }Und ich habe dieser Methode eine $ scope-Eigenschaft zugewiesen : $scope.somePrimitive = aService.somePrimitive;. Dann habe ich die Scope-Methode im HTML verwendet: <span>{{somePrimitive()}}</span>
Mark Rajcok

4
@MarkRajcok Nein, benutze keine Grundelemente. Fügen Sie sie einem Objekt hinzu. Primitive sind keine veränderlichen Elemente und daher funktioniert die
Jimmy Kane,

3
@ JimmyKane, ja, Grundelemente sollten nicht für die bidirektionale Datenbindung verwendet werden, aber ich denke, die Frage betraf das Beobachten von Dienstvariablen und nicht das Einrichten einer bidirektionalen Bindung. Wenn Sie nur eine Serviceeigenschaft / -variable überwachen müssen, ist kein Objekt erforderlich - ein Grundelement kann verwendet werden.
Mark Rajcok

3
In diesem Setup kann ich aService-Werte aus dem Bereich ändern. Der Umfang ändert sich jedoch nicht, wenn sich der aService ändert.
Ouwen Huang

4
Das funktioniert auch bei mir nicht. Durch einfaches Zuweisen $scope.foo = aService.foowird die Bereichsvariable nicht automatisch aktualisiert.
Darwin Tech

9

Sie können den Dienst in $ rootScope einfügen und Folgendes beobachten:

myApp.run(function($rootScope, aService){
    $rootScope.aService = aService;
    $rootScope.$watch('aService', function(){
        alert('Watch');
    }, true);
});

In Ihrem Controller:

myApp.controller('main', function($scope){
    $scope.aService.foo = 'change';
});

Eine andere Möglichkeit ist die Verwendung einer externen Bibliothek wie: https://github.com/melanke/Watch.JS

Funktioniert mit: IE 9+, FF 4+, SF 5+, WebKit, CH 7+, OP 12+, BESEN, Node.JS, Rhino 1.7+

Sie können die Änderungen eines, mehrerer oder aller Objektattribute beobachten.

Beispiel:

var ex3 = {
    attr1: 0,
    attr2: "initial value of attr2",
    attr3: ["a", 3, null]
};   
watch(ex3, function(){
    alert("some attribute of ex3 changes!");
});
ex3.attr3.push("new value");​

2
Ich kann nicht glauben, dass diese Antwort nicht die am häufigsten gewählte ist !!! Dies ist die eleganteste Lösung (IMO), da sie die Informationsentropie reduziert und wahrscheinlich den Bedarf an zusätzlichen Mediationshandlern verringert. Ich würde dies mehr abstimmen, wenn ich könnte ...
Cody

Das Hinzufügen all Ihrer Dienste zum $ rootScope, seine Vorteile und seine potenziellen Fallstricke werden hier etwas detaillierter beschrieben: stackoverflow.com/questions/14573023/…
Zymotik

6

Sie können die Änderungen innerhalb der Fabrik selbst beobachten und dann eine Änderung senden

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

Dann in Ihrem Controller:

angular.module('MyApp').controller('aController', ['$rootScope', function ($rootScope) {

    $rootScope.$on('aFactory:keyChanged', function currentCityChanged(event, value) {
        // do something
    });
}]);

Auf diese Weise fügen Sie den gesamten zugehörigen Factory-Code in die Beschreibung ein und können sich dann nur auf die Übertragung von außen verlassen


6

== AKTUALISIERT ==

Sehr einfach jetzt in $ watch.

Stift hier .

HTML:

<div class="container" data-ng-app="app">

  <div class="well" data-ng-controller="FooCtrl">
    <p><strong>FooController</strong></p>
    <div class="row">
      <div class="col-sm-6">
        <p><a href="" ng-click="setItems([ { name: 'I am single item' } ])">Send one item</a></p>
        <p><a href="" ng-click="setItems([ { name: 'Item 1 of 2' }, { name: 'Item 2 of 2' } ])">Send two items</a></p>
        <p><a href="" ng-click="setItems([ { name: 'Item 1 of 3' }, { name: 'Item 2 of 3' }, { name: 'Item 3 of 3' } ])">Send three items</a></p>
      </div>
      <div class="col-sm-6">
        <p><a href="" ng-click="setName('Sheldon')">Send name: Sheldon</a></p>
        <p><a href="" ng-click="setName('Leonard')">Send name: Leonard</a></p>
        <p><a href="" ng-click="setName('Penny')">Send name: Penny</a></p>
      </div>
    </div>
  </div>

  <div class="well" data-ng-controller="BarCtrl">
    <p><strong>BarController</strong></p>
    <p ng-if="name">Name is: {{ name }}</p>
    <div ng-repeat="item in items">{{ item.name }}</div>
  </div>

</div>

JavaScript:

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

app.factory('PostmanService', function() {
  var Postman = {};
  Postman.set = function(key, val) {
    Postman[key] = val;
  };
  Postman.get = function(key) {
    return Postman[key];
  };
  Postman.watch = function($scope, key, onChange) {
    return $scope.$watch(
      // This function returns the value being watched. It is called for each turn of the $digest loop
      function() {
        return Postman.get(key);
      },
      // This is the change listener, called when the value returned from the above function changes
      function(newValue, oldValue) {
        if (newValue !== oldValue) {
          // Only update if the value changed
          $scope[key] = newValue;
          // Run onChange if it is function
          if (angular.isFunction(onChange)) {
            onChange(newValue, oldValue);
          }
        }
      }
    );
  };
  return Postman;
});

app.controller('FooCtrl', ['$scope', 'PostmanService', function($scope, PostmanService) {
  $scope.setItems = function(items) {
    PostmanService.set('items', items);
  };
  $scope.setName = function(name) {
    PostmanService.set('name', name);
  };
}]);

app.controller('BarCtrl', ['$scope', 'PostmanService', function($scope, PostmanService) {
  $scope.items = [];
  $scope.name = '';
  PostmanService.watch($scope, 'items');
  PostmanService.watch($scope, 'name', function(newVal, oldVal) {
    alert('Hi, ' + newVal + '!');
  });
}]);

1
Ich mag den PostmanService, aber wie muss ich die $ watch-Funktion auf dem Controller ändern, wenn ich mehr als eine Variable abhören muss?
Jedi

Hallo jedi, danke für das Heads Up! Ich habe den Stift und die Antwort aktualisiert. Ich empfehle, dafür eine weitere Überwachungsfunktion hinzuzufügen. Also habe ich PostmanService um eine neue Funktion erweitert. Ich hoffe das hilft :)
Hayatbiralem

Eigentlich ja ja :) Wenn Sie mehr Details über das Problem mitteilen, kann ich Ihnen vielleicht helfen.
Hayatbiralem

4

Aufbauend auf der Antwort dtheodor könnten Sie etwas Ähnliches wie das Folgende verwenden, um sicherzustellen, dass Sie nicht vergessen, die Registrierung des Rückrufs aufzuheben ... Einige lehnen es jedoch möglicherweise ab, den Rückruf $scopean einen Dienst weiterzuleiten.

factory('aService', function() {
  var observerCallbacks = [];

  /**
   * Registers a function that will be called when
   * any modifications are made.
   *
   * For convenience the callback is called immediately after registering
   * which can be prevented with `preventImmediate` param.
   *
   * Will also automatically unregister the callback upon scope destory.
   */
  this.registerObserver = function($scope, cb, preventImmediate){
    observerCallbacks.push(cb);

    if (preventImmediate !== true) {
      cb();
    }

    $scope.$on('$destroy', function () {
      observerCallbacks.remove(cb);
    });
  };

  function notifyObservers() {
    observerCallbacks.forEach(function (cb) {
      cb();
    });
  };

  this.foo = someNgResource.query().$then(function(){
    notifyObservers();
  });
});

Array.remove ist eine Erweiterungsmethode, die folgendermaßen aussieht:

/**
 * Removes the given item the current array.
 *
 * @param  {Object}  item   The item to remove.
 * @return {Boolean}        True if the item is removed.
 */
Array.prototype.remove = function (item /*, thisp */) {
    var idx = this.indexOf(item);

    if (idx > -1) {
        this.splice(idx, 1);

        return true;
    }
    return false;
};

2

Hier ist mein allgemeiner Ansatz.

mainApp.service('aService',[function(){
        var self = this;
        var callbacks = {};

        this.foo = '';

        this.watch = function(variable, callback) {
            if (typeof(self[variable]) !== 'undefined') {
                if (!callbacks[variable]) {
                    callbacks[variable] = [];
                }
                callbacks[variable].push(callback);
            }
        }

        this.notifyWatchersOn = function(variable) {
            if (!self[variable]) return;
            if (!callbacks[variable]) return;

            angular.forEach(callbacks[variable], function(callback, key){
                callback(self[variable]);
            });
        }

        this.changeFoo = function(newValue) {
            self.foo = newValue;
            self.notifyWatchersOn('foo');
        }

    }]);

In Ihrem Controller

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

    $scope._initWatchers = function() {
        aService.watch('foo', $scope._onFooChange);
    }

    $scope._onFooChange = function(newValue) {
        $scope.foo = newValue;
    }

    $scope._initWatchers();

}

FooCtrl.$inject = ['$scope', 'aService'];

2

Für diejenigen wie mich, die nur nach einer einfachen Lösung suchen, ist dies fast genau das, was Sie von der Verwendung normaler $ watch in Controllern erwarten. Der einzige Unterschied besteht darin, dass die Zeichenfolge in ihrem Javascript-Kontext und nicht in einem bestimmten Bereich ausgewertet wird. Sie müssen $ rootScope in Ihren Dienst einfügen, obwohl es nur verwendet wird, um sich ordnungsgemäß in die Digest-Zyklen einzubinden.

function watch(target, callback, deep) {
    $rootScope.$watch(function () {return eval(target);}, callback, deep);
};

2

Bei einem sehr ähnlichen Problem habe ich eine Funktion im Gültigkeitsbereich beobachtet und die Funktion die Dienstvariable zurückgeben lassen. Ich habe eine js Geige erstellt . Sie finden den Code unten.

    var myApp = angular.module("myApp",[]);

myApp.factory("randomService", function($timeout){
    var retValue = {};
    var data = 0;

    retValue.startService = function(){
        updateData();
    }

    retValue.getData = function(){
        return data;
    }

    function updateData(){
        $timeout(function(){
            data = Math.floor(Math.random() * 100);
            updateData()
        }, 500);
    }

    return retValue;
});

myApp.controller("myController", function($scope, randomService){
    $scope.data = 0;
    $scope.dataUpdated = 0;
    $scope.watchCalled = 0;
    randomService.startService();

    $scope.getRandomData = function(){
        return randomService.getData();    
    }

    $scope.$watch("getRandomData()", function(newValue, oldValue){
        if(oldValue != newValue){
            $scope.data = newValue;
            $scope.dataUpdated++;
        }
            $scope.watchCalled++;
    });
});

2

Ich kam zu dieser Frage, aber es stellte sich heraus, dass mein Problem darin bestand, dass ich setInterval verwendete, wenn ich den Winkelintervall-Intervall-Anbieter hätte verwenden sollen. Dies gilt auch für setTimeout (verwenden Sie stattdessen $ timeout). Ich weiß, dass es nicht die Antwort auf die Frage des OP ist, aber es könnte einigen helfen, wie es mir geholfen hat.


Sie können setTimeoutoder jede andere Nicht-Angular-Funktion verwenden, aber vergessen Sie nicht, den Code in den Rückruf mit einzuschließen $scope.$apply().
Magnetronnie

2

Ich habe auf dem anderen Thread eine wirklich gute Lösung mit einem ähnlichen Problem gefunden, aber einem völlig anderen Ansatz. Quelle: AngularJS: $ watch in der Direktive funktioniert nicht, wenn der Wert von $ rootScope geändert wird

Grundsätzlich sagt die Lösung dort NICHT zu verwenden, $watchda es sich um eine sehr schwere Lösung handelt. Stattdessen schlagen sie vor, $emitund zu verwenden $on.

Mein Problem war , zu sehen eine Variable in meinem Dienst und reagiert in Richtlinie . Und mit der oben genannten Methode ist es sehr einfach!

Mein Modul / Service-Beispiel:

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

Also im Grunde beobachte ich meinen user - wann immer er auf einen neuen Wert gesetzt wird ich $emiteinen user:changeStatus.

In meinem Fall habe ich in der Richtlinie Folgendes verwendet:

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

Jetzt höre ich in der Direktive auf die $rootScopeund auf die gegebene Änderung - ich reagiere jeweils. Sehr einfach und elegant!


1

// service: (hier nichts besonderes)

myApp.service('myService', function() {
  return { someVariable:'abc123' };
});

// Strg:

myApp.controller('MyCtrl', function($scope, myService) {

  $scope.someVariable = myService.someVariable;

  // watch the service and update this ctrl...
  $scope.$watch(function(){
    return myService.someVariable;
  }, function(newValue){
    $scope.someVariable = newValue;
  });
});

1

Ein bisschen hässlich, aber ich habe meinem Dienst die Registrierung von Bereichsvariablen hinzugefügt, um umzuschalten:

myApp.service('myService', function() {
    var self = this;
    self.value = false;
    self.c2 = function(){};
    self.callback = function(){
        self.value = !self.value; 
       self.c2();
    };

    self.on = function(){
        return self.value;
    };

    self.register = function(obj, key){ 
        self.c2 = function(){
            obj[key] = self.value; 
            obj.$apply();
        } 
    };

    return this;
});

Und dann in der Steuerung:

function MyCtrl($scope, myService) {
    $scope.name = 'Superhero';
    $scope.myVar = false;
    myService.register($scope, 'myVar');
}

Vielen Dank. Eine kleine Frage: Warum kehren Sie thisvon diesem Dienst zurück, anstatt self?
Shrekuu

4
Weil manchmal Fehler gemacht werden. ;-)
nclu

Gute Praxis return this;für Ihre Konstrukteure ;-)
Cody

1

Schauen Sie sich diesen Plunker an: Dies ist das einfachste Beispiel, das ich mir vorstellen kann

http://jsfiddle.net/HEdJF/

<div ng-app="myApp">
    <div ng-controller="FirstCtrl">
        <input type="text" ng-model="Data.FirstName"><!-- Input entered here -->
        <br>Input is : <strong>{{Data.FirstName}}</strong><!-- Successfully updates here -->
    </div>
    <hr>
    <div ng-controller="SecondCtrl">
        Input should also be here: {{Data.FirstName}}<!-- How do I automatically updated it here? -->
    </div>
</div>



// declare the app with no dependencies
var myApp = angular.module('myApp', []);
myApp.factory('Data', function(){
   return { FirstName: '' };
});

myApp.controller('FirstCtrl', function( $scope, Data ){
    $scope.Data = Data;
});

myApp.controller('SecondCtrl', function( $scope, Data ){
    $scope.Data = Data;
});

0

Ich habe hier einige schreckliche Beobachtermuster gesehen, die bei großen Anwendungen zu Speicherverlusten führen.

Ich bin vielleicht etwas spät dran, aber so einfach ist das.

Die Überwachungsfunktion sucht nach Referenzänderungen (primitiven Typen), wenn Sie etwas wie Array-Push sehen möchten. Verwenden Sie einfach:

someArray.push(someObj); someArray = someArray.splice(0);

Dadurch wird die Referenz aktualisiert und die Uhr von überall aktualisiert. Einschließlich einer Services-Getter-Methode. Alles, was ein Grundelement ist, wird automatisch aktualisiert.


0

Ich bin zu spät zum Teil, aber ich habe einen schöneren Weg gefunden, dies zu tun als die oben angegebene Antwort. Anstatt eine Variable zuzuweisen, die den Wert der Dienstvariablen enthält, habe ich eine an den Bereich angehängte Funktion erstellt, die die Dienstvariable zurückgibt.

Regler

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

Ich denke, das wird tun, was Sie wollen. Mein Controller überprüft mit dieser Implementierung ständig den Wert meines Dienstes. Ehrlich gesagt ist dies viel einfacher als die ausgewählte Antwort.


Warum wurde es herabgestuft? Ich habe auch viele Male ähnliche Techniken verwendet und es hat funktioniert.
undefiniert

0

Ich habe zwei einfache Dienstprogramme geschrieben, mit denen ich Änderungen der Diensteigenschaften nachverfolgen kann.

Wenn Sie die lange Erklärung überspringen möchten, können Sie zu jsfiddle gehen

  1. WatchObj

mod.service('WatchObj', ['$rootScope', WatchObjService]);

function WatchObjService($rootScope) {
  // returns watch function
  // obj: the object to watch for
  // fields: the array of fields to watch
  // target: where to assign changes (usually it's $scope or controller instance)
  // $scope: optional, if not provided $rootScope is use
  return function watch_obj(obj, fields, target, $scope) {
    $scope = $scope || $rootScope;
    //initialize watches and create an array of "unwatch functions"
    var watched = fields.map(function(field) {
      return $scope.$watch(
        function() {
          return obj[field];
        },
        function(new_val) {
          target[field] = new_val;
        }
      );
    });
    //unregister function will unregister all our watches
    var unregister = function unregister_watch_obj() {
      watched.map(function(unregister) {
        unregister();
      });
    };
    //automatically unregister when scope is destroyed
    $scope.$on('$destroy', unregister);
    return unregister;
  };
}

Dieser Dienst wird in der Steuerung folgendermaßen verwendet: Angenommen, Sie haben einen Dienst "testService" mit den Eigenschaften 'prop1', 'prop2', 'prop3'. Sie möchten die Bereiche 'prop1' und 'prop2' beobachten und ihnen zuweisen. Mit dem Uhrendienst sieht es so aus:

app.controller('TestWatch', ['$scope', 'TestService', 'WatchObj', TestWatchCtrl]);

function TestWatchCtrl($scope, testService, watch) {
  $scope.prop1 = testService.prop1;
  $scope.prop2 = testService.prop2;
  $scope.prop3 = testService.prop3;
  watch(testService, ['prop1', 'prop2'], $scope, $scope);
}

  1. apply Watch obj ist großartig, aber es reicht nicht aus, wenn Sie asynchronen Code in Ihrem Dienst haben. In diesem Fall verwende ich ein zweites Dienstprogramm, das so aussieht:

mod.service('apply', ['$timeout', ApplyService]);

function ApplyService($timeout) {
  return function apply() {
    $timeout(function() {});
  };
}

Ich würde es am Ende meines asynchronen Codes auslösen, um die $ Digest-Schleife auszulösen. So wie das:

app.service('TestService', ['apply', TestService]);

function TestService(apply) {
  this.apply = apply;
}
TestService.prototype.test3 = function() {
  setTimeout(function() {
    this.prop1 = 'changed_test_2';
    this.prop2 = 'changed2_test_2';
    this.prop3 = 'changed3_test_2';
    this.apply(); //trigger $digest loop
  }.bind(this));
}

Das alles zusammen wird also so aussehen (Sie können es ausführen oder die Geige öffnen ):

// TEST app code

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

app.controller('TestWatch', ['$scope', 'TestService', 'WatchObj', TestWatchCtrl]);

function TestWatchCtrl($scope, testService, watch) {
  $scope.prop1 = testService.prop1;
  $scope.prop2 = testService.prop2;
  $scope.prop3 = testService.prop3;
  watch(testService, ['prop1', 'prop2'], $scope, $scope);
  $scope.test1 = function() {
    testService.test1();
  };
  $scope.test2 = function() {
    testService.test2();
  };
  $scope.test3 = function() {
    testService.test3();
  };
}

app.service('TestService', ['apply', TestService]);

function TestService(apply) {
  this.apply = apply;
  this.reset();
}
TestService.prototype.reset = function() {
  this.prop1 = 'unchenged';
  this.prop2 = 'unchenged2';
  this.prop3 = 'unchenged3';
}
TestService.prototype.test1 = function() {
  this.prop1 = 'changed_test_1';
  this.prop2 = 'changed2_test_1';
  this.prop3 = 'changed3_test_1';
}
TestService.prototype.test2 = function() {
  setTimeout(function() {
    this.prop1 = 'changed_test_2';
    this.prop2 = 'changed2_test_2';
    this.prop3 = 'changed3_test_2';
  }.bind(this));
}
TestService.prototype.test3 = function() {
  setTimeout(function() {
    this.prop1 = 'changed_test_2';
    this.prop2 = 'changed2_test_2';
    this.prop3 = 'changed3_test_2';
    this.apply();
  }.bind(this));
}
//END TEST APP CODE

//WATCH UTILS
var mod = angular.module('watch_utils', []);

mod.service('apply', ['$timeout', ApplyService]);

function ApplyService($timeout) {
  return function apply() {
    $timeout(function() {});
  };
}

mod.service('WatchObj', ['$rootScope', WatchObjService]);

function WatchObjService($rootScope) {
  // target not always equals $scope, for example when using bindToController syntax in 
  //directives
  return function watch_obj(obj, fields, target, $scope) {
    // if $scope is not provided, $rootScope is used
    $scope = $scope || $rootScope;
    var watched = fields.map(function(field) {
      return $scope.$watch(
        function() {
          return obj[field];
        },
        function(new_val) {
          target[field] = new_val;
        }
      );
    });
    var unregister = function unregister_watch_obj() {
      watched.map(function(unregister) {
        unregister();
      });
    };
    $scope.$on('$destroy', unregister);
    return unregister;
  };
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div class='test' ng-app="app" ng-controller="TestWatch">
  prop1: {{prop1}}
  <br>prop2: {{prop2}}
  <br>prop3 (unwatched): {{prop3}}
  <br>
  <button ng-click="test1()">
    Simple props change
  </button>
  <button ng-click="test2()">
    Async props change
  </button>
  <button ng-click="test3()">
    Async props change with apply
  </button>
</div>

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.