AngularJS: Як переглянути змінні служби?


414

У мене є служба, скажімо:

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

  return service;
}]);

І я хотів би використовувати fooдля керування списком, що надається в HTML:

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

Для того, щоб контролер виявив, коли aService.fooоновлено, я зв'язав цей шаблон, де я додаю aService до контролера, $scopeа потім використовую $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;
    }
  });
}

Це відчувається довго, і я це повторював у кожному контролері, який використовує змінні служби. Чи є кращий спосіб досягти перегляду загальних змінних?


1
Ви можете передати третій параметр $ watch, встановлений на true, для глибокого перегляду aService та всіх його властивостей.
SirTophamHatt

7
$ range.foo = aService.foo достатньо, ви можете втратити рядок вище. І що це робиться всередині $ watch не має сенсу, якщо ви хочете призначити нове значення $ range.foo просто зробіть це ...
Цзінь

4
Чи можете ви просто вказати aService.fooв розмітці html? (як-от так: plnkr.co/edit/aNrw5Wo4Q0IxR2loipl5?p=preview )
thetallweeks

1
Я додав приклад без зворотних зворотів або $ годин, див. Відповідь нижче ( jsfiddle.net/zymotik/853wvv7s )
Zymotik

1
@MikeGledhill, ти маєш рацію. Я думаю, що це обумовлено характером Javascript, ви можете побачити цю закономірність і в багатьох інших місцях (не тільки в Angular, але і в JS взагалі). З одного боку, ви передаєте значення (і воно не зв'язане), а з іншого - ви передаєте об'єкт (або значення, яке посилається на об'єкт ...), і тому властивості правильно оновлюються (як ідеально показано на прикладі Зимотика вище).
Крістоф Відал

Відповіді:


277

Ви завжди можете використовувати старий добрий зразок спостерігача, якщо хочете уникнути свавілля та накладних витрат $watch.

У сервісі:

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

І в контролері:

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

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

21
@Moo прослухайте $destoryподію на темі і додайте нереєстраційний методaService
Джеймі

13
Які плюси цього рішення? Потрібно більше коду в сервісі і дещо стільки ж коду в контролері (оскільки нам також потрібно скасувати реєстрацію на $ знищити). Я можу сказати щодо швидкості виконання, але в більшості випадків це просто не має значення.
Алекс Че

6
не впевнений, наскільки це краще рішення, ніж $ watch, запитуючий запитував простий спосіб обміну даними, він виглядає ще більш громіздким. Я вважаю за краще скористатись $ трансляцією, ніж це
Цзінь

11
$watchпроти шаблону спостерігача - це просто вибір, чи опитуватись чи натиснути, і це, головним чином, питання продуктивності, тому використовуйте його, коли продуктивність має значення. Я використовую схему спостерігачів, коли в іншому випадку мені доведеться "глибоко" спостерігати за складними об'єктами. Я додаю цілі сервіси до області $ замість перегляду значень однієї служби. Я уникаю кутових $ годинник, як чорт, достатньо того, що відбувається в директивах і в природних кутових прив'язках даних.
dtheodor

107
Причина, чому ми використовуємо таку структуру, як Angular, полягає в тому, щоб не готувати власні шаблони спостерігачів.
Кодекс Шепітера

230

У такому сценарії, коли кілька / невідомі об'єкти можуть бути зацікавлені у змінах, використовуйте $rootScope.$broadcastелемент, що змінюється.

Замість того, щоб створювати власний реєстр слухачів (які потрібно очистити від різних $ знищень), ви повинні мати можливість $broadcastз відповідної служби.

Ви все одно повинні кодувати $onобробники в кожному слухачі, але модель від'єднується від декількох дзвінків $digestі, таким чином, уникає ризику довготривалих спостерігачів.

Таким чином, слухачі також можуть приїжджати і переходити з DOM та / або різних областей, не змінюючи свою поведінку.

** оновлення: приклади **

Трансляція матиме найбільше сенс у "глобальних" послугах, які можуть впливати на безліч інших речей у вашій програмі. Хороший приклад - служба користувача, в якій може відбутися ряд подій, таких як вхід, вихід із системи, оновлення, очікування тощо. Я вважаю, що саме там трансляції мають найбільш сенс, оскільки будь-яка сфера може слухати подію, без навіть вводить послугу, і не потрібно оцінювати будь-які вирази чи результати кешу, щоб перевірити на наявність змін. Він просто вистрілює і забуває (тому переконайтеся, що це повідомлення про пожежу та забуття, а не те, що вимагає дій)

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

Служба, наведена вище, транслюватиме повідомлення в будь-яку сферу, коли функція збереження () завершена, а дані дійсні. Крім того, якщо це ресурс $ або подання ajax, перемістіть широкомовний виклик у зворотний виклик, щоб він запустився, коли сервер відповів. Трансляції особливо добре відповідають цій схемі, оскільки кожен слухач просто чекає події без необхідності перевіряти сферу застосування кожного дайджесту $. Слухач виглядав би так:

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

 }]);

Однією з переваг цього є усунення декількох годин. Якщо ви поєднували поля або отримували значення, як у наведеному вище прикладі, вам доведеться переглянути властивості імені та прізвища. Перегляд функції getUser () спрацює лише в тому випадку, якщо об’єкт користувача буде замінено на оновлення, він не запуститься, якщо користувальницький об’єкт просто оновив його властивості. У такому випадку вам доведеться глибоко спостерігати, і це більш інтенсивно.

$ Broadcast надсилає повідомлення з тієї сфери, на яку воно закликається вниз, у будь-які дочірні області. Тому виклик його з $ rootScope запускатиме всі області. Якщо ви, наприклад, передавали $ із сфери управління контролера, він запускатиметься лише у сферах дії, які успадковуються від вашої області контролера. $ emit переходить у зворотному напрямку і поводиться аналогічно події DOM, коли воно пухиряє ланцюг області.

Майте на увазі, що існують сценарії, де $ мовлення має багато сенсу, і є сценарії, де $ watch є кращим варіантом - особливо, якщо це окрема область із дуже конкретним виразом годинника.


1
Вихід із циклу дайджестів $ - це гарна річ, особливо якщо зміни, які ви переглядаєте, не є цінністю, яка безпосередньо і негайно перейде в домен.
XML

Чи є спосіб уникнути .save () методу. Схоже, надмірність, коли ви лише стежите за оновленням однієї змінної в sharedService. Чи можемо ми переглядати змінну зсередини sharedService та транслювати, коли вона змінюється?
JerryKur

Я спробував досить багато способів обміну даними між контролерами, але це єдиний, який працював. Добре зіграно, сер.
abettermap

Я вважаю за краще це перед іншими відповідями, здається менш хакітним , дякую
JMK

9
Це правильна схема дизайну, лише якщо ваш споживач-контролер має декілька можливих джерел даних; іншими словами, якщо у вас ситуація MIMO (Кілька входів / Кілька вихідних даних). Якщо ви просто використовуєте шаблон "один на багато", вам слід використовувати пряме посилання на об'єкт і дозволяти Angular Framework робити двосторонній зв'язок для вас. Horkyze пов’язав це нижче, і це добре пояснення автоматичного двостороннього прив'язки, і це обмеження: stsc3000.github.io/blog/2013/10/26/…
Чарльз

47

Я використовую аналогічний підхід, як @dtheodot, але використовую углову обіцянку замість передачі зворотних дзвінків

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

Тоді, де тільки використовувати myService.setFoo(foo)метод для оновлення fooна сервісі. У своєму контролері ви можете використовувати його як:

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

Перші два аргументи then- успіх та помилка зворотних викликів, третій - це зворотній зв'язок із сповіщенням.

Довідка для $ q.


Яка була б перевага цього методу над $ трансляцією, описаною нижче Меттом Пілегі?
Фабіо

Ну і обидва методи мають своє використання. Перевагами мовлення для мене були б людська читаність та можливість слухати більше місця на одній події. Я думаю, головним недоліком є ​​те, що трансляція посилає повідомлення всім нащадкам, тому це може бути проблемою продуктивності.
Крим

2
У мене виникли проблеми, коли робота $scope.$watchзі змінною сервісу, здавалося б, не працювала (область, за якою я спостерігав, - модал, який успадкований від $rootScope) - це спрацювало. Класна хитрість, дякую за обмін!
Seiyria

4
Як би ви прибирали після себе таким підходом? Чи можливо видалити зареєстрований зворотний дзвінок із обіцянки, коли область буде знищена?
Абріс

Хороше питання. Я, чесно кажучи, не знаю. Я спробую зробити кілька тестів на те, як можна було видалити сповіщення з обіцянки.
Крим

41

Без відкликання годинника чи спостереження ( 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 працює, коли ми передаємо об'єкт із сервісу, а не значення. Коли об’єкт JavaScript повертається з сервісу, Angular додає годинник до всіх його властивостей.

Також зауважте, що я використовую "var self = this", оскільки мені потрібно зберігати посилання на оригінальний об'єкт, коли виконується $ timeout, інакше "this" буде стосуватися об'єкта вікна.


3
Це чудовий підхід! Чи є спосіб прив’язати просто властивість послуги до сфери застосування замість усієї послуги? Просто робити $scope.count = service.countне виходить.
jvannistelrooy

Ви також можете вкласти властивість всередині (довільного) об'єкта, щоб воно передалося посиланням. $scope.data = service.data <p>Count: {{ data.count }}</p>
Алекс Росс

1
Відмінний підхід! Хоча на цій сторінці багато вагомих функціональних відповідей, це, безумовно, а) найпростіший в реалізації та б) найпростіший для розуміння під час читання коду. Ця відповідь повинна бути набагато вищою, ніж зараз.
CodeMoose

Спасибі @CodeMoose, я сьогодні ще більше спростив її для тих, хто не ввійшов у AngularJS / JavaScript.
Зимотик

2
Нехай Бог благословить вас. Я б сказав, що я витратив, як мільйон годин. Тому що я боровся з 1,5 і angularjs перехід від 1 до 2 , а також хотів акцію
Amna

29

Я натрапив на це питання, шукаючи щось подібне, але вважаю, що воно заслуговує ґрунтовного пояснення того, що відбувається, а також деяких додаткових рішень.

Коли кутове вираження, таке, яке ви використовували, присутнє в HTML, Angular автоматично встановлює $watchдля $scope.foo, і оновлюватиме HTML щоразу, коли $scope.fooзміниться.

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

Незгадане питання полягає в тому, що одна з двох речей впливає на aService.foo таке, що зміни не виявляються. Ці дві можливості:

  1. aService.foo щоразу встановлюється на новий масив, внаслідок чого посилання на нього застаріває.
  2. aService.fooоновлюється таким чином, що $digestцикл на оновлення не спрацьовує.

Проблема 1: Застарілі посилання

Враховуючи першу можливість, якщо припустити $digest, що застосовується a , якщо aService.fooзавжди був один і той же масив, автоматично встановлений режим $watchвиявив би зміни, як показано в фрагменті коду нижче.

Рішення 1-a. Переконайтеся, що масив або об'єкт є однаковим об'єктом для кожного оновлення

Як ви можете бачити, нг-повтор нібито додається до aService.fooне оновлюється , коли aService.fooзміни, але нг повтор додається до aService2.foo робить . Це тому, що наше посилання на aService.fooзастаріле, але наше посилання на aService2.fooце не так. Ми створили посилання на початковий масив з $scope.foo = aService.foo;, який потім було відкинуто службою під час наступного оновлення, тобто $scope.fooбільше не посилався на масив, на який ми хотіли.

Однак, хоча існує кілька способів переконатися, що початкове посилання зберігається в такті, іноді може знадобитися змінити об'єкт або масив. Або що робити, якщо властивість служби посилається на примітив типу a Stringабо Number? У таких випадках ми не можемо просто покластися на посилання. Так що можна робити?

Кілька відповідей, що були раніше, вже дають деякі рішення цієї проблеми. Однак я особисто виступаю за використання простого методу, запропонованого Джином та thetallweeks у коментарях:

просто посилайтесь на aService.foo у розмітці html

Рішення 1-b: приєднайте службу до сфери застосування та посилання {service}.{property}в HTML.

Значить, просто так:

HTML:

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

JS:

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

Таким чином, рішення $watchбуде вирішене aService.fooдля кожного $digest, який отримає правильно оновлене значення.

Це щось із того, що ви намагалися зробити зі своїм вирішенням, але в набагато меншій мірі. Ви додали непотрібні $watchв контролері , який явно ставить fooна $scopeвсякий раз , коли вона змінюється. Ви не маєте потребу в додатковому $watchпри підключенні aServiceзамість aService.fooдо $scope, і прив'язати явно aService.fooв розмітці.


Тепер це все добре і добре, якщо застосовувати $digestцикл. У своїх вище прикладах я використовував $intervalслужбу Angular для оновлення масивів, яка автоматично завершує $digestцикл після кожного оновлення. Але що робити, якщо змінні служби (з будь-якої причини) не оновлюються всередині «кутового світу». Іншими словами, ми DonT є $digestцикл активується автоматично , коли змінюється якість роботи?


Проблема 2: Відсутня $digest

Багато тут рішень вирішать це питання, але я згоден з Code Whisperer :

Причина, чому ми використовуємо таку структуру, як Angular, полягає в тому, щоб не готувати власні шаблони спостерігачів

Тому я вважаю за краще продовжувати використовувати aService.fooпосилання у розмітці HTML, як показано у другому прикладі вище, і не потрібно реєструвати додатковий зворотний виклик у контролері.

Рішення 2: Використовуйте сетер та геттер $rootScope.$apply()

Я був здивований, поки ніхто не запропонував використовувати сетер та геттер . Ця можливість була введена в ECMAScript5, і тому вона існує вже багато років. Звичайно, це означає, що якщо з будь-якої причини вам потрібно підтримувати дійсно старі браузери, тоді цей метод не працюватиме, але я відчуваю, що геттери та сетери значно використовуються в JavaScript. У цьому конкретному випадку вони можуть бути дуже корисними:

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

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

Тут я додав «приватний» змінну у функції служби: realFoo. Це оновлюється та отримується за допомогою відповідно get foo()і set foo()функцій на serviceоб'єкті.

Зверніть увагу на використання $rootScope.$apply()в заданій функції. Це гарантує, що Angular буде в курсі будь-яких змін service.foo. Якщо у вас виникли помилки "inprog", перегляньте цю корисну довідкову сторінку , або якщо ви використовуєте Angular> = 1.3, ви можете просто скористатися $rootScope.$applyAsync().

Також будьте обережні, якщо aService.fooвін оновлюється дуже часто, оскільки це може суттєво вплинути на ефективність роботи. Якщо продуктивність буде проблемою, ви можете встановити шаблон спостерігача, подібний до інших відповідей тут, використовуючи сеттер.


3
Це правильне і найпростіше рішення. Як кажуть @NanoWizard, $ digest спостерігає servicesза властивостями, які належать самій службі.
Сарпдорук Тахмаз

28

Наскільки я можу сказати, вам не потрібно робити щось таке витончене. Ви вже призначили foo з сервісу до вашої області, а оскільки foo - це масив (а, в свою чергу, об'єкт, він присвоюється посиланням!). Отже, все, що вам потрібно зробити, - це щось подібне:

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

 }

Якщо якась інша змінна в цьому ж Ctrl залежить від зміни foo, то так, вам знадобиться стежити, щоб спостерігати за foo та вносити зміни до цієї змінної. Але поки це просте перегляд довідок, це зайве. Сподіваюсь, це допомагає.


35
Я намагався, і мені не $watchвдалося працювати з примітивом. Замість цього, я визначив метод на службу , яка буде повертати елементарне значення: somePrimitive() = function() { return somePrimitive }І я присвоїв властивість $ можливості для цього методу: $scope.somePrimitive = aService.somePrimitive;. Тоді я застосував метод застосування в HTML: <span>{{somePrimitive()}}</span>
Марк Райкок

4
@MarkRajcok Не використовуйте примітиви. Додайте їх у об’єкт. Примітиви не є мутантами, тому 2-канальне прив'язування даних не працюватиме
Джиммі Кейн

3
@JimmyKane, так, примітиви не повинні використовуватися для двостороннього зв’язування даних, але я думаю, питання полягало у перегляді змінних служб, а не в налаштуванні двостороннього прив'язки. Якщо вам потрібно лише переглянути властивість послуги / змінну, об'єкт не потрібен - примітив може бути використаний.
Марк Райкок

3
У цій настройці я можу змінити значення aService з області застосування. Але сфера не змінюється у відповідь на зміну сервісу.
Увен Хуанг

4
Це також не працює для мене. Просто призначення, $scope.foo = aService.fooне змінює область змінної області автоматично.
Дарвін Тех

9

Ви можете вставити послугу в $ rootScope і спостерігати:

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

У вашому контролері:

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

Іншим варіантом є використання зовнішньої бібліотеки на зразок: https://github.com/melanke/Watch.JS

Працює з: IE 9+, FF 4+, SF 5+, WebKit, CH 7+, OP 12+, BESEN, Node.JS, Rhino 1.7+

Ви можете спостерігати за змінами одного, багатьох або всіх атрибутів об'єкта.

Приклад:

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
Я не можу вірити, що це відповідь - це НЕ НАЙБІЛЬШИЙ ГОЛОС !!! Це найелегантніше рішення (IMO), оскільки воно зменшує інформаційну ентропію та, ймовірно, пом’якшує потребу в додаткових обробниках посередництва. Я би проголосував за це більше, якби міг ...
Коді,

Додавання всіх послуг в $ rootScope, його переваги , і це потенційні проблеми, докладно описані кілька тут: stackoverflow.com/questions/14573023 / ...
Zymotik

6

Ви можете спостерігати за змінами на самому заводі, а потім транслювати зміни

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

Потім у своєму контролері:

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

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

Таким чином, ви ставите весь відповідний заводський код в його опис, тоді ви можете розраховувати на трансляцію лише ззовні


6

== ОНОВЛЕНО ==

Зараз дуже просто в $ watch.

Ручка тут .

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, але як мені потрібно змінити функцію $ watch на контролері, якщо мені потрібно слухати більше однієї змінної?
jedi

Привіт джеді, дякую за голови! Я оновив ручку та відповідь. Я рекомендую додати ще одну функцію годинника для цього. Отже, я додав нову функцію до PostmanService. Сподіваюсь, це допомагає :)
hayatbiralem

Насправді, так це :) Якщо ви поділитесь детальніше про проблему, можливо, я можу вам допомогти.
hayatbiralem

4

Спираючись на відповідь dtheodor, ви можете скористатися чимось подібним до наведеного нижче, щоб не забути скасувати реєстрацію зворотного дзвінка ... Деякі можуть заперечити проти передачі $scopeпослуги в службу.

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 - метод розширення, який виглядає приблизно так:

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

Ось мій загальний підхід.

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

    }]);

У своєму контролері

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

Для таких, як я просто шукаю просте рішення, це майже точно те, що ви очікуєте від використання звичайних $ годинників у контролерах. Єдина відмінність полягає в тому, що він оцінює рядок у своєму javascript-контексті, а не в певній області застосування. Вам доведеться ввести $ rootScope у свою послугу, хоча вона використовується лише для належного підключення до циклів дайджесту.

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

2

стоячи перед дуже подібною проблемою, я переглянув функцію за обсягом і мав функцію повертати службову змінну. Я створив загадку js . ви можете знайти код нижче.

    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

Я прийшов до цього питання, але виявилось, що моя проблема полягала в тому, що я використовував setInterval, коли я мав використовувати провайдер кутових інтервалів $. Це також стосується setTimeout (замість цього використовуйте $ timeout). Я знаю, що це не відповідь на питання ОП, але це може допомогти деяким, як і мені.


Ви можете використовувати setTimeoutабо будь-яку іншу невугольну функцію, але просто не забудьте загорнути код у зворотній дзвінок $scope.$apply().
magnetronnie

2

Я знайшов дійсно чудове рішення для іншої теми з подібною проблемою, але зовсім іншим підходом. Джерело: AngularJS: $ watch у межах директиви не працює при зміні значення $ rootScope

В основному, рішення там говорить НЕ використовувати, $watchоскільки це дуже важке рішення. Замість цього вони пропонують використовувати $emitі $on.

Моя проблема полягала в тому, щоб спостерігати за змінною в моїй службі та реагувати на директиви . А з вищевказаним методом це дуже просто!

Приклад мого модуля / послуги:

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

Тому в основному я дивитися My user - щоразу , коли він встановлений на нове значення I статус.$emituser:change

Тепер у моєму випадку в директиві я використав:

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

Тепер у директиві, яку я слухаю $rootScopeі про дану зміну - я реагую відповідно. Дуже легко і елегантно!


1

// сервіс: (тут нічого особливого)

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

Трохи некрасивий, але я додав реєстрацію змінних сфери в свою службу для перемикання:

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

А потім у контролері:

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

Дякую. Невелике запитання: чому ви повертаєтесь thisіз цієї служби замість цього self?
шрекуу

4
Тому що помилки трапляються іноді. ;-)
nclu

Хороша практика, щоб return this;вийти зі своїх конструкторів ;-)
Коді

1

Погляньте на цей планкер: це найпростіший приклад, про який я міг придумати

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

Тут я бачив деякі жахливі моделі спостерігачів, які спричиняють витік пам'яті у великих програмах.

Я можу трохи запізнитися, але це так просто, як це.

Функція watch спостерігає за змінами посилань (примітивні типи), якщо ви хочете дивитися щось на зразок натискання масиву, просто використовуйте:

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

Це оновить посилання та оновить годинник з будь-якого місця. Включаючи метод отримання послуг. Все, що є примітивом, буде оновлено автоматично.


0

Я запізнююся на цю частину, але я знайшов кращий спосіб зробити це, ніж відповідь, розміщена вище. Замість того, щоб призначити змінну для утримання значення змінної служби, я створив функцію, приєднану до області, що повертає службову змінну.

контролер

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

Я думаю, що це зробить те, що ти хочеш. Мій контролер постійно перевіряє цінність моєї послуги за допомогою цієї реалізації. Чесно кажучи, це набагато простіше, ніж обрана відповідь.


чому це було знижено .. Я також багато разів використовував подібну техніку, і вона спрацювала.
визначено

0

Я написав два простих утиліти, які допомагають мені відстежувати зміни властивостей сервісу.

Якщо ви хочете пропустити довге пояснення, можете перейти до 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;
  };
}

Ця послуга використовується в контролері наступним чином: Припустимо, у вас є служба "testService" із властивостями "prop1", "prop2", "prop3". Ви хочете переглядати та призначати області "prop1" та "prop2". З послугою годинника це буде виглядати так:

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. застосувати Watch obj - це чудово, але його недостатньо, якщо у вашому сервісі є асинхронний код. У цьому випадку я використовую другу утиліту, яка виглядає так:

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

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

Я б запустив його в кінці свого асинхронного коду, щоб запустити цикл перетворення $. Щось схоже на те:

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

Отже, все це буде виглядати так (ви можете запустити його або відкрити скрипку ):

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.