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
, я объединил этот шаблон, где я добавляю 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
.
В сервисе:
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
};
Ответ 2
В сценарии, подобном этому, где несколько /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 - лучший вариант, особенно если в области выделения с очень конкретным выражением часов.
Ответ 3
Я использую подобный подход как @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.
Ответ 4
Без часов или обратных вызовов наблюдателя (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' будет ссылаться на объект окна.
Ответ 5
Насколько я могу судить, вам не нужно делать что-то столь же сложное. Вы уже назначили foo из службы в свою область действия, и поскольку foo - это массив (и, в свою очередь, объект назначается ссылкой!). Итак, все, что вам нужно сделать, это что-то вроде этого:
function FooCtrl($scope, aService) {
$scope.foo = aService.foo;
}
Если какая-то другая переменная в этом же Ctrl зависит от изменения foo, то да, вам понадобятся часы для наблюдения foo и внесения изменений в эту переменную. Но до тех пор, пока это простое наблюдение, не нужно. Надеюсь это поможет.
Ответ 6
Я наткнулся на этот вопрос, ища что-то подобное, но я думаю, что он заслуживает подробного объяснения того, что происходит, а также моего решения.
Когда выражение 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="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>
Ответ 7
Вы можете вставить услугу в $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");
Ответ 8
Вы можете просматривать изменения внутри самого 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 в его описание, тогда вы можете полагаться только на трансляцию извне
Ответ 9
== == ОБНОВЛЕНО
Очень просто сейчас в $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 + '!');
});
}]);
Ответ 10
На основе ответа 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;
};
Ответ 11
Вот мой общий подход.
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'];
Ответ 12
столкнувшись с очень похожими проблемами, я просмотрел функцию в области видимости и возвратил функцию в переменную службы. Я создал 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++;
});
});
Ответ 13
Я пришел к этому вопросу, но оказалось, что моя проблема заключалась в том, что я использовал setInterval, когда я должен был использовать поставщик angular $interval. Это также относится к setTimeout (вместо этого используется $timeout). Я знаю, что это не ответ на вопрос ОП, но он может помочь некоторым, поскольку это помогло мне.
Ответ 14
Я нашел действительно отличное решение в другом потоке с аналогичной проблемой, но совершенно другой подход. Источник: 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 данное изменение - я реагирую соответственно. Очень легкий и элегантный!
Ответ 15
Немного уродливый, но я добавил регистрацию переменных области для моей службы для переключения:
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');
}
Ответ 16
Для таких, как я, просто ищет простое решение, это почти то, что вы ожидаете от использования обычных $watch в контроллерах.
Единственное различие заключается в том, что он оценивает строку в контексте javascript, а не в определенной области. Вам нужно будет ввести $rootScope в свою службу, хотя он используется только для правильного подключения к дайджесту.
function watch(target, callback, deep) {
$rootScope.$watch(function () {return eval(target);}, callback, deep);
};
Ответ 17
//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;
});
});
Ответ 18
Я видел здесь несколько ужасных шаблонов наблюдателей, которые вызывают утечку памяти в больших приложениях.
Возможно, я немного опоздаю, но это так просто, как это.
Функция часов отслеживает изменения ссылок (примитивные типы), если вы хотите посмотреть что-то вроде push массива, просто используйте:
someArray.push(someObj); someArray = someArray.splice(0);
Это обновит ссылку и обновит часы из любого места. Включая метод getter услуг.
Все, что примитив будет обновляться автоматически.
Ответ 19
Я опаздываю на эту часть, но я нашел более удобный способ сделать это, чем ответ, опубликованный выше. Вместо того, чтобы назначать переменную для хранения значения служебной переменной, я создал функцию, прикрепленную к области, которая возвращает служебную переменную.
контроллер
$scope.foo = function(){
return aService.foo;
}
Я думаю, что это будет делать то, что вы хотите. Мой контроллер продолжает проверять ценность моего сервиса с этой реализацией. Честно говоря, это намного проще, чем выбранный ответ.
Ответ 20
Я написал две простые служебные службы, которые помогают мне отслеживать изменения свойств службы.
Если вы хотите пропустить длинное объяснение, вы можете перейти к jsfiddle
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;
};
}
Ответ 21
Посмотрите на этот плункер:: это самый простой пример, который я мог бы придумать
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;
});