AngularJS: Làm thế nào để xem các biến dịch vụ?


414

Tôi có một dịch vụ, nói:

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

  return service;
}]);

Và tôi muốn sử dụng foo để kiểm soát một danh sách được hiển thị trong HTML:

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

Để bộ điều khiển phát hiện khi nào aService.foođược cập nhật, tôi đã ghép lại mẫu này với nhau, nơi tôi thêm aService vào bộ điều khiển $scopevà sau đó sử dụng $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;
    }
  });
}

Điều này cảm thấy thuận tay và tôi đã lặp lại nó trong mọi bộ điều khiển sử dụng các biến của dịch vụ. Có cách nào tốt hơn để thực hiện việc xem các biến được chia sẻ không?


1
Bạn có thể chuyển tham số thứ ba cho $ watch được đặt thành true để xem sâu aService và tất cả các thuộc tính của nó.
SirTophamHatt

7
$ scope.foo = aService.foo là đủ, bạn có thể mất dòng trên. Và những gì nó làm trong $ watch không có ý nghĩa gì, nếu bạn muốn gán một giá trị mới cho $ scope.foo chỉ cần làm điều đó ...
Jin

4
Bạn chỉ có thể tham khảo aService.footrong đánh dấu html? (như thế này: plnkr.co/edit/aNrw5Wo4Q0IxR2loipl5?p=preview )
thetallweek

1
Tôi đã thêm một ví dụ không có Callbacks hoặc $ đồng hồ, xem câu trả lời bên dưới ( jsfiddle.net/zymotik/853wvv7s )
Zymotik

1
@MikeGledhill, bạn nói đúng. Tôi nghĩ đó là do bản chất của Javascript, bạn có thể thấy mô hình này ở nhiều nơi khác (không chỉ ở Angular, mà nói chung là nói về JS). Một mặt, bạn chuyển giá trị (và nó không bị ràng buộc) và mặt khác, bạn chuyển một đối tượng (hoặc giá trị tham chiếu đến đối tượng ...) và đó là lý do tại sao các thuộc tính được cập nhật chính xác (như hoàn hảo thể hiện trong ví dụ về Zymotik ở trên).
Barshe Vidal

Câu trả lời:


277

Bạn luôn có thể sử dụng mô hình quan sát cũ tốt nếu bạn muốn tránh sự chuyên chế và chi phí $watch.

Trong dịch vụ:

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

Và trong bộ điều khiển:

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

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

21
@Moo lắng nghe $destorysự kiện trên phạm vi và thêm một phương thức chưa đăng ký vàoaService
Jamie

13
Ưu điểm của giải pháp này là gì? Nó cần nhiều mã hơn trong một dịch vụ và phần nào cùng số lượng mã trong bộ điều khiển (vì chúng ta cũng cần hủy đăng ký $ hủy). Tôi có thể nói về tốc độ thực hiện, nhưng trong hầu hết các trường hợp, điều đó không thành vấn đề.
Alex Che

6
không chắc chắn làm thế nào đây là một giải pháp tốt hơn so với $ watch, người hỏi đã yêu cầu một cách đơn giản để chia sẻ dữ liệu, nó trông thậm chí còn cồng kềnh hơn. Tôi thà sử dụng $ Broadcast hơn thế này
Jin

11
$watchMô hình quan sát chỉ đơn giản là chọn lựa chọn để thăm dò ý kiến ​​hay thúc đẩy, và về cơ bản là vấn đề hiệu suất, vì vậy hãy sử dụng nó khi hiệu suất quan trọng. Tôi sử dụng mẫu quan sát khi không tôi sẽ phải "xem sâu" các vật thể phức tạp. Tôi đính kèm toàn bộ dịch vụ vào phạm vi $ thay vì xem các giá trị dịch vụ đơn lẻ. Tôi tránh đồng hồ $ của angular như ác quỷ, có đủ điều đó xảy ra trong các chỉ thị và ràng buộc dữ liệu góc cạnh bản địa.
dtheodor

107
Lý do tại sao chúng tôi sử dụng một khung như Angular là để không nấu các mẫu quan sát riêng của chúng tôi.
Code Whisperer

230

Trong một kịch bản như thế này, nơi nhiều đối tượng / không rõ có thể quan tâm đến các thay đổi, hãy sử dụng $rootScope.$broadcast từ mục được thay đổi.

Thay vì tạo sổ đăng ký người nghe của riêng bạn (phải được dọn sạch trên nhiều bản hủy $ khác nhau), bạn sẽ có thể $broadcasttừ dịch vụ được đề cập.

Bạn vẫn phải viết mã $ontrình xử lý trong mỗi người nghe nhưng mẫu được tách rời khỏi nhiều cuộc gọi đến $digestvà do đó tránh được rủi ro của người theo dõi lâu dài.

Bằng cách này, người nghe cũng có thể đến và đi từ DOM và / hoặc các phạm vi con khác nhau mà không cần dịch vụ thay đổi hành vi của nó.

** cập nhật: ví dụ **

Truyền phát sẽ có ý nghĩa nhất trong các dịch vụ "toàn cầu" có thể ảnh hưởng đến vô số thứ khác trong ứng dụng của bạn. Một ví dụ điển hình là dịch vụ Người dùng có một số sự kiện có thể diễn ra như đăng nhập, đăng xuất, cập nhật, nhàn rỗi, v.v. Tôi tin rằng đây là nơi phát sóng có ý nghĩa nhất vì mọi phạm vi đều có thể nghe một sự kiện, mà không thậm chí tiêm dịch vụ và không cần đánh giá bất kỳ biểu thức hoặc kết quả bộ đệm nào để kiểm tra các thay đổi. Nó chỉ kích hoạt và quên (vì vậy hãy chắc chắn rằng đó là một thông báo quên và không phải là thứ cần phải hành động)

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

Dịch vụ trên sẽ phát một thông báo tới mọi phạm vi khi hàm save () hoàn thành và dữ liệu hợp lệ. Ngoài ra, nếu đó là tài nguyên $ hoặc gửi ajax, hãy chuyển cuộc gọi quảng bá vào cuộc gọi lại để nó kích hoạt khi máy chủ phản hồi. Phát sóng phù hợp với mô hình đó đặc biệt tốt bởi vì mọi người nghe chỉ chờ đợi sự kiện mà không cần phải kiểm tra phạm vi trên mỗi $ digest. Người nghe sẽ như thế nào:

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

 }]);

Một trong những lợi ích của việc này là loại bỏ nhiều đồng hồ. Nếu bạn đang kết hợp các trường hoặc lấy các giá trị như ví dụ trên, bạn phải xem cả thuộc tính tên và họ. Xem hàm getUser () sẽ chỉ hoạt động nếu đối tượng người dùng được thay thế trên các bản cập nhật, nó sẽ không kích hoạt nếu đối tượng người dùng chỉ cập nhật các thuộc tính của nó. Trong trường hợp đó, bạn phải làm một chiếc đồng hồ sâu và chuyên sâu hơn.

$ Broadcast gửi tin nhắn từ phạm vi mà nó được gọi vào bất kỳ phạm vi con nào. Vì vậy, gọi nó từ $ rootScope sẽ bắn trên mọi phạm vi. Ví dụ, nếu bạn đã phát $ từ phạm vi của bộ điều khiển, nó sẽ chỉ bắn trong phạm vi kế thừa từ phạm vi điều khiển của bạn. $ emit đi theo hướng ngược lại và hoạt động tương tự như một sự kiện DOM ở chỗ nó tạo ra chuỗi phạm vi.

Hãy nhớ rằng có những kịch bản mà $ Broadcast có ý nghĩa rất lớn, và có những kịch bản trong đó $ watch là một lựa chọn tốt hơn - đặc biệt là trong phạm vi cách ly với biểu thức đồng hồ rất cụ thể.


1
Thoát khỏi chu trình $ digest là một điều tốt, đặc biệt nếu những thay đổi bạn đang xem không phải là một giá trị sẽ trực tiếp và ngay lập tức đi vào DOM.
XML

Có cách nào để tránh phương thức .save (). Có vẻ như quá mức khi bạn chỉ theo dõi cập nhật của một biến duy nhất trong dịch vụ sharedService. Chúng tôi có thể xem biến từ bên trong dịch vụ sharedService và phát khi thay đổi không?
JerryKur

Tôi đã thử khá nhiều cách để chia sẻ dữ liệu giữa các bộ điều khiển, nhưng đây là cách duy nhất có hiệu quả. Chơi tốt, thưa ngài.
abettermap

Tôi thích câu trả lời này hơn các câu trả lời khác, có vẻ ít hack hơn , cảm ơn
JMK

9
Đây là mẫu thiết kế chính xác chỉ khi bộ điều khiển tiêu thụ của bạn có nhiều nguồn dữ liệu có thể; nói cách khác, nếu bạn có tình huống MIMO (Nhiều đầu vào / Nhiều đầu ra). Nếu bạn chỉ đang sử dụng mẫu một-nhiều, bạn nên sử dụng tham chiếu đối tượng trực tiếp và để khung Angular thực hiện ràng buộc hai chiều cho bạn. Horkyze đã liên kết điều này bên dưới và đó là một lời giải thích tốt về ràng buộc hai chiều tự động và những hạn chế của nó: stsc3000.github.io/blog/2013/10/26/ Lỗi
Charles

47

Tôi đang sử dụng cách tiếp cận tương tự như @dtheodot nhưng sử dụng lời hứa góc cạnh thay vì chuyển các cuộc gọi lại

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

Sau đó, bất cứ nơi nào chỉ cần sử dụng myService.setFoo(foo)phương pháp để cập nhật footrên dịch vụ. Trong bộ điều khiển của bạn, bạn có thể sử dụng nó như:

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

Hai đối số đầu tiên thenlà gọi lại thành công và lỗi, thứ ba là thông báo gọi lại.

Tham khảo cho $ q.


Điều gì sẽ là lợi thế của phương pháp này so với phát sóng $ được mô tả dưới đây bởi Matt Pileggi?
Fabio

Cả hai phương pháp đều có công dụng của chúng. Ưu điểm của việc phát sóng đối với tôi sẽ là khả năng đọc của con người và khả năng nghe ở nhiều nơi hơn cho cùng một sự kiện. Tôi đoán nhược điểm chính là phát sóng đang phát thông điệp đến tất cả các phạm vi hậu duệ nên nó có thể là một vấn đề hiệu suất.
Krym

2
Tôi đã gặp vấn đề khi thực hiện $scope.$watchbiến dịch vụ dường như không hoạt động (phạm vi tôi đang xem là một phương thức được kế thừa từ đó $rootScope) - điều này đã hoạt động. Thủ thuật hay, cảm ơn vì đã chia sẻ!
Seiyria

4
Làm thế nào bạn sẽ làm sạch sau khi chính mình với phương pháp này? Có thể xóa cuộc gọi lại đã đăng ký khỏi lời hứa khi phạm vi bị hủy không?
Abris

Câu hỏi hay. Tôi thực sự không biết. Tôi sẽ thử thực hiện một số thử nghiệm về cách bạn có thể xóa thông báo gọi lại từ lời hứa.
Krym

41

Không có đồng hồ hoặc cuộc gọi lại của người quan sát ( 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 này hoạt động khi chúng ta chuyển một đối tượng trở lại từ dịch vụ chứ không phải là một giá trị. Khi một đối tượng JavaScript được trả về từ một dịch vụ, Angular sẽ thêm đồng hồ vào tất cả các thuộc tính của nó.

Cũng lưu ý rằng tôi đang sử dụng 'var self = this' vì tôi cần giữ một tham chiếu đến đối tượng ban đầu khi $ timeout thực thi, nếu không 'this' sẽ đề cập đến đối tượng cửa sổ.


3
Đây là một cách tiếp cận tuyệt vời! Có cách nào để ràng buộc chỉ một thuộc tính của một dịch vụ với phạm vi thay vì toàn bộ dịch vụ không? Chỉ làm $scope.count = service.countkhông làm việc.
jvannistelrooy

Bạn cũng có thể lồng tài sản bên trong một đối tượng (tùy ý) để nó được chuyển qua tham chiếu. $scope.data = service.data <p>Count: {{ data.count }}</p>
Alex Ross

1
Cách tiếp cận tuyệt vời! Mặc dù có rất nhiều câu trả lời mạnh mẽ, đầy đủ chức năng trên trang này, nhưng đến nay a) dễ thực hiện nhất và b) dễ hiểu nhất khi đọc qua mã. Câu trả lời này sẽ cao hơn rất nhiều so với hiện tại.
CodeMoose

Cảm ơn @CodeMoose, tôi đã đơn giản hóa nó hơn nữa cho những người mới sử dụng AngularJS / JavaScript.
Zymotik

2
Chúa có thể ban phước cho bạn. Tôi đã lãng phí như hàng triệu giờ tôi sẽ nói. Bởi vì tôi đã vật lộn với 1,5 và angularjs chuyển từ 1 sang 2 và cũng muốn chia sẻ dữ liệu
Amna

29

Tôi vấp phải câu hỏi này để tìm kiếm một cái gì đó tương tự, nhưng tôi nghĩ rằng nó xứng đáng được giải thích kỹ lưỡng về những gì đang xảy ra, cũng như một số giải pháp bổ sung.

Khi một biểu thức góc như biểu thức bạn đã sử dụng có trong HTML, Angular sẽ tự động thiết lập một $watchfor $scope.foovà sẽ cập nhật HTML bất cứ khi nào $scope.foothay đổi.

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

Vấn đề chưa được giải quyết ở đây là một trong hai điều đang ảnh hưởng đến aService.foo mức những thay đổi không bị phát hiện. Hai khả năng này là:

  1. aService.foo đang được thiết lập thành một mảng mới mỗi lần, khiến tham chiếu đến nó bị lỗi thời.
  2. aService.foođang được cập nhật theo cách mà một $digestchu kỳ không được kích hoạt trên bản cập nhật.

Vấn đề 1: Tài liệu tham khảo lỗi thời

Xem xét khả năng đầu tiên, giả sử một $digestđang được áp dụng, nếu aService.fooluôn luôn là cùng một mảng, tự động được đặt$watch sẽ phát hiện các thay đổi, như được hiển thị trong đoạn mã dưới đây.

Giải pháp 1-a: Đảm bảo mảng hoặc đối tượng là cùng một đối tượng trên mỗi bản cập nhật

Như bạn có thể thấy, ng-repeat được cho là gắn liền với aService.fookhông cập nhật khi aService.foothay đổi, nhưng những ng-repeat gắn liền với aService2.foo thực hiện . Điều này là do tham chiếu của chúng tôi aService.foođã lỗi thời, nhưng tham chiếu của chúng tôi aService2.foothì không. Chúng tôi đã tạo một tham chiếu đến mảng ban đầu $scope.foo = aService.foo;, sau đó bị dịch vụ loại bỏ trên bản cập nhật tiếp theo, nghĩa là$scope.foo không còn tham chiếu mảng chúng tôi muốn nữa.

Tuy nhiên, trong khi có một số cách để đảm bảo tham chiếu ban đầu được giữ trong chiến thuật, đôi khi có thể cần phải thay đổi đối tượng hoặc mảng. Hoặc nếu tài sản dịch vụ tham chiếu một nguyên thủy như một Stringhoặc Number? Trong những trường hợp đó, chúng ta không thể chỉ dựa vào một tài liệu tham khảo. Vậy những gì có thể chúng ta làm gì?

Một số câu trả lời được đưa ra trước đây đã đưa ra một số giải pháp cho vấn đề đó. Tuy nhiên, cá nhân tôi ủng hộ việc sử dụng phương pháp đơn giản được đề xuất bởi Jinthetallweek trong các bình luận:

chỉ cần tham khảo aService.foo trong đánh dấu html

Giải pháp 1-b: Đính kèm dịch vụ vào phạm vi và tham chiếu {service}.{property}trong HTML.

Có nghĩa là, chỉ cần làm điều này:

HTML:

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

JS:

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

Bằng cách đó, ý $watchchí sẽ giải quyết aService.footrên mỗi$digest , sẽ nhận được giá trị cập nhật chính xác.

Đây là loại những gì bạn đã cố gắng thực hiện với cách giải quyết của bạn, nhưng theo cách ít vòng hơn nhiều. Bạn đã thêm không cần thiết $watchtrong bộ điều khiển mà rõ ràng đặt footrên $scopebất cứ khi nào nó thay đổi. Bạn không cần phải thêm rằng $watchkhi bạn đính kèm aServicethay vì aService.foođến $scope, và ràng buộc rõ ràng để aService.footrong đánh dấu.


Bây giờ tất cả đều tốt và tốt khi giả định một $digestchu kỳ đang được áp dụng. Trong các ví dụ của tôi ở trên, tôi đã sử dụng $intervaldịch vụ của Angular để cập nhật các mảng, tự động khởi động một $digestvòng lặp sau mỗi lần cập nhật. Nhưng điều gì xảy ra nếu các biến dịch vụ (vì bất kỳ lý do gì) không được cập nhật bên trong "Thế giới góc cạnh". Nói cách khác, chúng ta không$digestchu trình được kích hoạt tự động mỗi khi thuộc tính dịch vụ thay đổi?


Vấn đề 2: Mất tích $digest

Nhiều giải pháp ở đây sẽ giải quyết vấn đề này, nhưng tôi đồng ý với Code Whisperer :

Lý do tại sao chúng tôi sử dụng một khung như Angular là để không nấu các mẫu quan sát riêng của chúng tôi

Vì vậy, tôi muốn tiếp tục sử dụng aService.foo tham chiếu trong đánh dấu HTML như trong ví dụ thứ hai ở trên và không phải đăng ký một cuộc gọi lại bổ sung trong Bộ điều khiển.

Giải pháp 2: Sử dụng setter và getter với $rootScope.$apply()

Tôi đã ngạc nhiên khi chưa có ai đề nghị sử dụng settergetter . Khả năng này đã được giới thiệu trong ECMAScript5, và do đó đã có từ nhiều năm nay. Tất nhiên, điều đó có nghĩa là nếu vì bất kỳ lý do gì, bạn cần hỗ trợ các trình duyệt thực sự cũ, thì phương pháp này sẽ không hoạt động, nhưng tôi cảm thấy như getters và setters được sử dụng rất nhiều trong JavaScript. Trong trường hợp cụ thể này, chúng có thể khá hữu ích:

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

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

Ở đây tôi đã thêm một biến 'riêng tư' trong chức năng dịch vụ : realFoo. Điều này nhận được cập nhật và truy xuất bằng cách sử dụng get foo()và các set foo()chức năng tương ứng trênservice đối tượng.

Lưu ý việc sử dụng $rootScope.$apply()trong chức năng thiết lập. Điều này đảm bảo rằng Angular sẽ nhận thức được bất kỳ thay đổi nào service.foo. Nếu bạn gặp lỗi 'inprog', hãy xem trang tham khảo hữu ích này hoặc nếu bạn sử dụng Angular> = 1.3, bạn chỉ có thể sử dụng $rootScope.$applyAsync().

Ngoài ra, hãy cảnh giác với điều này nếu aService.foođược cập nhật rất thường xuyên, vì điều đó có thể ảnh hưởng đáng kể đến hiệu suất. Nếu hiệu suất là một vấn đề, bạn có thể thiết lập một mẫu quan sát tương tự như các câu trả lời khác ở đây bằng cách sử dụng bộ cài đặt.


3
Đây là giải pháp chính xác và dễ dàng nhất. Như @NanoWizard nói, đồng hồ $ digest serviceskhông dành cho các thuộc tính thuộc về chính dịch vụ.
Sarpdoruk Tahmaz

28

Theo như tôi có thể nói, bạn không cần phải làm một cái gì đó công phu như thế. Bạn đã gán foo từ dịch vụ cho phạm vi của mình và vì foo là một mảng (và đến lượt một đối tượng, nó được gán bởi tham chiếu!). Vì vậy, tất cả những gì bạn cần làm là một cái gì đó như thế này:

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

 }

Nếu một số, biến khác trong cùng Ctrl này phụ thuộc vào foo thay đổi thì có, bạn sẽ cần một chiếc đồng hồ để quan sát foo và thay đổi biến đó. Nhưng miễn là nó là một tài liệu tham khảo đơn giản là không cần thiết. Hi vọng điêu nay co ich.


35
Tôi đã thử, và tôi không thể $watchlàm việc với người nguyên thủy. Thay vào đó, tôi đã định nghĩa một phương thức trên dịch vụ sẽ trả về giá trị nguyên thủy: somePrimitive() = function() { return somePrimitive }Và tôi đã gán một thuộc tính $ scope cho phương thức đó : $scope.somePrimitive = aService.somePrimitive;. Sau đó, tôi đã sử dụng phương thức phạm vi trong HTML: <span>{{somePrimitive()}}</span>
Mark Rajcok

4
@MarkRajcok Không sử dụng nguyên thủy. Thêm chúng trong một đối tượng. Người nguyên thủy không thể biến đổi và do đó, liên kết dữ liệu 2 chiều sẽ không hoạt động
Jimmy Kane

3
@JimmyKane, vâng, không nên sử dụng các nguyên hàm cho cơ sở dữ liệu 2 chiều, nhưng tôi nghĩ câu hỏi là về việc xem các biến dịch vụ, không thiết lập ràng buộc 2 chiều. Nếu bạn chỉ cần xem một thuộc tính / biến dịch vụ, một đối tượng không bắt buộc - có thể sử dụng một nguyên thủy.
Mark Rajcok

3
Trong thiết lập này, tôi có thể thay đổi giá trị aService từ phạm vi. Nhưng phạm vi không thay đổi để đáp ứng thay đổi aService.
Ouwen Huang

4
Điều này cũng không làm việc cho tôi. Chỉ cần gán $scope.foo = aService.fookhông cập nhật biến phạm vi tự động.
Darwin Tech

9

Bạn có thể chèn dịch vụ vào $ rootScope và xem:

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

Trong bộ điều khiển của bạn:

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

Tùy chọn khác là sử dụng thư viện bên ngoài như: https://github.com/melanke/Watch.JS

Hoạt động với: IE 9+, FF 4+, SF 5+, WebKit, CH 7+, OP 12+, BESEN, Node.JS, Rhino 1.7+

Bạn có thể quan sát những thay đổi của một, nhiều hoặc tất cả các thuộc tính đối tượng.

Thí dụ:

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
TÔI KHÔNG THỂ TIN CÂU TRẢ LỜI NÀY KHÔNG PHẢI LÀ NGƯỜI ĐẦU TIÊN NHẤT !!! Đây là giải pháp tao nhã nhất (IMO) vì nó làm giảm entropy thông tin & có thể giảm thiểu sự cần thiết phải xử lý Hòa giải bổ sung. Tôi sẽ bỏ phiếu này nhiều hơn nếu tôi có thể ...
Cody

Thêm tất cả các dịch vụ của bạn vào $ rootScope, lợi ích của nó và những cạm bẫy tiềm ẩn của nó, được trình bày chi tiết ở đây: stackoverflow.com/questions/14573023/ trên
Zymotik

6

Bạn có thể xem các thay đổi trong chính nhà máy và sau đó phát một thay đổi

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

Sau đó, trong bộ điều khiển của bạn:

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

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

Theo cách này, bạn đặt tất cả các mã nhà máy liên quan trong mô tả của nó, sau đó bạn chỉ có thể dựa vào phát sóng từ bên ngoài


6

== CẬP NHẬT ==

Bây giờ rất đơn giản trong $ watch.

Bút ở đây .

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
Tôi thích PostmanService nhưng làm thế nào tôi phải thay đổi chức năng $ watch trên bộ điều khiển nếu tôi cần nghe nhiều hơn một biến?
jedi

Hi jedi, cảm ơn cho những người đứng đầu! Tôi cập nhật bút và câu trả lời. Tôi khuyên bạn nên thêm một chức năng đồng hồ cho điều đó. Vì vậy, tôi đã thêm một tính năng mới vào PostmanService. Tôi hy vọng điều này sẽ giúp :)
hayatbirusem

Trên thực tế, đúng vậy :) Nếu bạn chia sẻ chi tiết hơn về vấn đề có lẽ tôi có thể giúp bạn.
hayatbirusem

4

Dựa trên câu trả lời của dtheodor, bạn có thể sử dụng một cái gì đó tương tự như dưới đây để đảm bảo rằng bạn không quên hủy đăng ký cuộc gọi lại ... Tuy nhiên, một số người có thể phản đối việc chuyển $scopesang dịch vụ.

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 là một phương thức mở rộng trông như thế này:

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

Đây là cách tiếp cận chung của tôi.

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

    }]);

Trong bộ điều khiển của bạn

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

Đối với những người như tôi chỉ tìm kiếm một giải pháp đơn giản, điều này gần như chính xác những gì bạn mong đợi từ việc sử dụng đồng hồ $ bình thường trong bộ điều khiển. Sự khác biệt duy nhất là, nó đánh giá chuỗi trong ngữ cảnh javascript chứ không phải trên một phạm vi cụ thể. Bạn sẽ phải tiêm $ rootScope vào dịch vụ của mình, mặc dù nó chỉ được sử dụng để móc vào các chu trình tiêu hóa đúng cách.

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

2

Trong khi đối mặt với một vấn đề rất giống nhau, tôi đã xem một hàm trong phạm vi và có hàm trả về biến dịch vụ. Tôi đã tạo ra một fiddle js . bạn có thể tìm thấy mã dưới đây.

    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

Tôi đã đến câu hỏi này nhưng hóa ra vấn đề của tôi là tôi đã sử dụng setInterval khi tôi nên sử dụng nhà cung cấp $ distance góc. Đây cũng là trường hợp cho setTimeout (thay vào đó sử dụng $ timeout). Tôi biết đó không phải là câu trả lời cho câu hỏi của OP, nhưng nó có thể giúp một số người, vì nó đã giúp tôi.


Bạn có thể sử dụng setTimeouthoặc bất kỳ chức năng phi góc nào khác, nhưng đừng quên bọc mã trong cuộc gọi lại với $scope.$apply().
Magnetronnie

2

Tôi đã tìm thấy một giải pháp thực sự tuyệt vời trên chủ đề khác với một vấn đề tương tự nhưng cách tiếp cận hoàn toàn khác. Nguồn: AngularJS: $ watch trong chỉ thị không hoạt động khi giá trị $ rootScope bị thay đổi

Về cơ bản , giải pháp ở đó bảo KHÔNG sử dụng $watchvì đây là giải pháp rất nặng. Thay vào đó họ đề xuất sử dụng $emit$on .

Vấn đề của tôi là xem một biến trong dịch vụ của tôi và phản ứng theo chỉ thị . Và với phương pháp trên thì rất dễ!

Ví dụ mô-đun / dịch vụ của tôi:

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

Vì vậy, về cơ bản tôi xem của tôi user - bất cứ khi nào nó được đặt thành giá trị mới, tôi $emitlà một user:changetrạng thái.

Bây giờ trong trường hợp của tôi, trong chỉ thị tôi đã sử dụng:

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

Bây giờ trong chỉ thị tôi nghe trên $rootScopevề sự thay đổi được - tôi phản ứng tương ứng. Rất dễ dàng và thanh lịch!


1

// dịch vụ: (không có gì đặc biệt ở đây)

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

Một chút xấu xí, nhưng tôi đã thêm đăng ký biến phạm vi cho dịch vụ của mình để chuyển đổi:

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

Và sau đó trong bộ điều khiển:

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

Cảm ơn. Một câu hỏi nhỏ: tại sao bạn trở về thistừ dịch vụ đó thay vì self?
shrekuu

4
Bởi vì đôi khi sai lầm được thực hiện. ;-)
nclu

Thực hành tốt để return this;ra khỏi các nhà xây dựng của bạn ;-)
Cody

1

Hãy xem plunker này :: đây là ví dụ đơn giản nhất tôi có thể nghĩ đến

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

Tôi đã thấy một số mẫu quan sát khủng khiếp ở đây gây rò rỉ bộ nhớ trên các ứng dụng lớn.

Tôi có thể hơi muộn nhưng nó đơn giản như thế này.

Chức năng đồng hồ theo dõi các thay đổi tham chiếu (loại nguyên thủy) nếu bạn muốn xem một cái gì đó như đẩy mảng chỉ đơn giản là sử dụng:

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

Điều này sẽ cập nhật các tài liệu tham khảo và cập nhật đồng hồ từ bất cứ đâu. Bao gồm một phương thức getter dịch vụ. Bất cứ điều gì đó là nguyên thủy sẽ được cập nhật tự động.


0

Tôi đến trễ một phần nhưng tôi tìm thấy một cách tốt hơn để làm điều này hơn câu trả lời được đăng ở trên. Thay vì chỉ định một biến để giữ giá trị của biến dịch vụ, tôi đã tạo một hàm gắn liền với phạm vi, trả về biến dịch vụ.

bộ điều khiển

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

Tôi nghĩ rằng điều này sẽ làm những gì bạn muốn. Bộ điều khiển của tôi tiếp tục kiểm tra giá trị dịch vụ của tôi với việc triển khai này. Thành thật mà nói, điều này đơn giản hơn nhiều so với câu trả lời được chọn.


tại sao nó bị hạ cấp .. Tôi cũng đã sử dụng kỹ thuật tương tự nhiều lần và nó đã hoạt động.
không xác định

0

Tôi đã viết hai dịch vụ tiện ích đơn giản giúp tôi theo dõi các thay đổi thuộc tính dịch vụ.

Nếu bạn muốn bỏ qua lời giải thích dài, bạn có thể đi đến eo biển 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;
  };
}

Dịch vụ này được sử dụng trong bộ điều khiển theo cách sau: Giả sử bạn có dịch vụ "testService" với các thuộc tính 'prop1', 'prop2', 'prop3'. Bạn muốn xem và gán cho phạm vi 'prop1' và 'prop2'. Với dịch vụ đồng hồ, nó sẽ trông như thế:

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. áp dụng Watch obj là tuyệt vời, nhưng nó không đủ nếu bạn có mã không đồng bộ trong dịch vụ của mình. Trong trường hợp đó, tôi sử dụng một tiện ích thứ hai trông như thế:

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

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

Tôi sẽ kích hoạt nó ở cuối mã async của mình để kích hoạt vòng lặp $ digest. Như thế

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

Vì vậy, tất cả những thứ đó cùng nhau sẽ trông như thế (bạn có thể chạy nó hoặc mở fiddle ):

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

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.