AngularJS: Bagaimana cara menonton variabel layanan?

414

Saya punya layanan, katakan:

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

  return service;
}]);

Dan saya ingin menggunakan foountuk mengontrol daftar yang dirender dalam HTML:

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

Agar controller dapat mendeteksi kapan aService.foodiperbarui, saya telah menggabungkan pola ini di mana saya menambahkan aService ke controller $scopedan kemudian gunakan $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;
    }
  });
}

Ini terasa sulit, dan saya telah mengulanginya di setiap pengontrol yang menggunakan variabel layanan. Apakah ada cara yang lebih baik untuk menyelesaikan menonton variabel yang dibagikan?

berto
sumber
1
Anda dapat melewatkan parameter ketiga ke $ watch disetel ke true ke deep watch aService dan semua propertinya.
SirTophamHatt
7
$ scope.foo = aService.foo sudah cukup, Anda bisa kehilangan baris di atas. Dan apa yang dilakukannya di dalam $ watch tidak masuk akal, jika Anda ingin memberikan nilai baru ke $ scope.foo lakukan saja ...
Jin
4
Bisakah Anda referensi aService.foodi markup html? (seperti ini: plnkr.co/edit/aNrw5Wo4Q0IxR2loipl5?p=preview )
thetallweeks
1
Saya telah menambahkan contoh tanpa Callbacks atau $ watches, lihat jawaban di bawah ( jsfiddle.net/zymotik/853wvv7s )
Zymotik
1
@ MikeGledhill, Anda benar. Saya pikir itu karena sifat Javascript, Anda dapat melihat pola ini di banyak tempat lain (tidak hanya di Angular, tetapi di JS secara umum). Di satu sisi, Anda mentransfer nilai (dan tidak terikat) dan di sisi lain Anda mentransfer objek (atau nilai yang mereferensikan objek ...), dan itulah sebabnya properti diperbarui dengan benar (seperti sempurna ditunjukkan dalam contoh Zymotik di atas).
Christophe Vidal

Jawaban:

277

Anda selalu dapat menggunakan pola pengamat lama yang baik jika Anda ingin menghindari tirani dan overhead $watch.

Dalam layanan:

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

Dan di controller:

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

  aService.registerObserverCallback(updateFoo);
  //service now in control of updating foo
};
dekorator
sumber
21
@Moo dengarkan $destoryacara pada lingkup dan tambahkan metode membatalkan pendaftaran keaService
Jamie
13
Apa kelebihan dari solusi ini? Dibutuhkan lebih banyak kode dalam suatu layanan, dan jumlah kode yang agak sama dalam sebuah kontroler (karena kita juga perlu membatalkan registrasi pada $ destroy). Saya bisa mengatakan untuk kecepatan eksekusi, tetapi dalam kebanyakan kasus itu tidak masalah.
Alex Che
6
tidak yakin bagaimana ini adalah solusi yang lebih baik daripada $ watch, si penanya menanyakan cara sederhana berbagi data, itu terlihat lebih rumit. Saya lebih suka menggunakan $ broadcast daripada ini
Jin
11
$watchvs pengamat pola hanya memilih apakah akan polling atau mendorong, dan pada dasarnya adalah masalah kinerja, jadi gunakan itu ketika kinerja penting. Saya menggunakan pola pengamat ketika kalau tidak saya harus "mendalam" menonton benda-benda kompleks. Saya melampirkan seluruh layanan ke $ scope daripada menonton nilai layanan tunggal. Saya menghindari $ arloji sudut seperti iblis, ada cukup banyak yang terjadi dalam arahan dan dalam pengikatan data sudut asli.
dtheodor
107
Alasan mengapa kita menggunakan kerangka kerja seperti Angular adalah untuk tidak memasak pola pengamat kita sendiri.
Code Whisperer
230

Dalam skenario seperti ini, di mana beberapa objek / tidak dikenal mungkin tertarik pada perubahan, gunakan $rootScope.$broadcastdari item yang diubah.

Daripada membuat registri pendengar Anda sendiri (yang harus dibersihkan pada berbagai $ Destroys), Anda harus dapat $broadcastdari layanan yang bersangkutan.

Anda masih harus memberi kode pada $onpenangan di setiap pendengar tetapi polanya dipisahkan dari beberapa panggilan ke $digestdan dengan demikian menghindari risiko pengamat berjalan lama.

Dengan cara ini, pendengar dapat datang dan pergi dari DOM dan / atau lingkup anak yang berbeda tanpa layanan mengubah perilakunya.

** pembaruan: contoh **

Siaran akan paling masuk akal dalam layanan "global" yang dapat memengaruhi banyak hal lain di aplikasi Anda. Contoh yang baik adalah layanan Pengguna di mana ada sejumlah peristiwa yang dapat terjadi seperti masuk, keluar, diperbarui, menganggur, dll. Saya percaya ini adalah tempat siaran paling masuk akal karena ruang lingkup apa pun dapat mendengarkan suatu acara, tanpa bahkan menyuntikkan layanan, dan tidak perlu mengevaluasi ekspresi atau hasil cache apa pun untuk memeriksa perubahan. Itu hanya api dan lupa (jadi pastikan itu pemberitahuan api-dan-lupakan, bukan sesuatu yang membutuhkan tindakan)

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

Layanan di atas akan menyiarkan pesan ke setiap lingkup ketika fungsi save () selesai dan data valid. Atau, jika itu adalah $ resource atau submisi ajax, pindahkan panggilan broadcast ke dalam callback sehingga menyala ketika server merespons. Siaran sesuai dengan pola itu terutama karena setiap pendengar hanya menunggu acara tanpa perlu memeriksa ruang lingkup pada setiap $ digest tunggal. Pendengar akan terlihat seperti:

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

 }]);

Salah satu manfaat dari ini adalah penghapusan beberapa jam tangan. Jika Anda menggabungkan bidang atau memperoleh nilai seperti contoh di atas, Anda harus menonton properti firstname dan lastname. Menonton fungsi getUser () hanya akan berfungsi jika objek pengguna diganti pada pembaruan, itu tidak akan menyala jika objek pengguna hanya memiliki sifat-sifatnya diperbarui. Dalam hal ini Anda harus melakukan pengamatan mendalam dan itu lebih intensif.

$ broadcast mengirimkan pesan dari lingkup yang dipanggil ke lingkup anak apa pun. Jadi memanggilnya dari $ rootScope akan menyala di setiap ruang lingkup. Jika Anda $ broadcast dari ruang lingkup controller Anda, misalnya, itu akan menembak hanya di lingkup yang mewarisi dari lingkup controller Anda. $ emit berjalan ke arah yang berlawanan dan berperilaku serupa dengan peristiwa DOM dalam hal itu menggembungkan rantai lingkup.

Perlu diingat bahwa ada skenario di mana $ broadcast masuk akal, dan ada skenario di mana $ watch adalah pilihan yang lebih baik - terutama jika dalam ruang lingkup yang terisolasi dengan ekspresi menonton yang sangat spesifik.

Matt Pileggi
sumber
1
Keluar dari siklus $ digest adalah Good Thing, terutama jika perubahan yang Anda tonton bukan nilai yang akan langsung dan langsung masuk ke DOM.
XML
Apakah ada jauh untuk menghindari metode .save (). Sepertinya berlebihan jika Anda hanya memantau pembaruan satu variabel di sharedService. Bisakah kita menonton variabel dari dalam sharedService dan disiarkan ketika itu berubah?
JerryKur
Saya mencoba beberapa cara untuk berbagi data antar pengendali, tetapi ini adalah satu-satunya yang berfungsi. Bermain bagus, tuan.
abettermap
Saya lebih suka ini daripada jawaban lain, tampaknya kurang hacky , terima kasih
JMK
9
Ini adalah pola desain yang benar hanya jika pengontrol Anda memiliki banyak sumber data yang memungkinkan; dengan kata lain, jika Anda memiliki situasi MIMO (Multiple Input / Multiple Output). Jika Anda hanya menggunakan pola satu-ke-banyak, Anda harus menggunakan referensi objek langsung dan membiarkan kerangka Angular melakukan ikatan dua arah untuk Anda. Horkyze menautkan ini di bawah, dan ini adalah penjelasan yang bagus tentang pengikatan dua arah otomatis, dan batasannya: stsc3000.github.io/blog/2013/10/26/…
Charles
47

Saya menggunakan pendekatan yang sama seperti @dtheodot tetapi menggunakan janji sudut alih-alih melewati panggilan balik

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

Lalu di mana saja gunakan myService.setFoo(foo)metode untuk memperbarui foolayanan. Di controller Anda, Anda dapat menggunakannya sebagai:

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

Dua argumen pertama thenadalah keberhasilan dan kesalahan panggilan balik, yang ketiga adalah memberitahukan panggilan balik.

Referensi untuk $ q.

Krym
sumber
Apa keuntungan metode ini dibandingkan $ broadcast yang dijelaskan di bawah oleh Matt Pileggi?
Fabio
Yah kedua metode memiliki kegunaannya. Keuntungan siaran bagi saya adalah keterbacaan manusia dan kemungkinan untuk mendengarkan lebih banyak tempat di acara yang sama. Saya kira kerugian utama adalah bahwa siaran memancarkan pesan ke semua lingkup turunan sehingga mungkin menjadi masalah kinerja.
Krym
2
Saya mengalami masalah di mana melakukan $scope.$watchpada variabel layanan sepertinya tidak berfungsi (ruang lingkup yang saya tonton adalah modal yang diwarisi dari $rootScope) - ini berhasil. Trik keren, terima kasih sudah berbagi!
Seiyria
4
Bagaimana Anda membersihkan diri sendiri dengan pendekatan ini? Apakah mungkin untuk menghapus callback terdaftar dari janji ketika ruang lingkup dihancurkan?
Abris
Pertanyaan bagus. Sejujurnya aku tidak tahu. Saya akan mencoba melakukan beberapa tes tentang bagaimana Anda bisa menghapus pemberitahuan panggilan balik dari janji.
Krym
41

Tanpa arloji atau panggilan balik pengamat ( 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>

JavaScript ini berfungsi saat kami melewati objek kembali dari layanan daripada nilai. Saat objek JavaScript dikembalikan dari layanan, Angular menambahkan jam tangan ke semua propertinya.

Perhatikan juga bahwa saya menggunakan 'var self = this' karena saya perlu menyimpan referensi ke objek asli ketika $ timeout dijalankan, jika tidak 'ini' akan merujuk ke objek jendela.

Zymotik
sumber
3
Ini pendekatan hebat! Apakah ada cara untuk hanya mengikat properti layanan ke ruang lingkup daripada seluruh layanan? Melakukan sesuatu $scope.count = service.counttidak akan berhasil.
jvannistelrooy
Anda juga bisa membuat sarang properti di dalam objek (sewenang-wenang) sehingga dilewatkan dengan referensi. $scope.data = service.data <p>Count: {{ data.count }}</p>
Alex Ross
1
Pendekatan luar biasa! Walaupun ada banyak jawaban fungsional dan kuat pada halaman ini, ini sejauh ini a) yang paling mudah untuk diterapkan, dan b) yang paling mudah untuk dipahami ketika membaca kode. Jawaban ini harus jauh lebih tinggi daripada saat ini.
CodeMoose
Terima kasih @CodeMoose, saya telah menyederhanakannya lebih jauh hari ini untuk mereka yang baru menggunakan AngularJS / JavaScript.
Zymotik
2
Semoga Tuhan memberkatimu. Saya akan menyia-nyiakan jutaan jam yang akan saya katakan. Karena saya berjuang dengan 1,5 dan angular bergeser dari 1 ke 2 dan juga ingin berbagi data
Amna
29

Saya menemukan pertanyaan ini mencari sesuatu yang serupa, tetapi saya pikir itu layak mendapatkan penjelasan menyeluruh tentang apa yang terjadi, serta beberapa solusi tambahan.

Ketika ekspresi sudut seperti yang Anda gunakan ada dalam HTML, Angular secara otomatis menyiapkan a $watchuntuk $scope.foo, dan akan memperbarui HTML setiap kali ada $scope.fooperubahan.

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

Masalah yang tidak terungkap di sini adalah bahwa satu dari dua hal mempengaruhi aService.foo sehingga perubahan tidak terdeteksi. Dua kemungkinan ini adalah:

  1. aService.foo semakin diatur ke array baru setiap kali, menyebabkan referensi untuk itu menjadi usang.
  2. aService.foosedang diperbarui sedemikian rupa sehingga $digestsiklus tidak dipicu pada pembaruan.

Masalah 1: Referensi yang sudah ketinggalan zaman

Mempertimbangkan kemungkinan pertama, dengan asumsi a $digestsedang diterapkan, jika aService.fooselalu array yang sama, set otomatis $watchakan mendeteksi perubahan, seperti yang ditunjukkan dalam cuplikan kode di bawah ini.

Solusi 1-a: Pastikan array atau objek adalah objek yang sama pada setiap pembaruan

Seperti yang Anda lihat, ng-repeat yang seharusnya dilampirkan aService.footidak memperbarui ketika aService.fooperubahan, tetapi ng-repeat yang dilampirkan aService2.foo tidak . Ini karena referensi kami kepada aService.foosudah usang, tetapi referensi kami untuk aService2.footidak. Kami membuat referensi ke array awal dengan $scope.foo = aService.foo;, yang kemudian dibuang oleh layanan pada pembaruan berikutnya, yang berarti $scope.footidak lagi mereferensikan array yang kami inginkan lagi.

Namun, sementara ada beberapa cara untuk memastikan referensi awal disimpan dalam kebijaksanaan, kadang-kadang mungkin perlu untuk mengubah objek atau array. Atau bagaimana jika properti layanan merujuk ke primitif seperti Stringatau Number? Dalam kasus itu, kita tidak bisa hanya mengandalkan referensi. Jadi apa yang bisa kita lakukan?

Beberapa jawaban yang diberikan sebelumnya sudah memberikan beberapa solusi untuk masalah itu. Namun, secara pribadi saya mendukung penggunaan metode sederhana yang disarankan oleh Jin dan semuanya dalam komentar:

cukup referensi aService.foo di markup html

Solusi 1-b: Pasang layanan ke lingkup, dan referensi {service}.{property}dalam HTML.

Artinya, lakukan saja ini:

HTML:

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

JS:

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

Dengan begitu, $watchakan menyelesaikan aService.foomasing-masing $digest, yang akan mendapatkan nilai yang diperbarui dengan benar.

Ini adalah jenis apa yang Anda coba lakukan dengan solusi Anda, tetapi dalam cara yang jauh lebih sedikit. Anda menambahkan yang tidak perlu $watchdi controller yang secara eksplisit menempatkan foopada $scopesetiap kali perubahan. Anda tidak perlu ekstra $watchsaat melampirkan aServicedaripada aService.fooke $scope, dan mengikat secara eksplisit ke aService.foodalam markup.


Nah, itu semua baik dan bagus dengan asumsi $digestsiklus sedang diterapkan. Dalam contoh saya di atas, saya menggunakan layanan Angular $intervaluntuk memperbarui array, yang secara otomatis memulai $digestloop setelah setiap pembaruan. Tetapi bagaimana jika variabel layanan (untuk alasan apa pun) tidak diperbarui di "dunia sudut". Dengan kata lain, kami tidak memiliki $digestsiklus yang diaktifkan secara otomatis setiap kali properti layanan berubah?


Masalah 2: Hilang $digest

Banyak solusi di sini akan menyelesaikan masalah ini, tetapi saya setuju dengan Code Whisperer :

Alasan mengapa kita menggunakan kerangka kerja seperti Angular adalah untuk tidak memasak pola pengamat kita sendiri

Oleh karena itu, saya lebih suka untuk terus menggunakan aService.fooreferensi dalam markup HTML seperti yang ditunjukkan pada contoh kedua di atas, dan tidak harus mendaftarkan panggilan balik tambahan dalam Controller.

Solusi 2: Gunakan setter dan pengambil $rootScope.$apply()

Saya terkejut belum ada yang menyarankan penggunaan setter dan pengambil . Kemampuan ini diperkenalkan di ECMAScript5, dan telah ada selama bertahun-tahun sekarang. Tentu saja, itu berarti jika, untuk alasan apa pun, Anda perlu mendukung browser yang sangat lama, maka metode ini tidak akan berfungsi, tetapi saya merasa seperti getter dan setter sangat kurang digunakan dalam JavaScript. Dalam kasus khusus ini, mereka bisa sangat berguna:

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

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

Di sini saya menambahkan variabel 'pribadi' dalam fungsi pelayanan: realFoo. Get ini diperbarui dan diambil menggunakan get foo()dan set foo()fungsi masing-masing pada serviceobjek.

Perhatikan penggunaan $rootScope.$apply()dalam fungsi yang ditetapkan. Ini memastikan bahwa Angular akan mengetahui adanya perubahan service.foo. Jika Anda mendapatkan kesalahan 'inprog' lihat halaman referensi yang berguna ini , atau jika Anda menggunakan Angular> = 1.3 Anda bisa menggunakan $rootScope.$applyAsync().

Berhati-hatilah dengan hal ini jika aService.foodiperbarui dengan sangat sering, karena hal itu dapat berdampak signifikan terhadap kinerja. Jika kinerja akan menjadi masalah, Anda bisa mengatur pola pengamat yang mirip dengan jawaban lain di sini menggunakan setter.

NanoWizard
sumber
3
Ini adalah solusi yang benar dan termudah. Seperti yang dikatakan @NanoWizard, $ digest jam tangan servicesbukan untuk properti milik layanan itu sendiri.
Sarpdoruk Tahmaz
28

Sejauh yang saya tahu, Anda tidak perlu melakukan sesuatu yang rumit seperti itu. Anda telah menetapkan foo dari layanan ke ruang lingkup Anda dan karena foo adalah array (dan pada gilirannya objek itu ditetapkan oleh referensi!). Jadi, yang perlu Anda lakukan adalah sesuatu seperti ini:

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

 }

Jika beberapa, variabel lain dalam Ctrl yang sama ini bergantung pada perubahan foo maka ya, Anda akan memerlukan arloji untuk mengamati foo dan membuat perubahan pada variabel itu. Tetapi selama itu hanya menonton referensi sederhana tidak perlu. Semoga ini membantu.

Ganaraj
sumber
35
Saya mencoba, dan saya tidak bisa $watchbekerja dengan primitif. Sebaliknya, saya mendefinisikan sebuah metode pada layanan yang akan mengembalikan nilai primitif: somePrimitive() = function() { return somePrimitive }Dan saya ditugaskan properti $ lingkup untuk metode yang: $scope.somePrimitive = aService.somePrimitive;. Lalu saya menggunakan metode lingkup dalam HTML: <span>{{somePrimitive()}}</span>
Mark Rajcok
4
@MarkRajcok. Jangan gunakan primitif. Tambahkan mereka di objek. Primitif bukan bisa berubah-ubah dan pengikatan data 2way tidak akan berfungsi
Jimmy Kane
3
@ JimmyKane, ya, primitif tidak boleh digunakan untuk penyatuan data 2 arah, tapi saya pikir pertanyaannya adalah tentang menonton variabel layanan, tidak menyiapkan ikatan 2 arah. Jika Anda hanya perlu menonton properti layanan / variabel, objek tidak diperlukan - primitif dapat digunakan.
Mark Rajcok
3
Dalam pengaturan ini saya dapat mengubah nilai Layanan dari ruang lingkup. Tetapi ruang lingkup tidak berubah dalam menanggapi perubahan Layanan.
Ouwen Huang
4
Ini juga tidak berfungsi untuk saya. Cukup menetapkan $scope.foo = aService.footidak memperbarui variabel ruang lingkup secara otomatis.
Darwin Tech
9

Anda dapat memasukkan layanan dalam $ rootScope dan menonton:

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

Di controller Anda:

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

Pilihan lain adalah menggunakan perpustakaan eksternal seperti: https://github.com/melanke/Watch.JS

Bekerja dengan: IE 9+, FF 4+, SF 5+, WebKit, CH 7+, OP 12+, BESEN, Node.JS, Badak 1.7+

Anda dapat mengamati perubahan satu, banyak atau semua atribut objek.

Contoh:

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
sumber
2
AKU TIDAK BISA PERCAYA JAWABAN INI BUKAN TOP-PALING-SUARA !!! Ini adalah solusi paling elegan (IMO) karena mengurangi entropi informasi & mungkin mengurangi kebutuhan akan penangan Mediasi tambahan. Saya akan memilih ini lebih banyak jika saya bisa ...
Cody
Menambahkan semua layanan Anda ke $ rootScope, manfaatnya dan potensi jebakannya, dirinci agak di sini: stackoverflow.com/questions/14573023/…
Zymotik
6

Anda dapat menonton perubahan di dalam pabrik itu sendiri dan kemudian menyiarkan perubahan

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

Kemudian di controller Anda:

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

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

Dengan cara ini Anda memasukkan semua kode pabrik terkait ke dalam uraiannya maka Anda hanya dapat mengandalkan siaran dari luar

Flavien Volken
sumber
6

== DIPERBARUI ==

Sangat sederhana sekarang di $ watch.

Pena di sini .

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
sumber
1
saya suka PostmanService tetapi bagaimana saya harus mengubah fungsi $ watch pada controller jika saya perlu mendengarkan lebih dari satu variabel?
jedi
Hai jedi, terima kasih untuk kepala! Saya memperbarui pena dan jawabannya. Saya sarankan untuk menambahkan fungsi arloji lain untuk itu. Jadi, saya menambahkan fitur baru ke PostmanService. Saya harap ini membantu :)
hayatbiralem
Sebenarnya ya itu :) Jika Anda berbagi lebih detail dari masalah mungkin saya dapat membantu Anda.
hayatbiralem
4

Membangun berdasarkan jawaban dtheodor Anda dapat menggunakan sesuatu yang mirip dengan di bawah ini untuk memastikan bahwa Anda tidak lupa membatalkan pendaftaran callback ... Namun, beberapa orang mungkin keberatan untuk meneruskannya $scopeke layanan.

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 adalah metode ekstensi yang terlihat seperti ini:

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

Inilah pendekatan generik saya.

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

    }]);

Di Kontroler Anda

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
sumber
2

Bagi mereka seperti saya hanya mencari solusi sederhana, ini tidak persis seperti yang Anda harapkan dari menggunakan $ menonton normal di pengontrol. Satu-satunya perbedaan adalah, ia mengevaluasi string dalam konteks javascriptnya dan bukan pada lingkup tertentu. Anda harus menyuntikkan $ rootScope ke layanan Anda, meskipun itu hanya digunakan untuk menghubungkan ke siklus digest dengan benar.

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

saat menghadapi masalah yang sangat mirip saya melihat fungsi dalam lingkup dan fungsi mengembalikan variabel layanan. Saya telah membuat biola . Anda dapat menemukan kode di bawah ini.

    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++;
    });
});
lampu gantung
sumber
2

Saya sampai pada pertanyaan ini tetapi ternyata masalah saya adalah bahwa saya menggunakan setInterval ketika seharusnya saya menggunakan penyedia $ interval angular. Ini juga merupakan kasus untuk setTimeout (gunakan $ timeout). Saya tahu itu bukan jawaban untuk pertanyaan OP, tetapi mungkin membantu beberapa, karena membantu saya.

klakson
sumber
Anda dapat menggunakan setTimeout, atau fungsi non-sudut lainnya, tetapi jangan lupa untuk membungkus kode dengan panggilan balik $scope.$apply().
magnetronnie
2

Saya telah menemukan solusi yang sangat bagus di utas lainnya dengan masalah yang sama tetapi pendekatan yang sama sekali berbeda. Sumber: AngularJS: $ watch dalam direktif tidak berfungsi ketika nilai $ rootScope diubah

Pada dasarnya solusi yang ada TIDAK memberitahu untuk digunakan $watchkarena ini adalah solusi yang sangat berat. Sebaliknya mereka mengusulkan untuk menggunakan $emitdan $on.

Masalah saya adalah untuk menonton variabel dalam layanan saya dan bereaksi dalam arahan . Dan dengan metode di atas sangat mudah!

Modul / layanan saya contoh:

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

Jadi pada dasarnya saya menonton saya user - setiap kali diatur untuk nilai baru saya $emitsebuah user:changestatus.

Sekarang dalam kasus saya, dalam arahan yang saya gunakan:

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

Sekarang di direktif saya mendengarkan pada $rootScopedan di perubahan yang diberikan - saya bereaksi masing-masing. Sangat mudah dan elegan!

Atais
sumber
1

// layanan: (tidak ada yang istimewa di sini)

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
sumber
1

Agak jelek, tapi saya sudah menambahkan pendaftaran variabel lingkup ke layanan saya untuk beralih:

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

Dan kemudian di controller:

function MyCtrl($scope, myService) {
    $scope.name = 'Superhero';
    $scope.myVar = false;
    myService.register($scope, 'myVar');
}
nclu
sumber
Terima kasih. Sebuah pertanyaan kecil: mengapa Anda kembali thisdari layanan itu alih-alih self?
shrekuu
4
Karena terkadang kesalahan dibuat. ;-)
nclu
Latihan yang bagus untuk return this;keluar dari konstruktor Anda ;-)
Cody
1

Lihatlah plunker ini :: ini adalah contoh paling sederhana yang bisa saya pikirkan

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
sumber
0

Saya telah melihat beberapa pola pengamat yang mengerikan di sini yang menyebabkan kebocoran memori pada aplikasi besar.

Saya mungkin sedikit terlambat tetapi sesederhana ini.

Fungsi arloji mengawasi perubahan referensi (tipe primitif) jika Anda ingin menonton sesuatu seperti push array cukup gunakan:

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

Ini akan memperbarui referensi dan memperbarui arloji dari mana saja. Termasuk metode pengambil layanan. Apa pun yang primitif akan diperbarui secara otomatis.

Oliver Dixon
sumber
0

Saya terlambat ke bagian itu tetapi saya menemukan cara yang lebih baik untuk melakukan ini daripada jawaban yang diposting di atas. Alih-alih menugaskan variabel untuk menyimpan nilai variabel layanan, saya membuat fungsi yang dilampirkan ke lingkup, yang mengembalikan variabel layanan.

pengontrol

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

Saya pikir ini akan melakukan apa yang Anda inginkan. Pengontrol saya terus memeriksa nilai layanan saya dengan implementasi ini. Jujur, ini jauh lebih sederhana daripada jawaban yang dipilih.

alaboudi
sumber
mengapa itu downvoted .. Saya juga telah menggunakan teknik serupa berkali-kali dan itu berhasil.
undefined
0

Saya telah menulis dua layanan utilitas sederhana yang membantu saya melacak perubahan properti layanan.

Jika Anda ingin melewatkan penjelasan panjang, Anda bisa pergi ke 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;
  };
}

Layanan ini digunakan dalam controller dengan cara berikut: Misalkan Anda memiliki layanan "testService" dengan properti 'prop1', 'prop2', 'prop3'. Anda ingin menonton dan menetapkan ruang lingkup 'prop1' dan 'prop2'. Dengan layanan jam tangan akan terlihat seperti itu:

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 bagus, tetapi tidak cukup jika Anda memiliki kode asinkron dalam layanan Anda. Untuk itu, saya menggunakan utilitas kedua yang terlihat seperti itu:

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

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

Saya akan memicunya di akhir kode async saya untuk memicu loop $ digest. Seperti itu:

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

Jadi, semua itu bersama akan terlihat seperti itu (Anda dapat menjalankannya atau membuka biola ):

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

Maks
sumber