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

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' будет ссылаться на объект окна.


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обновляется очень часто, так как это может значительно повлиять на производительность. Если производительность будет проблемой, вы можете установить шаблон наблюдателя, подобный другим ответам здесь, используя установщик.


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 Нет, не используйте примитивы. Добавьте их в объект. Примитивы не являются изменяемыми, поэтому 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 + '!');
  });
}]);

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'];

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

2

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


Вы можете использовать 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и на данное изменение - реагирую соответственно. Очень легко и элегантно!


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

0

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

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

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.