AngularJS: как смотреть служебные переменные?

У меня есть услуга, говорят:

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 обновляется, я собрал этот шаблон, в котором я добавляю службу к $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;
    }
  });
}

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


person berto    schedule 25.09.2012    source источник
comment
Вы можете передать третий параметр в $ watch со значением true для глубокого просмотра aService и всех его свойств.   -  person SirTophamHatt    schedule 08.10.2013
comment
$ scope.foo = aService.foo достаточно, вы можете потерять строку выше. И то, что он делает внутри $ watch, не имеет смысла, если вы хотите присвоить новое значение $ scope.foo, просто сделайте это ...   -  person Jin    schedule 27.02.2014
comment
Не могли бы вы просто сослаться на aService.foo в разметке html? (например: plnkr.co/edit/aNrw5Wo4Q0IxR2loipl5?p=preview)   -  person thetallweeks    schedule 01.10.2014
comment
Я добавил пример без обратных вызовов или часов, см. Ответ ниже (jsfiddle.net/zymotik/853wvv7s)   -  person Zymotik    schedule 05.03.2015
comment
Это очень интересный вопрос. И я также обнаружил, что могу с радостью привязать элемент управления к aService.foo (и тогда он будет обновлен без каких-либо часов), но если бы мой контроллер использовал $ scope.foo = aService.foo; и я привязался к foo, тогда он никогда не обновлялся. Очень странный. Тем не менее, отличный вопрос.   -  person Mike Gledhill    schedule 19.08.2015
comment
@MikeGledhill, ты прав. Я думаю, это связано с природой Javascript, вы можете увидеть этот шаблон во многих других местах (не только в Angular, но и в JS в целом). С одной стороны, вы передаете значение (и оно не связывается), а с другой стороны, вы передаете объект (или значение, которое ссылается на объект ...), и поэтому свойства обновляются правильно (например, идеально показано в примере Zymotik выше).   -  person Christophe Vidal    schedule 07.12.2015


Ответы (21)


Вы всегда можете использовать старый добрый шаблон наблюдателя, если хотите избежать тирании и накладных расходов $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
};
person dtheodor    schedule 09.07.2013
comment
Как бы вы удалили зарегистрированный обратный вызов после того, как пользователь покинет страницу с помощью FooCtrl? - person niklr; 30.08.2013
comment
@Moo прослушайте событие $destory в области видимости и добавьте метод отмены регистрации в aService - person Jamie; 01.09.2013
comment
Поправьте меня, если я ошибаюсь, но вы должны $ scope. $ Применить изменение в обратном вызове наблюдателя в контроллере. По крайней мере, мне пришлось, потому что все изменения в моем сервисе происходят мгновенно. - person JNissi; 19.12.2013
comment
Какие плюсы у этого решения? Ему требуется больше кода в службе и примерно такое же количество кода в контроллере (поскольку нам также нужно отменить регистрацию на $ destroy). Я мог бы сказать о скорости выполнения, но в большинстве случаев это не имеет значения. - person Alex Che; 12.02.2014
comment
не уверен, что это лучшее решение, чем $ watch, спрашивающий просил простой способ обмена данными, это выглядит еще более громоздким. Я бы предпочел использовать $ broadcast, чем это - person Jin; 27.02.2014
comment
Шаблон $watch против наблюдателя - это просто выбор между опросом или отправкой, и в основном это вопрос производительности, поэтому используйте его, когда производительность имеет значение. Я использую шаблон наблюдателя, когда в противном случае мне пришлось бы внимательно наблюдать за сложными объектами. Я прикрепляю к $ scope целые сервисы вместо того, чтобы смотреть значения отдельных сервисов. Я избегаю angular $ watch, как дьявол, этого достаточно в директивах и в собственной привязке данных angular. - person dtheodor; 27.02.2014
comment
Я бы предложил это решение, даже если производительность не является большой проблемой, потому что поведение явно определено в коде и не зависит от магии библиотеки. Хороший ответ! - person Adam; 11.06.2014
comment
Причина, по которой мы используем такой фреймворк, как Angular, заключается в том, чтобы не придумывать собственные шаблоны наблюдателя. - person Code Whisperer; 15.07.2014
comment
Я не нахожу это слишком удивительным - извиняюсь, если я показался грубым - но это простой getSet подход. Либо используйте Object.defineProperties(object, ...), либо если вы ищете pubSub методологию, вы также можете использовать $rootScope.$on('changed:someData', setSomeData) & $rootScope.$broadcast('changed:someData', someData). ВООБРАЖЕННЫЕ ТОЧКИ ВЫКЛЮЧЕНЫ за этот ответ :( - person Cody; 09.02.2015
comment
У вас должен быть очень хороший пример использования, чтобы оправдать повторную реализацию событий в Angular. - person superluminary; 30.03.2015
comment
Разве мы не упускаем из виду «верни это»; в конце определения услуги? - person Patrik Beck; 07.06.2016
comment
Не очень Angular по сравнению с stackoverflow.com/a/20863597/418150 - person Lee Goddard; 16.06.2016
comment
@CodeWhisperer, зачем нам использовать здесь angulars $ broadcast? Зачем мне транслировать событие, которое, вероятно, предназначено только для одного контроллера, а не для всего приложения? И почему бы нам не использовать хорошо известный шаблон, если он подошел бы к делу? - person Fortuna; 10.01.2017
comment
$ watch и $ rootScope. $ emit довольно дороги, так как они должны пересекать всю область видимости, верно? Поскольку я много работаю с действительно дорогим интерфейсом для ЦП, это важно для меня, и я уже использовал его в версии 1.4. Шаблон наблюдателя по-прежнему является прямым соединением, и ничего не нужно преодолевать, что для высоких n может быть быстрее. Особенно, если слушателей rootScope уже много? Я не тестировал, но я так понимаю. если у вас есть другая информация, просветите меня пожалуйста! - person Manuel Graf; 04.09.2018
comment
За ответ @CodeWhisperer нельзя так проголосовать. Хотя это правда, он полностью игнорирует смысл создания вашего собственного шаблона наблюдателя в сервисе, то есть производительность. Если вы когда-либо использовали наблюдатели в приложении angularjs, вы знаете, как быстро они могут снизить производительность. Такой подход с меньшим весом, безусловно, имеет некоторую применимость. - person Mrl0330; 25.06.2019
comment
Считайте, что $ watch работает с методологией длительного опроса, когда шаблон наблюдателя вызывает метод (ы) только один раз (с правильной реализацией сбора мусора), когда работа выполнена. Я думаю, что с точки зрения производительности, на мой взгляд, паттерн наблюдателя намного превосходит его. - person Oguzhan; 02.10.2020

В подобном сценарии, когда несколько / неизвестных объектов могут быть заинтересованы в изменениях, используйте $rootScope.$broadcast из изменяемого элемента.

Вместо того, чтобы создавать свой собственный реестр слушателей (который нужно очищать при различных $ destroys), вы должны иметь возможность $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 () завершится и данные будут действительными. В качестве альтернативы, если это $ resource или отправка 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 ..
   });

 }]);

Одним из преимуществ этого является устранение нескольких часов. Если вы комбинируете поля или получаете значения, как в приведенном выше примере, вам нужно будет следить за свойствами как firstname, так и lastname. Наблюдение за функцией getUser () будет работать только в том случае, если пользовательский объект был заменен при обновлениях, он не сработает, если пользовательский объект просто обновит свои свойства. В этом случае вам придется проводить более глубокие часы, и это более интенсивно.

$ broadcast отправляет сообщение из вызываемой области в любые дочерние области. Поэтому вызов его из $ rootScope будет срабатывать для каждой области видимости. Например, если бы вы выполняли трансляцию $ из области действия вашего контроллера, она срабатывала бы только в областях, унаследованных от области действия вашего контроллера. $ emit действует в противоположном направлении и ведет себя аналогично событию DOM в том смысле, что всплывает вверх по цепочке областей видимости.

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

person Matt Pileggi    schedule 31.12.2013
comment
Выйти из цикла $ digest - это хорошо, особенно если наблюдаемые вами изменения не являются ценностью, которая сразу же попадет в DOM. - person XML; 28.01.2014
comment
Есть ли способ избежать использования метода .save (). Кажется излишним, когда вы просто отслеживаете обновление одной переменной в sharedService. Можем ли мы наблюдать за переменной из sharedService и транслировать, когда она изменяется? - person JerryKur; 02.05.2014
comment
Я пробовал несколько способов обмена данными между контроллерами, но это единственный, который сработал. Хорошо сыграно, сэр. - person abettermap; 28.02.2015
comment
Я предпочитаю это другим ответам, кажется менее хакерским, спасибо - person JMK; 16.05.2015
comment
Это правильный шаблон проектирования только в том случае, если у вашего потребляющего контроллера есть несколько возможных источников данных; другими словами, если у вас есть ситуация MIMO (множественный ввод / множественный вывод). Если вы просто используете шаблон «один ко многим», вам следует использовать прямую ссылку на объект и позволить фреймворку Angular выполнять двустороннюю привязку за вас. Хоркиз связал это ниже, и это хорошее объяснение автоматической двусторонней привязки и ее ограничений: stsc3000.github.io/blog/2013/10/26/ - person Charles; 04.06.2015

Я использую тот же подход, что и @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.

person Krym    schedule 11.04.2014
comment
В чем будет преимущество этого метода по сравнению с трансляцией $, описанной ниже Мэттом Пиледжи? - person Fabio; 12.09.2014
comment
Что ж, у обоих методов есть свои применения. Для меня преимущества трансляции заключаются в удобочитаемости и возможности слушать одно и то же событие в большем количестве мест. Я предполагаю, что основным недостатком является то, что широковещательная передача отправляет сообщение всем дочерним областям, поэтому это может быть проблемой производительности. - person Krym; 18.09.2014
comment
У меня была проблема, когда выполнение $scope.$watch для служебной переменной, казалось, не работало (область, которую я наблюдал, была модальной, унаследованной от $rootScope) - это сработало. Классный трюк, спасибо, что поделились! - person Seiyria; 29.10.2014
comment
Как бы вы убрали за собой с помощью этого подхода? Можно ли удалить зарегистрированный обратный вызов из обещания, когда область видимости уничтожена? - person Abris; 23.01.2015
comment
Хороший вопрос. Честно говоря, не знаю. Я попытаюсь провести несколько тестов, чтобы узнать, как удалить обратный вызов notify из обещания. - person Krym; 23.01.2015

Без часов или обратных вызовов наблюдателя (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 будет относиться к объекту окна.

person Zymotik    schedule 05.03.2015
comment
Это отличный подход! Есть ли способ привязать к области видимости только свойство службы, а не всю службу? Просто делать $scope.count = service.count не работает. - person JacobF; 23.03.2015
comment
Вы также можете вложить свойство внутри (произвольного) объекта, чтобы оно передавалось по ссылке. $scope.data = service.data <p>Count: {{ data.count }}</p> - person Alex Ross; 26.07.2015
comment
Отличный подход! Несмотря на то, что на этой странице есть много убедительных и функциональных ответов, это, безусловно, а) самый простой в реализации и б) самый простой для понимания при чтении кода. Этот ответ должен быть намного выше, чем сейчас. - person CodeMoose; 14.10.2015
comment
Спасибо @CodeMoose, сегодня я еще больше упростил его для тех, кто плохо знаком с AngularJS / JavaScript. - person Zymotik; 16.10.2015
comment
не могли бы вы рассказать мне, в чем преимущество линии var self = this;? почему бы просто не использовать this - person Afflatus; 27.10.2015
comment
Привет, @Afflatus, self используется для ссылки на оригинал this. См. http://stackoverflow.com/questions/962033/what-underlies-this-javascript-idiom-var-self-this - person Zymotik; 28.10.2015
comment
По-моему, здесь вы путаете людей с self. Нет смысла использовать их для создания функции, потому что this и self относятся к одному и тому же объекту. Имеет смысл использовать self только внутри функции, и опять же, нет необходимости использовать его, потому что вы можете использовать .bind(this), а когда у вас есть случай, когда вы не можете использовать .bind(), только тогда вы должны использовать self. - person sarunast; 25.11.2015
comment
Спасибо за комментарии @Stamy. Не могли бы вы продемонстрировать более простой подход в jsFiddle? Затем мы можем сравнить и обновить пример, если это необходимо. Нам действительно нужна ссылка на исходный this при использовании функции $timeout. - person Zymotik; 30.11.2015
comment
Вот ссылка с кодом: jsbin.com/guwogo/edit?js,console. Я добавил два подхода, меня устраивает любой из них. Но я бы никогда не стал использовать self для создания всего. Было бы неплохо, если бы вы не понимали, как работает this и scopes, но я никогда не видел ничего подобного ни в одной достойной библиотеке. - person sarunast; 30.11.2015
comment
@Zymotik, Значит, вам все равно понадобятся $ watch или $ broadcast, правда, при отображении служебных данных на более чем одном контроллере одновременно? jsfiddle.net/qqejytbz/1 - person NoobSter; 20.01.2017
comment
@NoobSter нет, ты бы не стал - person Zymotik; 21.01.2017
comment
@Zymotik, Спасибо за ответ. Я знаю, что это старое. В моей скрипке, опубликованной выше (разветвленная из вашего примера), счетчик не может обновляться в обоих контроллерах, если я не использую $ watch. Как правильно это сделать? Сценарий: jsfiddle.net/qqejytbz/1 - person NoobSter; 21.01.2017
comment
я переместил это в свой вопрос, если вам интересно: stackoverflow.com/questions/41786796/ - person NoobSter; 22.01.2017
comment
@noobster, пожалуйста, посмотрите первые два комментария, поскольку я считаю, что это отвечает на ваш вопрос. - person Zymotik; 24.01.2017
comment
Да благословит тебя Бог. Я бы сказал, что я потратил впустую миллион часов. Поскольку я боролся с переходом 1.5 и angularjs с 1 на 2, а также хотел поделиться данными - person Amna; 27.06.2019

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

Когда выражение angular, такое как то, которое вы использовали, присутствует в 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, если aService.foo всегда был одним и тем же массивом, автоматически установленный $watch обнаружит изменения, как показано в фрагменте кода ниже.

Решение 1-а: убедитесь, что массив или объект является одним и тем же объектом при каждом обновлении.

angular.module('myApp', [])
  .factory('aService', [
    '$interval',
    function($interval) {
      var service = {
        foo: []
      };

      // Create a new array on each update, appending the previous items and 
      // adding one new item each time
      $interval(function() {
        if (service.foo.length < 10) {
          var newArray = []
          Array.prototype.push.apply(newArray, service.foo);
          newArray.push(Math.random());
          service.foo = newArray;
        }
      }, 1000);

      return service;
    }
  ])
  .factory('aService2', [
    '$interval',
    function($interval) {
      var service = {
        foo: []
      };

      // Keep the same array, just add new items on each update
      $interval(function() {
        if (service.foo.length < 10) {
          service.foo.push(Math.random());
        }
      }, 1000);

      return service;
    }
  ])
  .controller('FooCtrl', [
    '$scope',
    'aService',
    'aService2',
    function FooCtrl($scope, aService, aService2) {
      $scope.foo = aService.foo;
      $scope.foo2 = aService2.foo;
    }
  ]);
<!DOCTYPE html>
<html>

<head>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
  <link rel="stylesheet" href="style.css" />
  <script src="script.js"></script>
</head>

<body ng-app="myApp">
  <div ng-controller="FooCtrl">
    <h1>Array changes on each update</h1>
    <div ng-repeat="item in foo">{{ item }}</div>
    <h1>Array is the same on each udpate</h1>
    <div ng-repeat="item in foo2">{{ item }}</div>
  </div>
</body>

</html>

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

Однако, хотя есть несколько способов убедиться, что исходная ссылка остается неизменной, иногда может потребоваться изменить объект или массив. Или что, если свойство службы ссылается на примитив, такой как 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;
}

angular.module('myApp', [])
  .factory('aService', [
    '$interval',
    function($interval) {
      var service = {
        foo: []
      };

      // Create a new array on each update, appending the previous items and 
      // adding one new item each time
      $interval(function() {
        if (service.foo.length < 10) {
          var newArray = []
          Array.prototype.push.apply(newArray, service.foo);
          newArray.push(Math.random());
          service.foo = newArray;
        }
      }, 1000);

      return service;
    }
  ])
  .controller('FooCtrl', [
    '$scope',
    'aService',
    function FooCtrl($scope, aService) {
      $scope.aService = aService;
    }
  ]);
<!DOCTYPE html>
<html>

<head>
  <script data-require="[email protected]" data-semver="1.4.7" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.js"></script>
  <link rel="stylesheet" href="style.css" />
  <script src="script.js"></script>
</head>

<body ng-app="myApp">
  <div ng-controller="FooCtrl">
    <h1>Array changes on each update</h1>
    <div ng-repeat="item in aService.foo">{{ item }}</div>
  </div>
</body>

</html>

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

Это то, что вы пытались сделать со своим обходным путем, но гораздо менее обходным способом. Вы добавили ненужный $watch в контроллер, который явно помещает foo в $scope при каждом изменении. Вам не понадобится этот дополнительный $watch, когда вы прикрепляете aService вместо aService.foo к $scope и явно привязываете к aService.foo в разметке.


Теперь все хорошо при условии, что применяется $digest цикл. В моих примерах выше я использовал службу Angular $interval для обновления массивов, который автоматически запускает цикл $digest после каждого обновления. Но что, если служебные переменные (по какой-либо причине) не обновляются в «мире Angular». Другими словами, у нас нет автоматической активации $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;
      }
    };
  // ...
}

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

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

      // Create a new array on each update, appending the previous items and 
      // adding one new item each time
      setInterval(function() {
        if (service.foo.length < 10) {
          var newArray = [];
          Array.prototype.push.apply(newArray, service.foo);
          newArray.push(Math.random());
          service.foo = newArray;
        }
      }, 1000);

      return service;
    }
  ])
  .controller('FooCtrl', [
    '$scope',
    'aService',
    function FooCtrl($scope, aService) {
      $scope.aService = aService;
    }
  ]);
<!DOCTYPE html>
<html>

<head>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
  <link rel="stylesheet" href="style.css" />
  <script src="script.js"></script>
</head>

<body ng-app="myApp">
  <div ng-controller="FooCtrl">
    <h1>Using a Getter/Setter</h1>
    <div ng-repeat="item in aService.foo">{{ item }}</div>
  </div>
</body>

</html>

Здесь я добавил в служебную функцию «частную» переменную: realFoo. Это get обновляется и извлекается с помощью функций get foo() и set foo() соответственно для объекта service.

Обратите внимание на использование $rootScope.$apply() в функции set. Это гарантирует, что Angular будет знать обо всех изменениях в service.foo. Если вы получаете сообщения об ошибках inprog, перейдите на эту полезную справочную страницу, или если вы используете Angular> = 1.3 вы можете просто использовать $rootScope.$applyAsync().

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

person NanoWizard    schedule 11.11.2015
comment
Это правильное и простое решение. Как говорит @NanoWizard, $ digest следит за services, а не за свойствами, принадлежащими самой службе. - person Sarpdoruk Tahmaz; 03.12.2015

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

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

 }

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

person ganaraj    schedule 25.09.2012
comment
Спасибо за ответ. Если переменная в службе является примитивом, а не объектом, например строкой, потребуется ли это, в свою очередь, $watch? - person berto; 25.09.2012
comment
Я попробовал, но не смог $watch работать с примитивом. Вместо этого я определил для службы метод, который будет возвращать примитивное значение: somePrimitive() = function() { return somePrimitive } И я назначил этому методу свойство $ scope: $scope.somePrimitive = aService.somePrimitive;. Затем я использовал метод области в HTML: <span>{{somePrimitive()}}</span> - person Mark Rajcok; 03.10.2012
comment
@MarkRajcok Нет, не используйте примитивы. Добавьте их в объект. Примитивы не являются изменяемыми, поэтому двусторонняя привязка данных работать не будет. - person Jimmy Kane; 02.09.2013
comment
@JimmyKane, да, примитивы не должны использоваться для двусторонней привязки данных, но я думаю, что вопрос касался наблюдения за служебными переменными, а не настройки двусторонней привязки. Если вам нужно только следить за свойством / переменной службы, объект не требуется - можно использовать примитив. - person Mark Rajcok; 03.09.2013
comment
В этой настройке я могу изменить значения aService из области видимости. Но объем не меняется в ответ на изменение aService. - person Ouwen Huang; 16.12.2014
comment
У меня это тоже не работает. Простое присвоение $scope.foo = aService.foo не приводит к автоматическому обновлению переменной области видимости. - person Darwin Tech; 22.01.2015
comment
Правильно, вы не можете назначить переменную области из переменной службы, а затем $ watch ('foo', ... Я использую angular 1.3 для тестирования, и часы не запускаются при изменении переменной службы - person lostintranslation; 07.02.2015
comment
Я пробовал несколько раз раньше ... Я знал, что есть лучшее, чем использование прицела, но у меня не получилось, когда я попробовал. Этим вечером я, наконец, сделал это, так здорово :) !!! Вы, сэр, заслуживаете большего количества голосов! - person maxime1992; 17.03.2015
comment
@lostintranslation каким было ваше решение для 1.3? - person ceebreenk; 23.03.2016
comment
Я не могу понять голосов, так как это не работает. Даже если в сервисе у меня есть объект, его значение не обновляется автоматически в контроллере. - person João Pereira; 09.02.2017
comment
И все это из фреймворка, который должен был облегчить нашу жизнь! Неудивительно, что они снова начали! РЖУ НЕ МОГУ. - person Norbert Norbertson; 30.11.2017

Вы можете вставить сервис в $ 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");​
person Rodolfo Jorge Nemer Nogueira    schedule 13.09.2014
comment
Я НЕ ВЕРАЮ, ЭТОТ ОТВЕТ НЕ НАИЛУЧШИМ ГОЛОСОВАННЫМ !!! Это наиболее элегантное решение (IMO), поскольку оно снижает информационную энтропию и, вероятно, снижает потребность в дополнительных обработчиках посредничества. Я бы проголосовал за это больше, если бы мог ... - person Cody; 09.02.2015
comment
Добавление всех ваших сервисов в $ rootScope, его преимущества и потенциальные ловушки подробно описаны здесь: stackoverflow.com/questions/14573023/ - person Zymotik; 10.03.2015

Вы можете наблюдать за изменениями на самом заводе, а затем транслировать изменение

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

Таким образом, вы помещаете весь связанный заводской код в его описание, тогда вы можете полагаться только на трансляцию извне.

person Flavien Volken    schedule 24.11.2014

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

Теперь очень просто в $ 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 + '!');
  });
}]);
person hayatbiralem    schedule 23.10.2015
comment
Мне нравится PostmanService, но как мне изменить функцию $ watch на контроллере, если мне нужно прослушивать более одной переменной? - person jedi; 19.07.2016
comment
Привет, джедаи, спасибо за внимание! Обновил ручку и ответ. Я рекомендую добавить для этого еще одну функцию часов. Итак, я добавил новую функцию в PostmanService. Надеюсь, это поможет :) - person hayatbiralem; 23.07.2016
comment
На самом деле, да, это так :) Если вы расскажете подробнее о проблеме, возможно, я смогу вам помочь. - person hayatbiralem; 26.07.2016

Основываясь на ответе 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;
};
person Jamie    schedule 02.09.2013

Вот мой общий подход.

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'];
person elitechance    schedule 17.09.2013

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

function watch(target, callback, deep) {
    $rootScope.$watch(function () {return eval(target);}, callback, deep);
};
person Justus Wingert    schedule 29.09.2014

столкнувшись с очень похожей проблемой, я наблюдал за функцией в области видимости, и функция возвращала служебную переменную. Я создал скрипт 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++;
    });
});
person chandings    schedule 29.12.2014

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

person honkskillet    schedule 18.02.2015
comment
Вы можете использовать setTimeout или любую другую функцию, отличную от Angular, но только не забудьте обернуть код в обратном вызове с помощью $scope.$apply(). - person magnetronnie; 06.11.2015

Я нашел действительно отличное решение в другом потоке с аналогичной проблемой, но совершенно другим подходом. Источник: 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;
        },
        ...
    };
});

Так что в основном я слежу за своим user - всякий раз, когда он устанавливается на новое значение, я $emit user:change статус.

В моем случае в директиве я использовал:

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

Теперь в директиве я прослушиваю $rootScope и on данное изменение - я реагирую соответственно. Очень просто и элегантно!

person Atais    schedule 23.07.2015

// сервис: (здесь ничего особенного)

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;
  });
});
person Nawlbergs    schedule 13.05.2016

Немного уродливо, но я добавил регистрацию переменных области в свой сервис для переключения:

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');
}
person nclu    schedule 06.06.2013
comment
Спасибо. Небольшой вопрос: почему вы возвращаете this из этой службы вместо self? - person shrekuu; 12.11.2014
comment
Потому что иногда допускаются ошибки. ;-) - person nclu; 12.11.2014
comment
Хорошая практика return this; из ваших конструкторов ;-) - person Cody; 09.02.2015

Взгляните на этот плункер :: это самый простой пример, который я мог придумать

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;
});
person anandharshan    schedule 07.09.2016

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

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

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

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

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

person Oliver Dixon    schedule 26.12.2015

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

контроллер

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

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

person alaboudi    schedule 18.08.2016
comment
почему он был отвергнут ... Я также много раз использовал подобную технику, и она сработала. - person undefined; 07.03.2018

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

Если вы хотите пропустить длинное объяснение, вы можете сразу перейти к 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>

person Max    schedule 25.08.2016