AngularJS: Jak oglądać zmienne serwisowe?

414

Mam usługę, powiedz:

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

  return service;
}]);

I chciałbym użyć foodo sterowania listą renderowaną w HTML:

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

Aby sterownik mógł wykryć, kiedy aService.foojest aktualizowany, połączyłem ten wzorzec, w którym dodałem usługę do kontrolera, $scopea następnie użyłem $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;
    }
  });
}

Wydaje mi się to długotrwałe i powtarzam to w każdym kontrolerze, który korzysta ze zmiennych usługi. Czy jest lepszy sposób na obejrzenie wspólnych zmiennych?

berto
źródło
1
Możesz przekazać trzeci parametr do $ watch ustawionego na true, aby dokładnie obejrzeć usługę i wszystkie jej właściwości.
SirTophamHatt
7
$ scope.foo = aService.foo jest wystarczający, możesz stracić powyższą linię. A to, co robi wewnątrz $ watch, nie ma sensu, jeśli chcesz przypisać nową wartość do $ scope.foo po prostu zrób to ...
Jin
4
Czy możesz po prostu odwoływać się aService.foow znacznikach HTML? (jak to: plnkr.co/edit/aNrw5Wo4Q0IxR2loipl5?p=preview )
thetallweeks
1
Dodałem przykład bez wywołań zwrotnych lub zegarków $, patrz odpowiedź poniżej ( jsfiddle.net/zymotik/853wvv7s )
Zymotik
1
@MikeGledhill, masz rację. Myślę, że wynika to z natury Javascript, ten wzór można zobaczyć w wielu innych miejscach (nie tylko w Angular, ale ogólnie w JS). Z jednej strony przenosisz wartość (i nie wiąże się), az drugiej przenosisz obiekt (lub wartość, która odwołuje się do obiektu ...), i dlatego właściwości są poprawnie aktualizowane (jak idealnie pokazano w przykładzie Zymotika powyżej).
Christophe Vidal,

Odpowiedzi:

277

Zawsze możesz użyć starego dobrego wzorca obserwatora, jeśli chcesz uniknąć tyranii i kosztów ogólnych $watch.

W serwisie:

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

I w kontrolerze:

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

  aService.registerObserverCallback(updateFoo);
  //service now in control of updating foo
};
dtheodor
źródło
21
@Moo nasłuchuj $destoryzdarzenia w zakresie i dodaj metodę wyrejestrowania doaService
Jamie
13
Jakie są zalety tego rozwiązania? Potrzebuje więcej kodu w usłudze i nieco tej samej ilości kodu w kontrolerze (ponieważ musimy również wyrejestrować się po $ zniszczyć). Mógłbym powiedzieć o szybkości wykonania, ale w większości przypadków to po prostu nie będzie miało znaczenia.
Alex Che
6
nie jestem pewien, jak to jest lepsze rozwiązanie niż $ watch, pytający poprosił o prosty sposób udostępniania danych, wygląda to jeszcze bardziej kłopotliwie. Wolę używać $ broadcast niż tego
Jin
11
$watchWzorzec vs obserwator to po prostu wybór odpytywania lub odpychania i zasadniczo jest to kwestia wydajności, więc używaj go, gdy liczy się wydajność. Używam wzorca obserwatora, kiedy inaczej musiałbym „głęboko” obserwować złożone obiekty. Dołączam całe usługi do zakresu $ zamiast oglądać wartości pojedynczej usługi. Unikam zegarka Angulara jak diabeł, tego wystarczająco dużo dzieje się w dyrektywach i rodzimym wiązaniu danych kątowych.
dtheodor
107
Powodem, dla którego używamy frameworku takiego jak Angular, jest nie gotowanie własnych wzorców obserwatorów.
Zaklinacz kodów,
230

W takim scenariuszu, w którym wiele / nieznanych obiektów może być zainteresowanych zmianami, skorzystaj $rootScope.$broadcastze zmienianego elementu.

Zamiast tworzyć własny rejestr słuchaczy (które muszą zostać wyczyszczone przy różnych zniszczeniach $), powinieneś mieć możliwość skorzystania $broadcastz danej usługi.

Nadal musisz kodować $onprogramy obsługi w każdym detektorze, ale wzorzec jest oddzielony od wielu wywołań do, $digestco pozwala uniknąć ryzyka długotrwałych obserwatorów.

W ten sposób również słuchacze mogą przychodzić i wychodzić z DOM i / lub różnych zakresów potomnych bez zmiany sposobu działania usługi.

** aktualizacja: przykłady **

Nadawanie byłoby najbardziej sensowne w „globalnych” usługach, które mogą wpływać na niezliczone inne rzeczy w Twojej aplikacji. Dobrym przykładem jest usługa użytkownika, w której może się zdarzyć wiele zdarzeń, takich jak logowanie, wylogowanie, aktualizacja, bezczynność itp. Uważam, że to tutaj nadawanie ma sens, ponieważ każdy zasięg może nasłuchiwać zdarzenia, bez nawet wstrzyknięcie usługi i nie trzeba oceniać żadnych wyrażeń ani wyników pamięci podręcznej w celu sprawdzenia zmian. Po prostu odpala i zapomina (więc upewnij się, że jest to powiadomienie o pożarze i zapomnieniu, a nie coś, co wymaga działania)

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

Powyższa usługa nadałaby komunikat do każdego zakresu, gdy funkcja save () zakończyła się i dane były prawidłowe. Alternatywnie, jeśli jest to zasób $ lub przesłanie ajax, przenieś wywołanie rozgłoszeniowe do wywołania zwrotnego, aby uruchamiało się, gdy serwer odpowiedział. Emisje pasują szczególnie do tego wzorca, ponieważ każdy słuchacz po prostu czeka na wydarzenie bez konieczności sprawdzania zasięgu przy każdym $ streszczeniu. Słuchacz wyglądałby następująco:

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

 }]);

Jedną z korzyści tego jest eliminacja wielu zegarków. Jeśli łączysz pola lub wyprowadzasz wartości jak w powyższym przykładzie, będziesz musiał obserwować zarówno imię, jak i nazwisko. Oglądanie funkcji getUser () działałoby tylko wtedy, gdy obiekt użytkownika został zastąpiony podczas aktualizacji, nie uruchamiałby się, gdyby obiekt użytkownika został po prostu zaktualizowany. W takim przypadku musisz zrobić dokładny zegarek, który jest bardziej intensywny.

$ broadcast wysyła wiadomość z zakresu, do którego została wywołana, do dowolnych zakresów potomnych. Więc wywołanie go z $ rootScope będzie uruchamiane na każdym zasięgu. Jeśli na przykład miałbyś nadawać z zakresu kontrolera, uruchamiałby się tylko w zakresach dziedziczących z zakresu kontrolera. $ emit idzie w przeciwnym kierunku i zachowuje się podobnie jak zdarzenie DOM, ponieważ bąbelki w górę łańcucha zasięgu.

Należy pamiętać, że istnieją scenariusze, w których $ broadcast ma wiele sensu, i są scenariusze, w których $ watch jest lepszą opcją - szczególnie jeśli jest w zakresie izolowanym z bardzo specyficznym wyrażeniem watch.

Matt Pileggi
źródło
1
Wyjście z cyklu $ digest jest dobrą rzeczą, szczególnie jeśli zmiany, które obserwujesz, nie są wartością, która bezpośrednio i natychmiast przejdzie do DOM.
XML
Czy istnieje możliwość uniknięcia metody .save (). Wygląda na przesadę, gdy monitorujesz tylko aktualizację jednej zmiennej w usłudze sharedService. Czy możemy oglądać zmienną z poziomu sharedService i nadawać, gdy się zmienia?
JerryKur
Wypróbowałem kilka sposobów udostępniania danych między kontrolerami, ale to jedyny, który zadziałał. Dobra gra, proszę pana.
abettermap
Wolę to od innych odpowiedzi, wydaje się mniej hakerskie , dzięki
JMK
9
Jest to prawidłowy wzorzec projektowy tylko wtedy, gdy kontroler konsumujący ma wiele możliwych źródeł danych; innymi słowy, jeśli masz sytuację MIMO (wiele wejść / wiele wyjść). Jeśli używasz tylko wzorca jeden do wielu, powinieneś używać bezpośredniego odwoływania się do obiektów i pozwolić środowisku Angular wykonać dwukierunkowe wiązanie za Ciebie. Horkyze połączył to poniżej i jest to dobre wyjaśnienie automatycznego dwustronnego wiązania i jego ograniczeń: stsc3000.github.io/blog/2013/10/26/…
Charles
47

Używam podobnego podejścia jak @dtheodot, ale używam obietnicy kątowej zamiast przekazywać wywołania zwrotne

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

Następnie, gdziekolwiek, użyj myService.setFoo(foo)metody aktualizacji foow serwisie. W kontrolerze możesz użyć go jako:

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

Pierwsze dwa argumenty thento wywołania zwrotne sukcesu i błędu, trzeci to powiadomienie zwrotne.

Referencje dla $ q.

Krym
źródło
Jaka byłaby przewaga tej metody w stosunku do audycji $ opisanej poniżej przez Matta Pileggiego?
Fabio
Obie metody mają swoje zastosowania. Zaletą transmisji byłaby dla mnie czytelność dla ludzi i możliwość słuchania w większej liczbie miejsc tego samego wydarzenia. Wydaje mi się, że główną wadą jest to, że transmisja emituje komunikat do wszystkich zakresów potomnych, więc może to być problem z wydajnością.
Krym,
2
Miałem problem polegający na tym, że działanie $scope.$watchna zmiennej usługi nie działało (zakres, na który patrzyłem, to modal, który odziedziczył po nim $rootScope) - zadziałało. Fajna sztuczka, dziękuję za udostępnienie!
Seiyria
4
Jak byś posprzątał po tym podejściu? Czy możliwe jest usunięcie zarejestrowanego wywołania zwrotnego z obietnicy, gdy zakres zostanie zniszczony?
Abris,
Dobre pytanie. Naprawdę nie wiem. Spróbuję wykonać kilka testów, w jaki sposób możesz usunąć powiadomienie o oddzwonieniu z obietnicy.
Krym
41

Bez zegarków i wywołań zwrotnych obserwatora ( 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>

Ten JavaScript działa, gdy przekazujemy obiekt z usługi, a nie wartość. Gdy obiekt JavaScript jest zwracany z usługi, Angular dodaje zegarki do wszystkich swoich właściwości.

Zauważ też, że używam „var self = this”, ponieważ muszę zachować odniesienie do oryginalnego obiektu, gdy wykonuje się limit czasu $, w przeciwnym razie „this” będzie odnosić się do obiektu okna.

Zymotik
źródło
3
To świetne podejście! Czy istnieje sposób na powiązanie tylko zakresu usługi z zakresem zamiast całej usługi? Po prostu robienie $scope.count = service.countnie działa.
jvannistelrooy
Możesz także zagnieździć właściwość wewnątrz (dowolnego) obiektu, aby był przekazywany przez referencję. $scope.data = service.data <p>Count: {{ data.count }}</p>
Alex Ross,
1
Doskonałe podejście! Chociaż na tej stronie znajduje się wiele silnych, funkcjonalnych odpowiedzi, jest to zdecydowanie a) najłatwiejszy do wdrożenia i b) najłatwiejszy do zrozumienia podczas czytania kodu. Ta odpowiedź powinna być znacznie wyższa niż obecnie.
CodeMoose
Dzięki @CodeMoose, uprościłem go jeszcze dziś dla tych, którzy są nowi w AngularJS / JavaScript.
Zymotik
2
Niech Bóg Cię błogosławi. Powiedziałbym, że zmarnowałem miliony godzin. Ponieważ walczyłem z 1.5 i angularjs zmieniłem z 1 na 2, a także chciałem udostępnić dane
Amna
29

Natknąłem się na to pytanie, szukając czegoś podobnego, ale myślę, że zasługuje na dokładne wyjaśnienie tego, co się dzieje, a także kilka dodatkowych rozwiązań.

Gdy wyrażenie HTML, takie jak użyte, jest obecne w kodzie HTML, Angular automatycznie konfiguruje $watchfor $scope.fooi będzie aktualizować HTML przy każdej $scope.foozmianie.

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

Niewypowiedzianym problemem jest to, że jedna z dwóch rzeczy wpływa na to aService.foo , że zmiany nie są wykrywane. Te dwie możliwości to:

  1. aService.foo ustawia się za każdym razem do nowej tablicy, powodując, że odwołanie do niej jest nieaktualne.
  2. aService.foojest aktualizowany w taki sposób, że $digestcykl nie jest uruchamiany podczas aktualizacji.

Problem 1: Nieaktualne referencje

Biorąc pod uwagę pierwszą możliwość, zakładając, że $digeststosuje się a, jeśli aService.foozawsze była to ta sama tablica, zestaw automatycznie $watchwykryłby zmiany, jak pokazano w poniższym fragmencie kodu.

Rozwiązanie 1-a: Upewnij się, że tablica lub obiekt jest tym samym obiektem przy każdej aktualizacji

Jak widać, NG-repeat rzekomo przypisane do aService.foonie aktualizuje kiedy aService.foozmiany, ale ng-repeat dołączone do aService2.foo robi . Jest tak, ponieważ nasze odniesienie do aService.foojest nieaktualne, ale nasze odniesienie do aService2.foonie jest. Utworzyliśmy odwołanie do początkowej tablicy $scope.foo = aService.foo;, która została następnie odrzucona przez usługę przy następnej aktualizacji, co oznacza, że $scope.foonie odnosi się już do tablicy, której chcieliśmy.

Jednakże, chociaż istnieje kilka sposobów, aby upewnić się, że początkowe odniesienie jest utrzymywane w takcie, czasami może być konieczna zmiana obiektu lub tablicy. A co jeśli właściwość usługi odwołuje się do prymitywu takiego jak Stringlub Number? W takich przypadkach nie możemy po prostu polegać na referencji. Więc co można zrobić?

Kilka podanych wcześniej odpowiedzi już daje pewne rozwiązania tego problemu. Jednak osobiście opowiadam się za zastosowaniem prostej metody sugerowanej przez Jina i thetallweeks w komentarzach:

po prostu odwołaj aService.foo w znaczniku HTML

Rozwiązanie 1-b: Dołącz usługę do zakresu i odwołanie {service}.{property}w kodzie HTML.

Czyli po prostu zrób to:

HTML:

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

JS:

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

W ten sposób $watchzostanie rozwiązany aService.fookażdy $digest, który uzyska poprawnie zaktualizowaną wartość.

Jest to rodzaj tego, co próbujesz zrobić ze swoim obejściem, ale w znacznie mniejszym stopniu. Dodałeś niepotrzebne $watchw kontrolerze, który jawnie nakłada foosię za $scopekażdym razem, gdy się zmienia. Nie potrzebujesz tego dodatkowego, $watchgdy dołączasz aServicezamiast aService.foodo $scopei przypisujesz się bezpośrednio do aService.fooznaczników.


To wszystko dobrze i dobrze, jeśli zastosuje się $digestcykl. W powyższych przykładach użyłem $intervalusługi Angulara do aktualizacji tablic, która automatycznie uruchamia $digestpętlę po każdej aktualizacji. Ale co, jeśli zmienne serwisowe (z jakiegokolwiek powodu) nie są aktualizowane w „świecie Angular”. Innymi słowy, czy nie mamy $digestcyklu aktywowanego automatycznie przy każdej zmianie właściwości usługi?


Problem 2: Brak $digest

Wiele rozwiązań tutaj rozwiązuje ten problem, ale zgadzam się z Code Whisperer :

Powodem, dla którego używamy frameworku takiego jak Angular, jest nie gotowanie własnych wzorców obserwatorów

Dlatego wolałbym nadal używać aService.fooodwołania w znacznikach HTML, jak pokazano w drugim przykładzie powyżej, i nie musiałbym rejestrować dodatkowego wywołania zwrotnego w kontrolerze.

Rozwiązanie 2: Użyj setera i gettera z $rootScope.$apply()

Byłem zaskoczony, że nikt jeszcze nie zasugerował użycia setera i gettera . Ta funkcja została wprowadzona w ECMAScript5 i dlatego istnieje już od lat. Oczywiście oznacza to, że jeśli z jakiegoś powodu musisz obsługiwać naprawdę stare przeglądarki, ta metoda nie będzie działać, ale wydaje mi się, że JavaScript i gettery są znacznie słabo wykorzystywane. W tym konkretnym przypadku mogą być bardzo przydatne:

factory('aService', [
  '$rootScope',
  function($rootScope) {
    var realFoo = [];

    var service = {
      set foo(a) {
        realFoo = a;
        $rootScope.$apply();
      },
      get foo() {
        return realFoo;
      }
    };
  // ...
}

Tutaj dodałem „prywatny” zmienną w funkcji serwisowej: realFoo. Ten plik jest aktualizowany i pobierany przy użyciu odpowiednio funkcji get foo()i set foo()na serviceobiekcie.

Zwróć uwagę na użycie $rootScope.$apply()funkcji set. Zapewnia to, że Angular będzie świadomy wszelkich zmian w service.foo. Jeśli pojawią się błędy „inprog”, zobacz tę przydatną stronę referencyjną lub jeśli używasz Angular> = 1.3, możesz po prostu użyć $rootScope.$applyAsync().

Uważaj na to, jeśli aService.foojest bardzo często aktualizowany, ponieważ może to znacząco wpłynąć na wydajność. Jeśli wydajność byłaby problemem, możesz ustawić wzorzec obserwatora podobny do innych odpowiedzi tutaj za pomocą setera.

NanoWizard
źródło
3
To jest prawidłowe i najłatwiejsze rozwiązanie. Jak mówi @NanoWizard, $ digest sprawdza, czy servicesnie dla właściwości należących do samej usługi.
Sarpdoruk Tahmaz
28

O ile mogę powiedzieć, nie musisz robić czegoś tak skomplikowanego jak to. Przypisałeś już foo z usługi do swojego zakresu, a ponieważ foo jest tablicą (i z kolei jest obiektem przypisywanym przez referencję!). Wszystko, co musisz zrobić, to coś takiego:

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

 }

Jeśli jakaś inna zmienna w tym samym Ctrl jest zależna od zmiany Foo, to tak, potrzebujesz obserwować Foo i dokonać zmian w tej zmiennej. Ale pod warunkiem, że jest to proste oglądanie referencji, nie jest konieczne. Mam nadzieję że to pomoże.

ganaraj
źródło
35
Próbowałem i nie mogłem $watchpracować z prymitywem. Zamiast tego, definiuje metodę na usługę, która zwraca wartość prymitywną: somePrimitive() = function() { return somePrimitive }I przypisana właściwość $ zakres do tej metody: $scope.somePrimitive = aService.somePrimitive;. Następnie użyłem metody zakresu w HTML: <span>{{somePrimitive()}}</span>
Mark Rajcok
4
@MarkRajcok Nie nie używaj prymitywów. Dodaj je do obiektu. Prymitywy nie są modyfikowalne, więc dwukierunkowe wiązanie danych nie będzie działać
Jimmy Kane,
3
@JimmyKane, tak, prymitywów nie należy używać do dwukierunkowego wiązania danych, ale myślę, że pytanie dotyczyło oglądania zmiennych usług, a nie konfigurowania wiązania dwukierunkowego. Jeśli potrzebujesz tylko obejrzeć właściwość / zmienną usługi, obiekt nie jest wymagany - można użyć operacji podstawowej.
Mark Rajcok
3
W tej konfiguracji mogę zmienić wartości usługi z zakresu. Ale zakres nie zmienia się w odpowiedzi na zmianę usługi.
Ouwen Huang
4
To też nie działa dla mnie. Proste przypisanie $scope.foo = aService.foonie aktualizuje automatycznie zmiennej zasięgu.
Darwin Tech
9

Możesz wstawić usługę do $ rootScope i oglądać:

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

W twoim kontrolerze:

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

Inną opcją jest użycie zewnętrznej biblioteki, takiej jak: https://github.com/melanke/Watch.JS

Współpracuje z: IE 9+, FF 4+, SF 5+, WebKit, CH 7+, OP 12+, BESEN, Node.JS, Rhino 1.7+

Możesz obserwować zmiany jednego, wielu lub wszystkich atrybutów obiektu.

Przykład:

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");​
Rodolfo Jorge Nemer Nogueira
źródło
2
NIE MOGĘ WIERZYĆ, ŻE ODPOWIEDŹ NIE JEST NAJBARDZIEJ GŁOSOWANA! Jest to najbardziej eleganckie rozwiązanie (IMO), ponieważ zmniejsza entropię informacyjną i prawdopodobnie zmniejsza potrzebę dodatkowych procedur mediacyjnych. Głosowałbym bardziej, gdybym mógł ...
Cody
Dodawanie wszystkich swoich usług do $ rootScope, jego zalety i potencjalne pułapki zostały szczegółowo omówione tutaj: stackoverflow.com/questions/14573023/...
Zymotik
6

Możesz obejrzeć zmiany w samej fabryce, a następnie opublikować zmianę

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

Następnie w kontrolerze:

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

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

W ten sposób umieścisz cały powiązany kod fabryczny w jego opisie, możesz polegać tylko na transmisji z zewnątrz

Flavien Volken
źródło
6

== AKTUALIZACJA ==

Bardzo proste teraz w zegarku $.

Pióro tutaj .

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 + '!');
  });
}]);
hayatbiralem
źródło
1
podoba mi się PostmanService, ale jak muszę zmienić funkcję $ watch na kontrolerze, jeśli muszę słuchać więcej niż jednej zmiennej?
jedi
Cześć Jedi, dzięki za heads up! Zaktualizowałem pióro i odpowiedź. Polecam dodać do tego kolejną funkcję zegarka. Dodałem więc nową funkcję do PostmanService. Mam nadzieję, że to pomoże :)
hayatbiralem
Właściwie to tak :) Jeśli podzielisz się bardziej szczegółowymi informacjami na temat problemu, może mogę ci pomóc.
hayatbiralem
4

Opierając się na odpowiedzi dtheodora, możesz użyć czegoś podobnego do poniższego, aby upewnić się, że nie zapomnisz wyrejestrować połączenia zwrotnego ... Niektórzy mogą jednak sprzeciwić się przekazaniu $scopeusługi.

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 to metoda rozszerzenia, która wygląda następująco:

/**
 * 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;
};
Jamie
źródło
2

Oto moje ogólne podejście.

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

    }]);

W twoim kontrolerze

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'];
elitechance
źródło
2

Dla takich jak ja, którzy szukają prostego rozwiązania, robi to prawie dokładnie to, czego oczekujesz od używania normalnego $ watch w kontrolerach. Jedyną różnicą jest to, że ocenia ciąg w kontekście javascript, a nie w określonym zakresie. Będziesz musiał wprowadzić $ rootScope do swojej usługi, chociaż służy on tylko do prawidłowego podłączenia do cykli trawienia.

function watch(target, callback, deep) {
    $rootScope.$watch(function () {return eval(target);}, callback, deep);
};
Justus Wingert
źródło
2

w obliczu bardzo podobnego problemu obejrzałem funkcję w zakresie i kazałem tej funkcji zwrócić zmienną usługi. Stworzyłem skrzypce js . kod znajdziesz poniżej.

    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++;
    });
});
pieśni
źródło
2

Przyszedłem do tego pytania, ale okazało się, że mój problem polega na tym, że korzystałem z setInterval, kiedy powinienem był używać dostawcy kątowego $ interwału. Dotyczy to również setTimeout (zamiast tego użyj $ timeout). Wiem, że to nie jest odpowiedź na pytanie PO, ale może pomóc niektórym, ponieważ pomogło mi.

honkskillet
źródło
Możesz użyć setTimeoutdowolnej innej funkcji innej niż Angular, ale nie zapomnij zawinąć kodu w wywołanie zwrotne $scope.$apply().
magnetronnie,
2

Znalazłem naprawdę świetne rozwiązanie w drugim wątku z podobnym problemem, ale zupełnie innym podejściem. Źródło: AngularJS: $ watch wewnątrz dyrektywy nie działa po zmianie wartości $ rootScope

Zasadniczo rozwiązanie mówi NIE używać, $watchponieważ jest to bardzo ciężkie rozwiązanie. Zamiast tego proponują użycie $emiti $on.

Moim problemem było obejrzenie zmiennej w moim serwisie i zareagowanie zgodnie z dyrektywą . I powyższą metodą jest to bardzo proste!

Przykład mojego modułu / usługi:

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

Więc zasadniczo oglądać Moje strony user - zawsze, gdy jest ustawiony na nową wartość I stanu.$emituser:change

Teraz w moim przypadku zastosowałem w dyrektywie :

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

Teraz w dyrektywie słucham $rootScopei na temat danej zmiany - odpowiednio reaguję. Bardzo łatwe i eleganckie!

Atais
źródło
1

// service: (tutaj nic specjalnego)

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

// ctrl:

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;
  });
});
Nawlbergs
źródło
1

Trochę brzydkie, ale dodałem rejestrację zmiennych zakresu do mojej usługi dla przełącznika:

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

A następnie w kontrolerze:

function MyCtrl($scope, myService) {
    $scope.name = 'Superhero';
    $scope.myVar = false;
    myService.register($scope, 'myVar');
}
nclu
źródło
Dzięki. Małe pytanie: dlaczego thiszamiast tego wracasz z tej usługi self?
shrekuu,
4
Ponieważ czasami popełniane są błędy. ;-)
nclu
Dobra praktyka, aby return this;wyjść poza konstruktorów ;-)
Cody
1

Spójrz na tego plunkera :: to najprostszy przykład, jaki mogłem wymyślić

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;
});
anandharshan
źródło
0

Widziałem tutaj okropne wzorce obserwatorów, które powodują wycieki pamięci w dużych aplikacjach.

Mogę się trochę spóźnić, ale to takie proste.

Funkcja zegarka obserwuje zmiany referencyjne (typy pierwotne), jeśli chcesz obejrzeć coś w rodzaju wypychania tablicy, po prostu użyj:

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

Spowoduje to zaktualizowanie referencji i aktualizację zegarka z dowolnego miejsca. W tym metoda pobierania usług. Wszystko, co jest prymitywne, zostanie zaktualizowane automatycznie.

Oliver Dixon
źródło
0

Jestem spóźniony do tej części, ale znalazłem lepszy sposób na zrobienie tego niż odpowiedź zamieszczona powyżej. Zamiast przypisywać zmienną do przechowywania wartości zmiennej usługi, utworzyłem funkcję dołączoną do zakresu, która zwraca zmienną usługi.

kontroler

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

Myślę, że zrobi to, co chcesz. Dzięki tej implementacji mój kontroler stale sprawdza wartość mojej usługi. Szczerze mówiąc, jest to o wiele prostsze niż wybrana odpowiedź.

alaboudi
źródło
dlaczego został odrzucony .. Użyłem również podobnej techniki wiele razy i zadziałało.
niezdefiniowany
0

Napisałem dwie proste usługi narzędziowe, które pomagają mi śledzić zmiany właściwości usługi.

Jeśli chcesz pominąć długie wyjaśnienie, możesz przejść cieśniną do jsfiddle

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

Ta usługa jest używana w kontrolerze w następujący sposób: Załóżmy, że masz usługę „testService” o właściwościach „prop1”, „prop2”, „prop3”. Chcesz obejrzeć i przypisać do zakresu „prop1” i „prop2”. Dzięki usłudze zegarka będzie to wyglądać tak:

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. Zastosuj Watch obj jest świetny, ale nie wystarczy, jeśli masz kod asynchroniczny w swojej usłudze. W takim przypadku używam drugiego narzędzia, które wygląda tak:

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

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

Wywołałbym go na końcu mojego kodu asynchronicznego, aby uruchomić pętlę $ digest. Tak:

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

Wszystko to razem będzie wyglądać tak (możesz to uruchomić lub otworzyć skrzypce ):

// 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>

Max
źródło