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
$ scope.foo = aService.foo достаточно, вы можете потерять строку выше. И то, что он делает внутри $ watch, не имеет смысла, если вы хотите назначить новое значение для $ scope.foo, просто сделайте это ...
Jin
4
Не могли бы вы просто сослаться aService.fooна html-разметку? (например: plnkr.co/edit/aNrw5Wo4Q0IxR2loipl5?p=preview )
thetallweeks
1
Я добавил пример без Callbacks или $ watches, смотрите ответ ниже ( jsfiddle.net/zymotik/853wvv7s )
Zymotik
1
@MikeGledhill, ты прав. Я думаю, что это связано с природой Javascript, вы можете увидеть эту модель во многих других местах (не только в Angular, но и в JS вообще). С одной стороны, вы передаете значение (и оно не становится связанным), а с другой стороны, вы передаете объект (или значение, которое ссылается на объект ...), и поэтому свойства корректно обновляются (например, идеально). показано на примере Zymotik выше).
Кристоф Видаль

Ответы:

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
};
dtheodor
источник
21
@ Слушайте $destoryсобытие в области и добавьте незарегистрированный метод кaService
Джейми,
13
Каковы плюсы этого решения? Для него требуется больше кода в службе и примерно такой же объем кода в контроллере (поскольку нам также необходимо отменить регистрацию в $ destroy). Я могу сказать о скорости выполнения, но в большинстве случаев это просто не имеет значения.
Алекс Че
6
Не уверен, что это лучшее решение, чем $ watch, спрашивающий спрашивал о простом способе обмена данными, это выглядит еще более громоздким. Я предпочел бы использовать трансляцию $, чем это
Jin
11
$watchСравнение с моделью наблюдателя - это просто выбор: опросить или нажать, и это в основном вопрос производительности, поэтому используйте его, когда важна производительность. Я использую шаблон наблюдателя, когда в противном случае мне пришлось бы «глубоко» наблюдать за сложными объектами. Я присоединяю целые сервисы к области $ вместо просмотра отдельных значений сервисов. Я избегаю $ watch в Angular, как дьявол, этого достаточно в директивах и в привязке данных к angular.
dtheodor
107
Причина, по которой мы используем фреймворк, такой как Angular, заключается в том, чтобы не создавать собственные шаблоны наблюдателей.
Код Whisperer
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;
}]);

Приведенный выше сервис будет транслировать сообщение во все области, когда функция save () завершится и данные будут действительными. В качестве альтернативы, если это $ ресурс или ajax-представление, переместите широковещательный вызов в обратный вызов, чтобы он срабатывал при ответе сервера. Широковещательные рассылки особенно подходят этому шаблону, потому что каждый слушатель просто ожидает события без необходимости проверять область действия каждого отдельного $ digest. Слушатель будет выглядеть так:

.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 в том смысле, что оно всплывает в цепочку областей видимости.

Имейте в виду, что есть сценарии, в которых $ broadcast имеет большой смысл, и есть сценарии, в которых $ watch является лучшим вариантом, особенно если он находится в изолированной области с очень конкретным выражением наблюдения.

Мэтт Пиледжи
источник
1
Выйти из цикла $ digest - это хорошо, особенно если изменения, которые вы наблюдаете, не являются ценностью, которая напрямую и сразу попадает в DOM.
XML
Есть ли, чтобы избежать .save () метод. Выглядит как излишнее, когда вы просто отслеживаете обновление одной переменной в sharedService. Можем ли мы смотреть переменную из sharedService и транслировать, когда она меняется?
JerryKur
Я попробовал несколько способов обмена данными между контроллерами, но это единственный, который сработал. Хорошо сыграно, сэр.
abettermap
Я предпочитаю это другим ответам, кажется менее хакерским , спасибо
JMK
9
Это правильный шаблон проектирования, только если ваш потребительский контроллер имеет несколько возможных источников данных; другими словами, если у вас есть ситуация MIMO (множественный вход / множественный выход). Если вы просто используете шаблон «один ко многим», вы должны использовать прямую ссылку на объект и позволить платформе Angular выполнить двустороннюю привязку за вас. Хоркизе связал это ниже, и это хорошее объяснение автоматической двусторонней привязки и ее ограничений: stsc3000.github.io/blog/2013/10/26/…
Charles
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) - это работало. Классный трюк, спасибо, что поделились!
Сейрия
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' будет ссылаться на объект окна.

Zymotik
источник
3
Это отличный подход! Есть ли способ привязать только свойство службы к области действия вместо всей службы? Просто делать $scope.count = service.countне работает.
jvannistelrooy
Вы также можете вкладывать свойство внутрь (произвольного) объекта, чтобы оно передавалось по ссылке. $scope.data = service.data <p>Count: {{ data.count }}</p>
Алекс Росс
1
Отличный подход! Хотя на этой странице много сильных, функциональных ответов, это, безусловно, а) самый простой для реализации и б) самый простой для понимания при чтении код. Этот ответ должен быть намного выше, чем сейчас.
CodeMoose
Спасибо @CodeMoose, сегодня я еще больше упростила это для новичков в AngularJS / JavaScript.
Zymotik
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: убедитесь, что массив или объект является одним и тем же объектом при каждом обновлении

Как вы можете видеть, ng-repeat, предположительно прикрепленный к aService.foo, не обновляется при aService.fooизменениях, но ng-repeat, прикрепленный к, aService2.foo делает . Это потому, что наша ссылка на aService.fooустарела, а наша ссылка на aService2.fooнет. Мы создали ссылку на исходный массив with $scope.foo = aService.foo;, который затем был отброшен службой при следующем обновлении, что означает, что мы $scope.fooбольше не ссылаемся на массив, который нам нужен.

Однако, хотя есть несколько способов убедиться, что исходная ссылка сохраняется в такте, иногда может потребоваться изменить объект или массив. Или что, если свойство service ссылается на примитив, такой как Stringили Number? В этих случаях мы не можем просто полагаться на ссылку. Так что можно делать?

Несколько ответов, приведенных ранее, уже дают некоторые решения этой проблемы. Тем не менее, я лично за использование простого метода, предложенного Jin и 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()в функции set. Это гарантирует, что Angular будет в курсе любых изменений service.foo. Если вы получаете ошибки 'inprog', посмотрите эту полезную справочную страницу , или если вы используете Angular> = 1.3, вы можете просто использовать $rootScope.$applyAsync().

Также будьте осторожны с этим, если aService.fooобновляется очень часто, так как это может значительно повлиять на производительность. Если производительность будет проблемой, вы можете установить шаблон наблюдателя, подобный другим ответам здесь, используя установщик.

NanoWizard
источник
3
Это правильное и простое решение. Как говорит @NanoWizard, $ digest следит servicesне за свойствами, которые принадлежат самой службе.
Сарпдорук Тахмаз
28

Насколько я могу сказать, вам не нужно делать что-то настолько сложное, как это. Вы уже присвоили foo из службы своей области видимости, и поскольку foo является массивом (и, в свою очередь, объектом, он назначается по ссылке!). Итак, все, что вам нужно сделать, это что-то вроде этого:

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

 }

Если какая-то другая переменная в этом же Ctrl зависит от изменения foo, тогда да, вам понадобятся часы для наблюдения за foo и внесения изменений в эту переменную. Но пока это просто просмотр ссылок не требуется. Надеюсь это поможет.

ganaraj
источник
35
Я пытался, и я не мог $watchработать с примитивом. Вместо этого, я определил метод на службу , которая будет возвращать элементарное значение: somePrimitive() = function() { return somePrimitive }И я присвоил свойство $ возможности для этого метода: $scope.somePrimitive = aService.somePrimitive;. Затем я использовал метод контекста в HTML: <span>{{somePrimitive()}}</span>
Марк Райкок,
4
@MarkRajcok Нет, не используйте примитивы. Добавьте их в объект. Примитивы не являются изменяемыми, поэтому 2way-связывание данных не будет работать
Джимми Кейн,
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 + '!');
  });
}]);
hayatbiralem
источник
1
Мне нравится PostmanService, но как мне изменить функцию $ watch на контроллере, если мне нужно прослушать более одной переменной?
Джедай
Привет джедай, спасибо за головы! Я обновил ручку и ответ. Я рекомендую добавить еще одну функцию часов для этого. Итак, я добавил новую функцию в PostmanService. Надеюсь это поможет :)
хаятбиралем
На самом деле, да, это так :) Если вы поделитесь более подробной информацией о проблеме, может быть, я могу помочь вам.
хаятбиралем
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'];
elitechance
источник
2

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

function watch(target, callback, deep) {
    $rootScope.$watch(function () {return eval(target);}, callback, deep);
};
Юстус Вингерт
источник
2

Столкнувшись с очень похожей проблемой, я посмотрел функцию в области видимости и заставил функцию возвращать переменную службы. Я создал JS Fiddle . Вы можете найти код ниже.

    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++;
    });
});
chandings
источник
2

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

honkskillet
источник
Вы можете использовать setTimeoutили любую другую неангулярную функцию, но только не забудьте обернуть код в обратный вызов $scope.$apply().
Магнетронние
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и на данное изменение - реагирую соответственно. Очень легко и элегантно!

Atais
источник
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;
  });
});
Nawlbergs
источник
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');
}
nclu
источник
Спасибо. Небольшой вопрос: почему вы возвращаетесь thisс этого сервиса вместо self?
shrekuu
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;
});
anandharshan
источник
0

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

Я мог бы немного опоздать, но это так просто.

Функция наблюдения отслеживает изменения ссылок (примитивные типы), если вы хотите наблюдать что-то вроде массива push, просто используйте:

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

Это обновит ссылку и обновит часы из любой точки мира. Включая метод получения услуг. Все, что является примитивом, будет обновлено автоматически.

Оливер Диксон
источник
0

Я опоздал на часть, но я нашел более хороший способ сделать это, чем ответ, опубликованный выше. Вместо того, чтобы назначать переменную для хранения значения служебной переменной, я создал функцию, прикрепленную к области, которая возвращает служебную переменную.

контроллер

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

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

alaboudi
источник
почему это было отклонено .. Я также использовал подобную технику много раз, и это работало.
не определено
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. apply Watch obj - это прекрасно, но этого недостаточно, если у вас есть асинхронный код в вашем сервисе. Для этого случая я использую вторую утилиту, которая выглядит так:

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

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

Я бы запустил его в конце моего асинхронного кода, чтобы запустить цикл $ digest. Как это:

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>

Максимум
источник