web-dev-qa-db-fra.com

La liaison d'objets à $ rootScope d'angular dans un service est-elle mauvaise?

En angulaire, j'ai un objet qui sera exposé à travers mon application via un service.

Certains des champs de cet objet sont dynamiques et seront mis à jour normalement par des liaisons dans les contrôleurs qui utilisent le service. Mais certains champs sont des propriétés calculées, qui dépendent des autres champs et doivent être mis à jour dynamiquement.

Voici un exemple simple (qui fonctionne sur jsbin ici ). Mon modèle de service expose les champs a, b et cc est calculé à partir de a + B Dans calcC(). Remarque, dans ma vraie application, les calculs sont beaucoup plus complexes, mais l'essentiel est ici.

La seule façon dont je peux penser pour que cela fonctionne, est de lier mon modèle de service au $rootScope, Puis d'utiliser $rootScope.$watch Pour surveiller l'un des contrôleurs qui change a ou b et quand ils le font, recalculer c. Mais cela semble moche. Existe-t-il une meilleure façon de procéder?


Une deuxième préoccupation est la performance. Dans mon application complète, a et b sont de grandes listes d'objets, qui sont agrégées jusqu'à c. Cela signifie que les fonctions $rootScope.$watch Effectueront de nombreuses vérifications approfondies des tableaux, ce qui semble nuire aux performances.

J'ai tout cela avec une approche événementielle dans BackBone, ce qui réduit le recalcul autant que possible, mais angular ne semble pas bien fonctionner avec une approche événementielle. Toute réflexion à ce sujet serait également très bien.


Voici l'exemple d'application.

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;
});
32
latentflip

Bien qu'à un niveau élevé, je suis d'accord avec la réponse de bmleite ($ rootScope existe pour être utilisé, et l'utilisation de $ watch semble fonctionner pour votre cas d'utilisation), je veux proposer une approche alternative.

Utilisation $rootScope.$broadcast pour pousser les modifications vers un $rootScope.$on listener, qui recalculerait alors votre valeur c.

Cela peut être fait manuellement, c'est-à-dire lorsque vous changez activement les valeurs de a ou b, ou peut-être même sur un court délai pour limiter la fréquence des mises à jour. Un pas plus loin serait de créer un indicateur 'sale' sur votre service, de sorte que c ne soit calculé que lorsque cela est nécessaire.

Évidemment, une telle approche signifie beaucoup plus d'implication dans le recalcul dans vos contrôleurs, directives, etc. - mais si vous ne voulez pas lier une mise à jour à chaque changement possible de a ou b, le problème devient une question de "où tracer la ligne".

7
Alex Osborn

Je dois admettre que la première fois que j'ai lu votre question et vu votre exemple, je me suis dit "c'est tout simplement faux" , cependant, après y avoir réfléchi encore une fois, j'ai réalisé que ce n'était pas aussi mauvais que je le pensais.

Regardons les faits en face, le $rootScope est là pour être utilisé, si vous voulez partager quelque chose à l'échelle de l'application, c'est l'endroit idéal pour le mettre. Bien sûr, vous devrez faire attention, c'est quelque chose qui est partagé entre toutes les étendues, donc vous ne voulez pas le modifier par inadvertance. Mais avouons-le, ce n'est pas le vrai problème, vous devez déjà être prudent lorsque vous utilisez des contrôleurs imbriqués (car les étendues enfants héritent des propriétés de portée parent) et des directives de portée non isolées. Le "problème" est déjà là et nous ne devons pas l'utiliser comme excuse pour ne pas suivre cette approche.

En utilisant $watch semble également être une bonne idée. C'est quelque chose que le framework vous fournit déjà gratuitement et fait exactement ce dont vous avez besoin. Alors, pourquoi réinventer la roue? L'idée est fondamentalement la même qu'une approche d'événement de "changement".

Au niveau des performances, votre approche peut en fait être "lourde", mais elle dépendra toujours de la fréquence à laquelle vous mettez à jour les propriétés a et b. Par exemple, si vous définissez a ou b comme ng-model d'une zone de saisie (comme sur votre exemple jsbin), c sera recalculé chaque fois que l'utilisateur tape quelque chose ... c'est clairement un sur-traitement. Si vous utilisez une approche souple et mettez à jour a et/ou b uniquement lorsque cela est nécessaire, vous ne devriez pas avoir de problèmes de performances. Ce serait la même chose que recalculer c en utilisant des événements 'change' ou une approche setter & getter. Cependant, si vous avez vraiment besoin de recalculer c en temps réel (c'est-à-dire: pendant que l'utilisateur tape), le problème de performances sera toujours là et ce n'est pas le fait que vous utilisez $rootScope ou $watch qui contribuera à l'améliorer.

Reprenant, à mon avis, votre approche n'est pas mauvaise (pas du tout!), Soyez prudent avec le $rootScope propriétés et évite le traitement en temps réel.

4
bmleite

Je me rends compte que c'est un an et demi plus tard, mais comme j'ai récemment eu la même décision à prendre, j'ai pensé proposer une réponse alternative qui "fonctionnait pour moi" sans polluer $rootScope avec de nouvelles valeurs.

Il le fait cependant toujours compter sur $rootScope. Plutôt que de diffuser des messages, il appelle simplement $rootScope.$digest.

L'approche de base consiste à fournir un objet de modèle complexe unique en tant que champ sur votre service angular. Vous pouvez fournir plus que vous le souhaitez, suivez simplement la même approche de base et assurez-vous que chaque champ héberge un objet complexe dont la référence ne change pas, c'est-à-dire ne réattribuez pas le champ avec un nouvel objet complexe. Modifiez plutôt les champs de cet objet modèle.

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

Les contrôleurs qui souhaitent dépendre du modèle du service sont alors libres d'injecter le modèle dans leur périmètre. Par exemple:

$scope.theServiceModel = aModel.myModel;

Et liez directement aux champs:

<div>A: {{theServiceModel.a}}</div>
<div>B: {{theServiceModel.b}}</div>
<div>C: {{theServiceModel.c}}</div>

Et tout sera automatiquement mis à jour chaque fois que les valeurs seront mises à jour au sein du service.

Notez que cela ne fonctionnera que si vous injectez des types qui héritent d'Object (par exemple un tableau, des objets personnalisés) directement dans la portée. Si vous injectez des valeurs primitives comme une chaîne ou un nombre directement dans la portée (par exemple $scope.a = aModel.myModel.a) vous obtiendrez une copie dans la portée et ne recevrez donc jamais de nouvelle valeur lors de la mise à jour. En règle générale, la meilleure pratique consiste à simplement injecter l'objet de modèle entier dans la portée, comme je l'ai fait dans mon exemple.

3
Randolpho

En général, ce n'est probablement pas une bonne idée. Il est également (en général) une mauvaise pratique d'exposer l'implémentation du modèle à tous ses appelants, si pour aucune autre raison que la refactorisation devient plus difficile et onéreuse. Nous pouvons facilement résoudre les deux:

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

C'est l'approche des meilleures pratiques. Il évolue bien, n'est appelé que lorsque cela est nécessaire et ne pollue pas $rootScope.

PS: vous pouvez également mettre à jour c lorsque a ou b est défini pour éviter le recalcul dans chaque appel à get_c; ce qui dépend le mieux des détails de votre implémentation.

2
Josh David Miller

D'après ce que je peux voir de votre structure, avoir a et b comme getters n'est peut-être pas une bonne idée mais c devrait être une fonction ...

Je pourrais donc suggérer

myModule.factory( 'aModel', function () {
  return {
    a: 10,
    b: 10,
    c: function () { return this.a + this.b; }
  };
});

Avec cette approche, vous ne pouvez bien sûr pas lier de manière bidirectionnelle c à une variable d'entrée. Mais la liaison bidirectionnelle c n'a pas de sens non plus car si vous définissez la valeur de c , comment répartiriez-vous la valeur entre a et b?

1
ganaraj