javascript - injection - ¿Los objetos vinculantes de $ rootScope de angular en un servicio son malos?
angular js (5)
En angular, tengo un objeto que se expondrá a través de mi aplicación a través de un servicio.
Algunos de los campos en ese objeto son dinámicos y se actualizarán de forma normal mediante enlaces en los controladores que usan el servicio. Pero algunos de los campos son propiedades calculadas, que dependen de otros campos y deben actualizarse dinámicamente.
Aquí hay un ejemplo simple (que está trabajando en jsbin here ). Mi modelo de servicio expone los campos a
, c
donde c
se calcula a partir de a + B
en calcC()
. Tenga en cuenta que en mi aplicación real los cálculos son mucho más complejos, pero la esencia está aquí.
La única forma en que puedo pensar para hacer que esto funcione es vincular mi modelo de servicio al $rootScope
y luego usar $rootScope.$watch
para observar si alguno de los controladores cambia b
o si lo hace, recalculando c
. Pero eso parece feo. ¿Hay una mejor manera de hacer esto?
Una segunda preocupación es el rendimiento. En mi aplicación completa, a
y b
son grandes listas de objetos, que se agregan hasta c
. Esto significa que las $rootScope.$watch
harán una gran cantidad de comprobaciones profundas, lo que parece perjudicar el rendimiento.
Tengo todo esto trabajando con un enfoque renovado en BackBone, que reduce el recálculo tanto como sea posible, pero angular parece no funcionar bien con un enfoque deventa. Cualquier idea sobre eso sería genial también.
Aquí está la aplicación de ejemplo.
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;
});
Aunque desde un alto nivel, estoy de acuerdo con la respuesta de bmleite ($ rootScope existe para ser utilizado, y el uso de $ watch parece funcionar para su caso de uso), quiero proponer un enfoque alternativo.
Utilice $rootScope.$broadcast
para enviar cambios a un $rootScope.$on
oyente, que luego recalculará su valor de c
.
Esto podría hacerse manualmente, es decir, cuando estaría cambiando activamente los valores b
, o posiblemente incluso con un tiempo de espera corto para reducir la frecuencia de las actualizaciones. Un paso más allá sería crear una bandera "sucia" en su servicio, de modo que c
solo se calcule cuando sea necesario.
Obviamente, tal enfoque implica una mayor participación en el recálculo en sus controladores, directivas, etc., pero si no desea vincular una actualización a cada cambio posible de b
, el problema se convierte en una cuestión de "dónde trazar la línea". ''.
Debo admitir que la primera vez que leí tu pregunta y vi tu ejemplo pensé "esto está simplemente mal" , sin embargo, después de analizarlo nuevamente me di cuenta de que no era tan malo como pensaba.
Afrontémoslos, $rootScope
está ahí para ser usado, si quieres compartir algo en toda la aplicación, ese es el lugar perfecto para ponerlo. Por supuesto, tendrá que tener cuidado, es algo que se comparte entre todos los ámbitos, por lo que no quiere cambiarlo inadvertidamente. Pero seamos sinceros, ese no es el problema real, ya debe tener cuidado al usar controladores anidados (porque los ámbitos secundarios heredan las propiedades del alcance principal) y las directivas de alcance no aislado. El ''problema'' ya está allí y no deberíamos usarlo como excusa para no seguir este enfoque.
Usar $watch
también parece ser una buena idea. Es algo que el marco ya le proporciona de forma gratuita y hace exactamente lo que necesita. Entonces, ¿por qué reinventar la rueda? La idea es básicamente la misma que un enfoque de evento de "cambio".
En un nivel de rendimiento, su enfoque puede ser ''pesado'', sin embargo, siempre dependerá de la frecuencia con la que actualice las propiedades a y b
. Por ejemplo, si configura b
como el ng-model
de un cuadro de entrada (como en el ejemplo de jsbin), c
se volverá a calcular cada vez que el usuario escriba algo ... eso es claramente un exceso de procesamiento. Si usa un enfoque suave y actualiza a
y / o solo cuando es necesario, entonces no debería tener problemas de rendimiento. Sería lo mismo que volver a calcular c
usando eventos de "cambio" o un enfoque de setter & getter. Sin embargo, si realmente necesita volver a calcular c
en tiempo real (es decir, mientras el usuario escribe), el problema de rendimiento siempre estará presente y no es el hecho de que esté utilizando $rootScope
o $watch
que lo ayudarán a mejorarlo. .
Reanudando, en mi opinión , su enfoque no es malo (¡en absoluto!), Solo tenga cuidado con las propiedades de $rootScope
y evite el procesamiento en tiempo real.
En general, esto probablemente no sea una buena idea. También es (en general) una mala práctica exponer la implementación del modelo a todas las personas que llaman, si no por otra razón que la refactorización se vuelve más difícil y onerosa. Podemos resolver ambos fácilmente:
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; }
};
});
Ese es el enfoque de mejores prácticas. Se escala bien, solo se llama cuando es necesario y no contamina $rootScope
.
PD: también puede actualizar c
cuando a
o b
está configurado para evitar el recalc en cada llamada a get_c
; cuál es el mejor depende de los detalles de su implementación.
Me doy cuenta de que esto es un año y medio después, pero dado que recientemente tomé la misma decisión, pensé que ofrecería una respuesta alternativa que "funcionó para mí" sin contaminar $rootScope
con ningún valor nuevo.
Sí, sin embargo sigue confiando en $rootScope
. Sin embargo, en lugar de transmitir mensajes, simplemente llama a $rootScope.$digest
.
El enfoque básico es proporcionar un único objeto modelo complejo como un campo en su servicio angular. Puede proporcionar más de lo que crea conveniente, simplemente siga el mismo enfoque básico y asegúrese de que cada campo alberga un objeto complejo cuya referencia no cambia, es decir, no reasigne el campo con un nuevo objeto complejo. En su lugar, solo modifique los campos de este objeto modelo.
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);
});
Los controladores que desean depender del modelo del servicio son libres de insertar el modelo en su alcance. Por ejemplo:
$scope.theServiceModel = aModel.myModel;
Y enlazar directamente a los campos:
<div>A: {{theServiceModel.a}}</div>
<div>B: {{theServiceModel.b}}</div>
<div>C: {{theServiceModel.c}}</div>
Y todo se actualizará automáticamente cada vez que los valores se actualicen dentro del servicio.
Tenga en cuenta que esto solo funcionará si inyecta tipos que heredan de Object (por ejemplo, matriz, objetos personalizados) directamente en el alcance. Si se inyectan valores primitivos como cadena o número directamente en el alcance (por ejemplo, $scope.a = aModel.myModel.a
), se colocará una copia en el alcance y, por lo tanto, nunca recibirá un nuevo valor en la actualización. Típicamente, la mejor práctica es simplemente inyectar todo el objeto modelo en el alcance, como lo hice en mi ejemplo.
Por lo que puedo ver de su estructura, tener a
y b
como captadores puede no ser una buena idea, pero c
debería ser una función ...
Así que podría sugerir
myModule.factory( ''aModel'', function () {
return {
a: 10,
b: 10,
c: function () { return this.a + this.b; }
};
});
Con este enfoque, no se puede, por supuesto, vincular 2 vías c
a una variable de entrada ... Pero el enlace bidireccional c
no tiene ningún sentido tampoco porque si se establece el valor de c
, ¿cómo se dividiría el valor entre b
?