AngularJS: Как посмотреть сервисные переменные?

398

У меня есть сервис, скажем:

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 и всех его свойств.
  • 6
    $ scope.foo = aService.foo достаточно, вы можете потерять строку выше. И то, что он делает внутри $ watch, не имеет смысла, если вы хотите назначить новое значение для $ scope.foo, просто сделайте это ...
Показать ещё 4 комментария
Теги:
angular-services
watch

21 ответ

258

Вы всегда можете использовать старый добрый шаблон наблюдателя, если хотите избежать тирании и накладных расходов $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
};
  • 0
    Как бы вы удалили зарегистрированный обратный вызов после того, как пользователь покинул страницу с помощью FooCtrl?
  • 20
    @ Слушайте событие $destory в области и добавьте незарегистрированный метод в aService
Показать ещё 13 комментариев
221

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

Вместо того, чтобы создавать собственный реестр слушателей (которые нужно очистить при различных $-двоих), вы должны иметь возможность $broadcast от рассматриваемой службы.

Вы должны все-таки кодировать обработчики $on в каждом слушателе, но шаблон отделяется от нескольких вызовов до $digest и, таким образом, избегает риска длительных наблюдателей.

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

** update: примеры **

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

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

 }]);

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

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

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

  • 1
    Выйти из цикла $ digest - это хорошо, особенно если изменения, которые вы наблюдаете, не являются ценностью, которая напрямую и сразу попадает в DOM.
  • 0
    Есть ли, чтобы избежать .save () метод. Выглядит как излишнее, когда вы просто отслеживаете обновление одной переменной в sharedService. Можем ли мы смотреть переменную из sharedService и транслировать, когда она меняется?
Показать ещё 3 комментария
45

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

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.

  • 0
    В чем будет преимущество этого метода по сравнению с трансляцией, описанной ниже Мэттом Пиледжи?
  • 0
    Ну, оба метода имеют свое применение. Преимуществами трансляции для меня будут читабельность и возможность прослушивать больше мест для одного и того же события. Я предполагаю, что основным недостатком является то, что широковещательная рассылка посылает сообщения всем областям-потомкам, поэтому это может быть проблемой производительности.
Показать ещё 3 комментария
38

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

  • 3
    Это отличный подход! Есть ли способ связать только свойство сервиса с областью, а не со всем сервисом? Простое выполнение $scope.count = service.count не работает.
  • 0
    Вы также можете вкладывать свойство внутрь (произвольного) объекта, чтобы оно передавалось по ссылке. $scope.data = service.data <p>Count: {{ data.count }}</p>
Показать ещё 12 комментариев
29

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

Когда выражение angular, такое как тот, который вы использовали, присутствует в HTML, angular автоматически устанавливает $watch для $scope.foo и обновляет HTML при каждом изменении $scope.foo.

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

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

  • aService.foo каждый раз получает значение для нового массива, в результате чего ссылка на него устаревает.
  • aService.foo обновляется таким образом, что цикл $digest не запускается при обновлении.

Проблема 1: Устаревшие ссылки

Учитывая первую возможность, предполагая, что применяется $digest, если aService.foo всегда был одним и тем же массивом, автоматически установленный $watch будет обнаруживать изменения, как показано в нижеприведенном фрагменте кода.

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

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="/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="/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 после каждого Обновить. Но что, если служебные переменные (по какой-либо причине) не обновляются внутри "мира w503". Другими словами, мы не запускаем цикл $digest, который автоматически активируется при изменении свойства службы?


Проблема 2: Отсутствует $digest

Многие из решений здесь помогут решить эту проблему, но я согласен с itcouldevenbeaboat:

Причина, по которой мы используем фреймворк вроде angular, состоит в том, чтобы не создавать собственные шаблоны наблюдателей

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

Решение 2. Используйте сеттер и геттер с помощью $rootScope.$apply()

Я был удивлен, что никто еще не предложил использовать setter и геттер, Эта возможность была внедрена в ECMAScript5 и, таким образом, существует уже много лет. Конечно, это означает, что по какой-то причине вам нужно поддерживать действительно старые браузеры, тогда этот метод не сработает, но я чувствую, что в JavaScript 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="/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>

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

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

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

  • 3
    Это правильное и простое решение. Как говорит @NanoWizard, $ digest отслеживает services не свойства, принадлежащие самой службе.
29

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

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

 }

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

  • 1
    Спасибо за ответ. Если переменная в сервисе является «примитивом», а не объектом, например строкой, потребует ли это $watch ?
  • 35
    Я пытался, и я не мог заставить $watch работать с примитивом. Вместо этого я определил метод в службе, который будет возвращать примитивное значение: somePrimitive() = function() { return somePrimitive } и назначил этому методу свойство $ scope: $scope.somePrimitive = aService.somePrimitive; , Затем я использовал метод области действия в HTML: <span>{{somePrimitive()}}</span>
Показать ещё 11 комментариев
8

Вы можете вставить услугу в $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), поскольку оно уменьшает информационную энтропию и, вероятно, уменьшает потребность в дополнительных обработчиках-посредниках. Я бы проголосовал больше, если бы мог ...
  • 0
    Добавление всех ваших сервисов в $ rootScope, его преимущества и возможные подводные камни подробно описано здесь: stackoverflow.com/questions/14573023/…
6

Вы можете просматривать изменения внутри самого factory и затем транслировать изменение

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

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

  • 0
    чертовски хорошая идея!
5

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

Очень просто сейчас в $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 на контроллере, если мне нужно прослушать более одной переменной?
  • 0
    Привет джедай, спасибо за головы! Я обновил ручку и ответ. Я рекомендую добавить еще одну функцию часов для этого. Итак, я добавил новую функцию в PostmanService. Надеюсь, это поможет :)
Показать ещё 2 комментария
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

Я нашел действительно отличное решение в другом потоке с аналогичной проблемой, но совершенно другой подход. Источник: AngularJS: $watch внутри директивы не работает, когда изменяется значение $rootScope

В основном решение там говорит НЕ TO использовать $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 a user:change.

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

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

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

2

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

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

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

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

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

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

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

function watch(target, callback, deep) {
    $rootScope.$watch(function () {return eval(target);}, callback, deep);
};
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');
}
  • 0
    Благодарю. Небольшой вопрос: почему вы возвращаете this из этого служения вместо self ?
  • 4
    Потому что иногда случаются ошибки. ;-)
Показать ещё 1 комментарий
0

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

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

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

Если вы хотите пропустить длинное объяснение, вы можете перейти к jsfiddle

  • 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 $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. применить Смотреть 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="/jquery.min.js"></script>
<script src="/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>
0

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

контроллер

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

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

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

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

Возможно, я немного опоздаю, но это так просто, как это.

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

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню