Является ли привязка объектов к angular $rootScope в службе плохой?
В angular у меня есть объект, который будет отображаться через мое приложение через службу.
Некоторые из полей этого объекта являются динамическими и будут обновляться как обычно с помощью привязок в контроллерах, которые используют эту службу. Но некоторые из полей являются вычисляемыми свойствами, которые зависят от других полей и требуют динамического обновления.
Вот простой пример (который работает над jsbin здесь). Моя модель обслуживания предоставляет поля a
, b
и c
, где c
вычисляется из a + B
в calcC()
. Обратите внимание, что в моем реальном приложении вычисления намного сложнее, но суть здесь.
Единственный способ, с помощью которого я могу заставить это работать, - привязать мою модель обслуживания к $rootScope
, а затем использовать $rootScope.$watch
для просмотра любого из контроллеров, изменяющих a
или b
, и когда они делают, пересчитывая c
. Но это кажется уродливым. Есть ли лучший способ сделать это?
Вторая проблема - производительность. В моем полном приложении a
и b
представлены большие списки объектов, которые объединяются до c
. Это означает, что функции $rootScope.$watch
будут выполнять много глубоких проверок массивов, что звучит так, что это повредит производительности.
У меня есть все, что работает с вычисленным подходом в BackBone, который максимально сокращает перерасчет, но angular, похоже, не очень хорошо работает с запланированным подходом. Любые мысли об этом тоже будут хороши.
Вот пример приложения.
var myModule = angular.module('myModule', []);
//A service providing a model available to multiple controllers
myModule.factory('aModel', function($rootScope) {
var myModel = {
a: 10,
b: 10,
c: null
};
//compute c from a and b
calcC = function() {
myModel.c = parseInt(myModel.a, 10) * parseInt(myModel.b, 10);
};
$rootScope.myModel = myModel;
$rootScope.$watch('myModel.a', calcC);
$rootScope.$watch('myModel.b', calcC);
return myModel;
});
myModule.controller('oneCtrl', function($scope, aModel) {
$scope.aModel = aModel;
});
myModule.controller('twoCtrl', function($scope, aModel) {
$scope.anotherModel = aModel;
});
Ответы
Ответ 1
Хотя с высокого уровня я согласен с ответом на bmleite (существует $rootScope, и использование $watch, похоже, будет работать для вашего случая использования), я хочу предложить альтернативный подход.
Используйте $rootScope.$broadcast
для изменения настроек прослушивателя $rootScope.$on
, который затем пересчитает значение c
.
Это можно сделать вручную - т.е. когда вы будете активно изменять значения a
или b
или, возможно, даже в короткий промежуток времени, чтобы уменьшить частоту обновлений. Еще одним шагом будет создание "грязного" флага на вашем сервисе, так что c
будет вычисляться только при необходимости.
Очевидно, что такой подход означает гораздо большее участие в пересчете в ваших контроллерах, директивах и т.д., но если вы не хотите привязывать обновление ко всем возможным изменениям a
или b
, проблема становится вопросом "где рисовать линию".
Ответ 2
Я должен признать, что в первый раз, когда я прочитал ваш вопрос и увидел ваш пример, я подумал: "Это просто неправильно", однако, просмотрев его снова, я понял, что это было не так плохо, поскольку я думал, что это будет,
Посмотрим правде в глаза, $rootScope
должен использоваться, если вы хотите поделиться чем-то в приложении, что идеально подходит для его создания. Конечно, вам нужно будет осторожно, это то, что разделяется между всеми областями, чтобы вы не захотели случайно изменить его. Но пусть сталкивается с этим, это не настоящая проблема, вы уже должны быть осторожны при использовании вложенных контроллеров (поскольку дочерние области наследуют свойства родительской области) и неизолированные рамки. "Проблема" уже существует, и мы не должны использовать ее в качестве оправдания, а не следовать этому подходу.
Использование $watch
также кажется хорошей идеей. Это то, что инфраструктура уже предоставляет вам бесплатно и делает именно то, что вам нужно. Итак, зачем изобретать колесо? Идея в основном такая же, как и подход "изменения".
На уровне производительности ваш подход может быть фактически "тяжелым", однако он всегда будет зависеть от частоты, которую вы обновляете свойствами a
и b
. Например, если вы установите a
или b
как ng-model
в поле ввода (например, на примере jsbin), c
будет перерасчитываться каждый раз, когда пользователь будет что-то набирать... что явно -переработка. Если вы используете мягкий подход и обновляете a
и/или b
исключительно, когда это необходимо, тогда у вас не должно быть проблем с производительностью. Это будет то же самое, что и пересчет c
с использованием событий "change" или подхода setter & getter. Однако, если вам действительно нужно перерасчитать c
в режиме реального времени (то есть: во время ввода пользователем), проблема с производительностью всегда будет там, и это не факт, что вы используете $rootScope
или $watch
, что поможет улучшить его.
Возобновление, , на мой взгляд, ваш подход не плох (вообще!), просто будьте осторожны с свойствами $rootScope
и избегайте обработки времени.
Ответ 3
Я понимаю, что через полтора года, но с тех пор, как я недавно принял такое же решение, я подумал, что предлагаю альтернативный ответ, который "работал на меня" без загрязнения $rootScope
любым новым значения.
Он все равно полагается на $rootScope
. Однако, вместо того, чтобы передавать сообщения, он просто вызывает $rootScope.$digest
.
Основной подход - предоставить единый объект комплексной модели как поле в вашей службе angular. Вы можете предоставить больше, чем считаете нужным, просто следуйте одному и тому же базовому подходу и убедитесь, что в каждом поле размещен сложный объект, ссылка на который не изменяется, т.е. Не переназначает поле новым сложным объектом. Вместо этого измените только поля этого объекта модели.
var myModule = angular.module('myModule', []);
//A service providing a model available to multiple controllers
myModule.service('aModel', function($rootScope, $timeout) {
var myModel = {
a: 10,
b: 10,
c: null
};
//compute c from a and b
calcC = function() {
myModel.c = parseInt(myModel.a, 10) * parseInt(myModel.b, 10);
};
calcC();
this.myModel = myModel;
// simulate an asynchronous method that frequently changes the value of myModel. Note that
// not appending false to the end of the $timeout would simply call $digest on $rootScope anyway
// but we want to explicitly not do this for the example, since most asynchronous processes wouldn't
// be operating in the context of a $digest or $apply call.
var delay = 2000; // 2 second delay
var func = function() {
myModel.a = myModel.a + 10;
myModel.b = myModel.b + 5;
calcC();
$rootScope.$digest();
$timeout(func, delay, false);
};
$timeout(func, delay, false);
});
Контроллеры, которые хотят зависеть от модели обслуживания, затем могут свободно вводить модель в свою область. Например:
$scope.theServiceModel = aModel.myModel;
И привязать непосредственно к полям:
<div>A: {{theServiceModel.a}}</div>
<div>B: {{theServiceModel.b}}</div>
<div>C: {{theServiceModel.c}}</div>
И все будет автоматически обновляться всякий раз, когда значения обновляются в службе.
Обратите внимание, что это будет работать, только если вы введете типы, наследующие объект (например, массив, пользовательские объекты) непосредственно в область видимости. Если вы вводите примитивные значения, такие как строка или число, непосредственно в область видимости (например, $scope.a = aModel.myModel.a
), вы получите копию, помещенную в область видимости, и таким образом никогда не получите новое значение при обновлении. Как правило, лучше всего просто вставить весь объект модели в область видимости, как это было в моем примере.
Ответ 4
В общем, это, вероятно, не очень хорошая идея. Это также (в общем) плохая практика, чтобы разоблачить реализацию модели для всех ее абонентов, если только по какой-либо другой причине рефакторинг становится более сложным и обременительным. Мы можем легко решить оба:
myModule.factory( 'aModel', function () {
var myModel = { a: 10, b: 10 };
return {
get_a: function () { return myModel.a; },
get_b: function () { return myModel.a; },
get_c: function () { return myModel.a + myModel.b; }
};
});
Это подход наилучшей практики. Он хорошо масштабируется, вызывается только тогда, когда он нужен, и не загрязняет $rootScope
.
PS: Вы также можете обновить c
, если для параметра a
или b
установлено значение, чтобы избежать повторного вызова в каждом вызове get_c
; который лучше всего зависит от ваших деталей реализации.
Ответ 5
Из того, что я вижу в вашей структуре, имея a
и b
, поскольку геттеры могут быть не очень хорошей идеей, но c
должна быть функцией...
Поэтому я мог бы предложить
myModule.factory( 'aModel', function () {
return {
a: 10,
b: 10,
c: function () { return this.a + this.b; }
};
});
При таком подходе вы не можете использовать двухстороннюю привязку c
к входной переменной.. Но двухсторонняя привязка c
не имеет никакого смысла, потому что если вы установите значение c
, как бы вы разделились значение между a
и b
?