AngularJS: Hizmet değişkenleri nasıl izlenir?


414

Bir hizmetim var, söyle:

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

  return service;
}]);

Ve fooHTML'de oluşturulan bir listeyi kontrol etmek için kullanmak istiyorum :

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

Denetleyicinin ne zaman aService.foogüncellendiğini algılaması için, denetleyiciye aService eklediğim $scopeve daha sonra kullandığım bu deseni bir araya topladım $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;
    }
  });
}

Bu, uzun süreli hissediyor ve hizmetin değişkenlerini kullanan her denetleyicide tekrarlıyorum. Paylaşılan değişkenleri izlemenin daha iyi bir yolu var mı?


1
AService ve tüm özelliklerini derinlemesine izlemek için $ watch değerine üçüncü bir parametreyi iletebilirsiniz.
SirTophamHatt

7
$ scope.foo = aService.foo yeterlidir, yukarıdaki satırı kaybedebilirsiniz. Ve $ watch içinde yaptığı şey mantıklı değil, eğer $ scope'a yeni bir değer atamak istiyorsanız. Sadece yapın ...
Jin

4
aService.fooHtml işaretlemesinde referans verebilir misiniz? (bunun gibi: plnkr.co/edit/aNrw5Wo4Q0IxR2loipl5?p=preview )
thetallweeks

1
Geri aramalar veya $ saatler olmadan bir örnek ekledim, aşağıdaki cevaba bakın ( jsfiddle.net/zymotik/853wvv7s )
Zymotik

1
@MikeGledhill, haklısın. Javascript'in doğasından dolayı olduğunu düşünüyorum, bu deseni başka birçok yerde görebilirsiniz (sadece Angular'da değil, genel olarak JS'de). Bir yandan, değeri aktarıyorsunuz (ve sınırlanmıyor) ve diğer yandan bir nesneyi (veya nesneye başvuran değeri ...) aktarıyorsunuz ve bu yüzden özellikler doğru bir şekilde güncelleniyor (mükemmel gibi yukarıdaki Zymotik örneğinde gösterilmiştir).
Christophe Vidal

Yanıtlar:


277

Zulüm ve ek yükten kaçınmak istiyorsanız her zaman eski eski gözlemci desenini kullanabilirsiniz $watch.

Serviste:

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

Ve denetleyicide:

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

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

21
@Moo $destorykapsamdaki etkinliği dinleyin ve kayıt dışı bir yöntem ekleyinaService
Jamie

13
Bu çözümün artıları nelerdir? Bir hizmette daha fazla koda ve bir denetleyicide aynı miktarda koda ihtiyaç duyar (çünkü $ destroy'da da kayıt olmamız gerekir). Yürütme hızı için söyleyebilirim, ancak çoğu durumda önemli değil.
Alex Che

6
bunun $ watch'dan daha iyi bir çözüm olduğundan emin değilim, soru soran verileri paylaşmak için basit bir yol istiyordu, daha da hantal görünüyor. Bundan daha fazla $ yayın kullanmayı tercih ederim
Jin

11
$watchvs gözlemci modeli basitçe yoklamayı mı yoksa zorlamayı mı seçiyor ve temelde bir performans meselesi, bu yüzden performans önemli olduğunda kullanın. Gözlemci desenini kullanırım, aksi takdirde karmaşık nesneleri "derinlemesine" izlemem gerekirdi. Tek bir hizmet değerini izlemek yerine tüm hizmetleri $ kapsamına eklerim. Ben açısal $ şeytan gibi izlemek kaçının, direktifleri ve doğal açısal veri bağlayıcı bu oluyor yeterli.
dtheodor

107
Angular gibi bir çerçeve kullanmamızın nedeni, kendi gözlemci kalıplarımızı pişirmemek.
Code Whisperer

230

Birden çok / bilinmeyen nesnenin değişikliklerle ilgilenebileceği böyle bir senaryoda, $rootScope.$broadcastdeğiştirilen öğeden kullanın .

Kendi dinleyicilerin kayıtlarını oluşturmak yerine (çeşitli $ yıkımlarda temizlenmesi gerekir), $broadcastsöz konusu hizmetten yararlanabilmelisiniz.

Yine $onde her dinleyicideki işleyicileri kodlamanız gerekir, ancak desen birden fazla çağrıdan ayrıştırılır $digestve böylece uzun süredir çalışan izleyici riskini önler.

Bu şekilde, dinleyiciler , hizmetin davranışını değiştirmeden DOM ve / veya farklı alt kapsamlardan gelip gidebilirler .

** güncelleme: örnekler **

Yayınlar, uygulamanızdaki sayısız şeyi etkileyebilecek "global" hizmetlerde en anlamlı olanıdır. Buna iyi bir örnek, giriş, çıkış, güncelleme, boşta, vb. Gibi gerçekleşebilecek bir dizi olayın gerçekleştiği bir Kullanıcı hizmetidir. hizmeti enjekte etse bile, değişiklikleri incelemek için herhangi bir ifadeyi veya önbellek sonucunu değerlendirmesine gerek yoktur. Yalnızca tetiklenir ve unutur (bu nedenle, eylem gerektiren bir şey değil, bir ateşle ve unut bildirimi olduğundan emin olun)

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

Yukarıdaki hizmet, save () işlevi tamamlandığında ve veriler geçerli olduğunda her kapsama bir mesaj gönderir. Alternatif olarak, bu bir $ kaynak veya ajax gönderimi ise, yayın çağrısını geri çağırmaya taşıyın, böylece sunucu yanıt verdiğinde etkinleşir. Yayınlar bu kalıba çok uygundur, çünkü her dinleyici, her bir $ özeti kapsamını incelemeye gerek kalmadan etkinliği bekler. Dinleyici şöyle görünecektir:

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

 }]);

Bunun yararlarından biri, çoklu saatlerin ortadan kaldırılmasıdır. Alanları birleştiriyor veya yukarıdaki örnek gibi değerler türetiyorsanız, hem ad hem de soyad özelliklerini izlemeniz gerekir. GetUser () işlevini izlemek yalnızca kullanıcı nesnesi güncelleştirmelerle değiştirildiyse çalışır, kullanıcı nesnesi yalnızca özellikleri güncellendiğinde tetiklenmez. Bu durumda derin bir saat yapmanız gerekir ve bu daha yoğundur.

$ broadcast mesajı, çağrıldığı kapsamdan alt kapsamlara gönderir. Yani $ rootScope'dan çağırmak her kapsamda tetiklenecektir. Örneğin, denetleyicinizin kapsamından $ yayınlayacak olsaydınız, yalnızca denetleyicinizin kapsamından miras alan kapsamlarda tetiklenirdi. $ emit ters yöne gider ve bir DOM etkinliğine benzer şekilde davranır, çünkü o kapsam zincirini oluşturur.

$ Broadcast'in çok mantıklı olduğu senaryolar olduğunu ve $ watch'un daha iyi bir seçenek olduğu senaryolar olduğunu unutmayın - özellikle çok özel bir watch ifadesine sahip ayrı bir kapsamda.


1
$ Digest döngüsünden çıkmak, özellikle izlediğiniz değişiklikler doğrudan ve hemen DOM'a girecek bir değer değilse İyi Bir Şeydir.
XML

.Save () yönteminden kaçınmak için uzakta mı? SharedService'te tek bir değişkenin güncellemesini izlediğinizde aşırı yüklenme gibi görünüyor. Değişkeni sharedService içinden izleyebilir ve değiştiğinde yayınlayabilir miyiz?
JerryKur

Denetleyiciler arasında veri paylaşmanın birkaç yolunu denedim, ancak işe yarayan tek şey bu. İyi oynadın, efendim.
abettermap

Bunu diğer cevaplara tercih ederim, daha az acayip görünüyor , teşekkürler
JMK

9
Bu, yalnızca tüketen denetleyicinizde birden fazla olası veri kaynağı varsa doğru tasarım modelidir; diğer bir deyişle, bir MIMO durumunuz varsa (Çoklu Giriş / Çoklu Çıkış). Yalnızca bire çok çoğaltma deseni kullanıyorsanız, doğrudan nesne referansı kullanmanız ve Açısal çerçevenin sizin için iki yönlü bağlamayı yapmasına izin vermeniz gerekir. Horkyze bunu aşağıya bağladı ve otomatik iki yönlü bağlamanın iyi bir açıklaması ve sınırlamaları: stsc3000.github.io/blog/2013/10/26/…
Charles

47

@ Dtheodot ile benzer bir yaklaşım kullanıyorum, ancak geri aramalar yerine açısal söz kullanıyorum

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

O zaman nerede hizmet myService.setFoo(foo)güncellemek fooiçin yöntemi kullanın . Oyun kumandanızda şu şekilde kullanabilirsiniz:

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

İlk iki argüman thenbaşarı ve hata geri çağrıları, üçüncüsü ise geri çağrı bildirimidir.

$ Q için referans.


Matt Pileggi tarafından açıklanan $ yayın üzerinden bu yöntemin avantajı ne olurdu?
Fabio

Her iki yöntemin de kullanımları vardır. Benim için yayın yapmanın avantajları, insan tarafından okunabilirliği ve aynı etkinliğin daha fazla yerde dinlenebilmesidir. Sanırım ana dezavantajı, yayın tüm torun kapsamlarına mesaj yayıyor olması, bu yüzden bir performans sorunu olabilir.
Krym

2
$scope.$watchBir hizmet değişkeni üzerinde çalışmanın işe yaramadığı bir sorun yaşıyordum (izlediğim kapsam, miras alınan bir moddu $rootScope) - bu işe yaradı. Harika numara, paylaştığın için teşekkürler!
Seiyria

4
Bu yaklaşımla kendinizden sonra nasıl temizlik yaparsınız? Kapsam yok edildiğinde kayıtlı geri aramayı vaatten çıkarmak mümkün müdür?
Abris

İyi soru. Dürüst olmak gerekirse bilmiyorum. Geri bildirimi vaatten nasıl kaldırabileceğiniz konusunda bazı testler yapmaya çalışacağım.
Krym

41

Saatler veya gözlemci geri çağrıları olmadan ( 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>

Bu JavaScript, bir nesneyi değer yerine hizmetten geri aktarırken çalışır. Bir hizmetten bir JavaScript nesnesi döndürüldüğünde, Angular tüm özelliklerine saatler ekler.

Ayrıca, $ zaman aşımı yürütüldüğünde orijinal nesneye bir başvuru tutmak gerektiğinden, 'var self = this' kullanıyorum, aksi takdirde 'this' pencere nesnesine atıfta bulunacağımı unutmayın.


3
Bu harika bir yaklaşım! Bir hizmetin yalnızca bir özelliğini, hizmetin tamamı yerine kapsama bağlamanın bir yolu var mı? Sadece yapmak $scope.count = service.countişe yaramıyor.
jvannistelrooy

Ayrıca özelliği (isteğe bağlı) bir nesnenin içine, başvuru ile iletilecek şekilde yuvalayabilirsiniz. $scope.data = service.data <p>Count: {{ data.count }}</p>
Alex Ross

1
Mükemmel yaklaşım! Bu sayfada çok güçlü ve işlevsel cevaplar olsa da, bu a) uygulaması en kolay ve b) kodu okurken anlaşılması en kolay olanıdır. Bu cevap şu anda olduğundan çok daha yüksek olmalıdır.
CodeMoose

Teşekkürler @CodeMoose, AngularJS / JavaScript'te yeni olanlar için bugün daha da basitleştirdim.
Zymotik

2
Tanrı seni korusun. Milyon saat gibi israf ettim diyebilirim. Çünkü 1.5 ile mücadele ediyordum ve angularjs 1'den 2'ye değişiyordu ve ayrıca veri paylaşmak istedim
Amna

29

Bu soruyu benzer bir şey aramak için tökezledim, ancak neler olduğunu ve bazı ek çözümleri tam olarak açıklamayı hak ettiğini düşünüyorum.

HTML'de kullandığınız gibi bir açısal ifade mevcut olduğunda, Angular otomatik olarak bir $watchfor ayarlar $scope.foove her $scope.foodeğiştiğinde HTML'yi günceller .

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

Buradaki söylenmeyen sorun, iki şeyden birinin aService.foo , değişikliklerin tespit edilmeyeceği şekilde etkilemesidir . Bu iki olasılık:

  1. aService.foo her seferinde yeni bir diziye ayarlanıyor ve referansın eski olmasına neden oluyor.
  2. aService.foogüncellemede bir $digestdöngü tetiklenmeyecek şekilde güncellenmektedir.

Sorun 1: Eski Referanslar

İlk olasılık göz önüne alındığında, a'nın $digestuygulandığı varsayılarak aService.foo, her zaman aynı dizi $watcholsaydı , otomatik ayarlanan , aşağıdaki kod snippet'inde gösterildiği gibi değişiklikleri algılar.

Çözüm 1-a: Dizi veya nesnenin her güncellemede aynı nesne olduğundan emin olun

Gördüğünüz gibi, sözde bağlı ng-repeat aService.foozaman güncelleme yok aService.foodeğişiklikleri, ancak bağlı ng-repeat aService2.foo yapar . Bunun nedeni, referansımızın aService.foomodası geçmiş olmasıdır, ancak referansımız aService2.foodeğildir. İlk diziye bir başvuru oluşturduk $scope.foo = aService.foo;; bu, daha sonra bir sonraki güncellemesinde hizmet tarafından atıldı, yani $scope.fooartık istediğimiz diziye artık başvurulmuyor.

Bununla birlikte, ilk referansın inceliğini koruduğundan emin olmanın birkaç yolu olsa da, bazen nesneyi veya diziyi değiştirmek gerekebilir. Veya service özelliği, a Stringveya gibi bir ilkeye başvuruyorsa ne olur Number? Bu gibi durumlarda, sadece bir referansa güvenemeyiz. Peki ne yapabiliriz ?

Daha önce verilen cevapların birçoğu zaten bu soruna bazı çözümler veriyor. Ancak, ben şahsen Jin ve thetallweeks tarafından yorumlarda önerilen basit yöntemi kullanmaktan yanayım :

html biçimlendirmesinde aService.foo dosyasına başvurmanız yeterlidir

Çözüm 1-b: Hizmeti kapsama ekleyin {service}.{property}ve HTML'ye bakın.

Anlamı, sadece şunu yapın:

HTML:

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

JS:

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

Bu şekilde, her biri üzerinde doğru şekilde güncellenen değeri alacak olan $watchçözülecektir .aService.foo$digest

Bu, geçici çözümünüzle yapmaya çalıştığınız şeydir, ancak çok daha az bir şekilde. $watchDenetleyiciye foo, $scopeher değiştiğinde açıkça atan bir gereksiz eklediniz . O ekstra gerekmez $watchEklemek zaman aServiceyerine aService.fookadar $scopebağlamak açıkça etmek ve aService.fooişaretlemesinde.


Şimdi bir $digestdöngü uygulandığı varsayılarak her şey yolunda ve güzel . Yukarıdaki örneklerimde, her güncellemeden sonra $intervalotomatik olarak bir $digestdöngüyü başlatan dizileri güncellemek için Angular'ın hizmetini kullandım . Peki ya hizmet değişkenleri (herhangi bir nedenle) "Açısal dünya" içinde güncellenmiyorsa. Başka bir deyişle, service özelliği her değiştiğinde otomatik olarak etkinleştirilen bir döngümüz yok$digest mu?


Sorun 2: Eksik $digest

Buradaki çözümlerin çoğu bu sorunu çözecektir, ancak Code Whisperer'a katılıyorum :

Angular gibi bir çerçeve kullanmamızın nedeni, kendi gözlemci kalıplarımızı pişirmemek.

Bu nedenle, aService.fooreferansı yukarıdaki ikinci örnekte gösterildiği gibi HTML biçimlendirmesinde kullanmaya devam etmeyi tercih ediyorum ve Denetleyici içinde ek bir geri arama kaydetmek zorunda değilim.

Çözüm 2: Bir ayarlayıcı ve alıcı kullanın. $rootScope.$apply()

Kimse henüz bir pasör ve alıcı kullanımını önermedi şaşırdı . Bu yetenek ECMAScript5'te tanıtıldı ve bu nedenle yıllardır varlığını sürdürüyor. Tabii ki, bu, hangi nedenle olursa olsun, gerçekten eski tarayıcıları desteklemeniz gerekiyorsa, o zaman bu yöntem işe yaramaz, ancak alıcıların ve ayarlayıcıların JavaScript'te çok az kullanıldığını hissediyorum. Bu özel durumda, oldukça yararlı olabilirler:

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

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

Burada hizmet fonksiyonunda bir 'özel' değişkeni eklendi: realFoo. Bu , nesnede sırasıyla get foo()ve set foo()işlevleri kullanılarak güncellenir ve alınır service.

$rootScope.$apply()Set fonksiyonunda kullanımına dikkat edin. Bu, Angular'ın herhangi bir değişiklikten haberdar olmasını sağlar service.foo. Eğer 'inprog' hataları alıyorsanız, bu faydalı referans sayfasına bakın ya da Açısal> = 1.3 kullanıyorsanız sadece kullanabilirsiniz $rootScope.$applyAsync().

Ayrıca aService.fooçok sık güncelleniyorsa buna dikkat edin , çünkü bu performansı önemli ölçüde etkileyebilir. Performans bir sorun teşkil ediyorsa, ayarlayıcıyı kullanarak diğer yanıtlara benzer bir gözlemci modeli ayarlayabilirsiniz.


3
Bu doğru ve en kolay çözümdür. @NanoWizard'ın dediği gibi, $ digest serviceshizmetin kendisine ait mülkleri izlemez .
Sarpdoruk Tahmaz

28

Anlayabildiğim kadarıyla, bu kadar ayrıntılı bir şey yapmak zorunda değilsiniz. Hizmetten kapsamınıza zaten foo atadınız ve foo bir dizi olduğundan (ve buna karşılık bir nesne referans olarak atanır!). Yani, tek yapmanız gereken şöyle bir şey:

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

 }

Aynı Ctrl'deki bazı değişkenler foo değişikliğine bağlıysa, evet, fooyu gözlemlemek ve bu değişkente değişiklikler yapmak için bir saate ihtiyacınız olacaktır. Ancak basit bir referans olduğu sürece izlemeye gerek yoktur. Bu yardımcı olur umarım.


35
Denedim ve $watchbir ilkel ile çalışamadım. Bunun yerine, ilkel değerini döndürecektir hizmeti bir yöntem tanımlanmıştır: somePrimitive() = function() { return somePrimitive }Ve o yönteme bir $ kapsamı özelliği atanır: $scope.somePrimitive = aService.somePrimitive;. Sonra HTML'de kapsam yöntemini kullandım: <span>{{somePrimitive()}}</span>
Mark Rajcok

4
@MarkRajcok İlkel kullanmayın. Onları bir nesneye ekleyin. Temel öğeler değişken değil ve bu nedenle 2 yönlü veri bağlama işe yaramayacak
Jimmy Kane

3
@JimmyKane, evet, ilkeller 2 yönlü veri bağlama için kullanılmamalıdır, ancak bence soru servis değişkenlerini izlemek, 2 yönlü bağlama kurmak değil. Yalnızca bir service özelliğini / değişkenini izlemeniz gerekiyorsa, bir nesne gerekli değildir - bir ilkel kullanılabilir.
Mark Rajcok

3
Bu kurulumda aService değerlerini kapsamdan değiştirebiliyorum. Ancak, aService değişikliğine yanıt olarak kapsam değişmez.
Ouwen Huang

4
Bu benim için de işe yaramıyor. Basitçe atamak $scope.foo = aService.foo, kapsam değişkenini otomatik olarak güncellemez.
Darwin Tech

9

Hizmeti $ rootScope'a ekleyebilir ve izleyebilirsiniz:

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

Kumandanızda:

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

Diğer seçenek, aşağıdaki gibi harici bir kütüphane kullanmaktır: https://github.com/melanke/Watch.JS

Şununla çalışır: IE 9+, FF 4+, SF 5+, WebKit, CH 7+, OP 12+, BESEN, Node.JS, Rhino 1.7+

Bir, çok veya tüm nesne özelliklerindeki değişiklikleri gözlemleyebilirsiniz.

Misal:

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
BU CEVAPIN EN ÇOK OYNANMIŞ OLMADIĞINA İNANAMIYORUM !!! Bu en zarif çözümdür (IMO) çünkü bilgi entropisini azaltır ve muhtemelen ek Arabuluculuk işleyicilerine olan ihtiyacı azaltır. Eğer yapabilseydim bunu daha çok
Cody

Tüm hizmetlerinizi $ rootScope'a eklemek, faydaları ve potansiyel tuzakları burada biraz ayrıntılı olarak verilmiştir: stackoverflow.com/questions/14573023/…
Zymotik

6

Fabrika içindeki değişiklikleri izleyebilir ve ardından bir değişiklik yayınlayabilirsiniz

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

Sonra kumandanızda:

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

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

Bu şekilde, ilgili tüm fabrika kodunu açıklamasına koyarsanız, yayına yalnızca dışarıdan güvenebilirsiniz


6

== GÜNCELLEME ==

Çok basit şimdi $ izle.

Burada kalem .

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
PostmanService gibi ama birden fazla değişken dinlemek gerekirse nasıl denetleyicide $ watch işlevini değiştirmek gerekir?
jedi

Merhaba jedi, dikkatleri için teşekkürler! Kalemi ve cevabı güncelledim. Bunun için başka bir saat fonksiyonu eklemeyi tavsiye ederim. Bu yüzden, PostmanService'e yeni bir özellik ekledim. Umarım bu yardımcı olur :)
hayatbiralem

Aslında, evet :) Eğer sorunun daha fazla detayını paylaşırsanız belki size yardımcı olabilirim.
hayatbiralem

4

Dtheodor'un cevabına dayanarak , geri aramanın kaydını silmeyi unutmadığınızdan emin olmak için aşağıdakine benzer bir şey kullanabilirsiniz ... Bazıları $scopebir hizmete geçmeye itiraz edebilir .

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, şuna benzer bir uzantı yöntemidir:

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

İşte benim genel yaklaşımım.

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

    }]);

Kumandanızda

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

Benim gibi sadece basit bir çözüm arayanlar için, bu kontrolörlerde normal $ watch kullanmaktan neredeyse beklediğiniz şeyi yapar. Tek fark, dizeyi belirli bir kapsamda değil, javascript bağlamında değerlendirmesidir. Hizmetinize yalnızca $ rootScope enjekte etmeniz gerekecek, ancak yalnızca sindirim döngülerine düzgün şekilde bağlanmak için kullanılıyor.

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

2

çok benzer bir sorunla karşı karşıya kalırken kapsamdaki bir fonksiyonu izledim ve fonksiyonun servis değişkenini döndürmesini sağladım. Bir js keman yarattım . kodu aşağıda bulabilirsiniz.

    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

Bu soruya geldim ama benim sorunum ortaya çıktı açısal $ aralık sağlayıcı kullanmalıyım setInterval kullanmak oldu. Bu setTimeout için de geçerlidir (bunun yerine $ timeout kullanın). OP'nin sorusunun cevabı olmadığını biliyorum, ama bana yardımcı olduğu için bazılarına yardımcı olabilir.


Sen kullanabilirsiniz setTimeout, ya da başka olmayan Eğik fonksiyonu, ama sadece birlikte geri aramasında kod tamamlamayı unutmayın $scope.$apply().
magnetronnie

2

Ben benzer bir sorun ama tamamen farklı bir yaklaşım ile diğer iş parçacığı üzerinde gerçekten harika bir çözüm bulduk. Kaynak: AngularJS: $ rootScope değeri değiştirildiğinde yönerge içindeki $ watch çalışmıyor

Temelde çözüm var söyler İÇİN DEĞİL kullanmak $watchçok ağır bir çözümdür olarak. Bunun yerine onlar kullanmayı teklif $emitve $on.

Benim sorunum hizmetimdeki bir değişkeni izlemek ve direktifte tepki göstermekti . Ve yukarıdaki yöntemle çok kolay!

Modül / servis örneğim:

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

Böylece temelde izlemek gözlerimi user - bu yeni değere ben ayarlandığında her $emitbir user:changedurum.

Şimdi benim durumumda, direktifte kullandım:

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

Şimdi de direktif ben dinlemek $rootScopeve üzerinde verilen değişiklik - Ben sırasıyla tepki. Çok kolay ve zarif!


1

// hizmet: (burada özel bir şey yok)

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

1

Çirkin bir çirkin, ancak kapsam değişkenlerinin hizmetime bir geçiş için kaydını ekledim:

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

Ve sonra denetleyicide:

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

Teşekkürler. Küçük bir soru: neden thisbu hizmetten geri dönüyorsunuz self?
shrekuu

4
Çünkü bazen hatalar yapılır. ;-)
nclu

İyi uygulama return this;
Cody

1

Bu dalgıç bir göz atın :: bu düşünebildiğim en basit örnek

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

Burada, büyük uygulamalarda bellek sızıntılarına neden olan bazı korkunç gözlemci kalıpları gördüm.

Biraz geç kalabilirim ama bu kadar basit.

Eğer dizi itme gibi bir şeyi izlemek istiyorsanız watch fonksiyonu referans değişiklikleri (ilkel tipler) izler:

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

Bu referansı güncelleyecek ve saati her yerden güncelleyecektir. Bir hizmet alıcısı yöntemi dahil. İlkel olan her şey otomatik olarak güncellenecektir.


0

Bölüme geç kaldım ama bunu yapmak için yukarıda verilen cevaptan daha güzel bir yol buldum. Hizmet değişkeninin değerini tutmak için bir değişken atamak yerine, kapsama bağlı, hizmet değişkenini döndüren bir işlev oluşturdum.

kontrolör

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

Bence bu istediğini yapacak. Denetleyicim bu uygulamayla hizmetimin değerini kontrol etmeye devam ediyor. Dürüst olmak gerekirse, bu seçilen cevaptan çok daha basittir.


neden reddedildi .. Birçok kez benzer bir teknik kullandım ve işe yaradı.
undefined

0

Hizmet özellikleri değişikliklerini izlememe yardımcı olan iki basit yardımcı hizmet yazdım.

Uzun açıklamayı atlamak istiyorsanız, jsfiddle'a gidebilirsiniz.

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

Bu hizmet denetleyicide şu şekilde kullanılır: 'prop1', 'prop2', 'prop3' özelliklerine sahip bir "testService" hizmetiniz olduğunu varsayalım. İzlemek ve 'prop1' ve 'prop2' kapsamına atamak istiyorsunuz. Saat servisi ile şöyle görünecektir:

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. Uygula Watch obj mükemmeldir, ancak hizmetinizde eşzamansız kod varsa yeterli değildir. Bu durumda, şöyle görünen ikinci bir yardımcı program kullanıyorum:

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

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

$ Digest döngüsünü tetiklemek için zaman uyumsuz kodumun sonunda tetikleyeceğim. Bunun gibi:

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

Yani, hepsi birlikte böyle görünecek (çalıştırabilir veya keman açabilirsiniz ):

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

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.