Is binding objects to angular's $rootScope in a service bad?

a 夏天 提交于 2019-11-28 18:44:35

问题


In angular, I have an object that will be exposed across my application via a service.

Some of the fields on that object are dynamic, and will be updated as normal by bindings in the controllers that use the service. But some of the fields are computed properties, that depend on the other fields, and need to be dynamically updated.

Here's a simple example (which is working on jsbin here). My service model exposes fields a, b and c where c is calculated from a + B in calcC(). Note, in my real application the calculations are a lot more complex, but the essence is here.

The only way I can think to get this to work, is to bind my service model to the $rootScope, and then use $rootScope.$watch to watch for any of the controllers changing a or b and when they do, recalculating c. But that seems ugly. Is there a better way of doing this?


A second concern is performance. In my full application a and b are big lists of objects, which get aggregated down to c. This means that the $rootScope.$watch functions will be doing a lot of deep array checking, which sounds like it will hurt performance.

I have this all working with an evented approach in BackBone, which cuts down the recalculation as much as possible, but angular doesn't seem to play well with an evented approach. Any thoughts on that would be great too.


Here's the example 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;
});

回答1:


Although from a high level, I agree with the answer by bmleite ($rootScope exists to be used, and using $watch appears to work for your use case), I want to propose an alternative approach.

Use $rootScope.$broadcast to push changes to a $rootScope.$on listener, which would then recalculate your c value.

This could either be done manually - i.e. when you would be actively changing a or b values, or possibly even on a short timeout to throttle the frequency of the updates. A step further from that would be to create a 'dirty' flag on your service, so that c is only calculated when required.

Obviously such an approach means a lot more involvement in recalculation in your controllers, directives etc - but if you don't want to bind an update to every possible change of a or b, the issue becomes a matter of 'where to draw the line'.




回答2:


I must admit, the first time I read your question and saw your example I thought to myself "this is just wrong", however, after looking into it again I realized it wasn't so bad as I thought it would be.

Let's face the facts, the $rootScope is there to be used, if you want to share anything application-wide, that's the perfect place to put it. Of course you will need to careful, it's something that's being shared between all the scopes so you don't want to inadvertently change it. But let's face it, that's not the real problem, you already have to be careful when using nested controllers (because child scopes inherit parent scope properties) and non-isolated scope directives. The 'problem' is already there and we shouldn't use it as an excuse not follow this approach.

Using $watch also seems to be a good idea. It's something that the framework already provides you for free and does exactly what you need. So, why reinventing the wheel? The idea is basically the same as an 'change' event approach.

On a performance level, your approach can be in fact 'heavy', however it will always depend on the frequency you update the a and b properties. For example, if you set a or b as the ng-model of an input box (like on your jsbin example), c will be re-calculated every time the user types something... that's clearly over-processing. If you use a soft approach and update a and/or b solely when necessary, then you shouldn't have performance problems. It would be the same as re-calculate c using 'change' events or a setter&getter approach. However, if you really need to re-calculate c on real-time (i.e: while the user is typing) the performance problem will always be there and is not the fact that you are using $rootScope or $watch that will help improve it.

Resuming, in my opinion, your approach is not bad (at all!), just be careful with the $rootScope properties and avoid ´real-time´ processing.




回答3:


I realize this is a year and a half later, but since I've recently had the same decision to make, I thought I'd offer an alternative answer that "worked for me" without polluting $rootScope with any new values.

It does, however still rely on $rootScope. Rather than broadcasting messages, however, it simply calls $rootScope.$digest.

The basic approach is to provide a single complex model object as a field on your angular service. You can provide more than as you see fit, just follow the same basic approach, and make sure each field hosts a complex object whose reference doesn't change, i.e. don't re-assign the field with a new complex object. Instead, only modify the fields of this model object.

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

Controllers that wish to depend on the service's model are then free to inject the model into their scope. For example:

$scope.theServiceModel = aModel.myModel;

And bind directly to the fields:

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

And everything will automatically update whenever the values update within the service.

Note that this will only work if you inject types that inherit from Object (e.g. array, custom objects) directly into the scope. If you inject primitive values like string or number directly into scope (e.g. $scope.a = aModel.myModel.a) you will get a copy put into scope and will thus never receive a new value on update. Typically, best practice is to just inject the whole model object into the scope, as I did in my example.




回答4:


In general, this is probably not a good idea. It's also (in general) bad practice to expose the model implementation to all of its callers, if for no other reason than refactoring becomes more difficult and onerous. We can easily solve both:

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

That's the best practice approach. It scales well, only gets called when it's needed, and doesn't pollute $rootScope.

PS: You could also update c when either a or b is set to avoid the recalc in every call to get_c; which is best depends on your implementation details.




回答5:


From what I can see of your structure, having a and b as getters may not be a good idea but c should be a function...

So I could suggest

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

With this approach you cannot ofcourse 2 way bind c to an input variable.. But two way binding c does not make any sense either because if you set the value of c, how would you split up the value between a and b?



来源:https://stackoverflow.com/questions/14573023/is-binding-objects-to-angulars-rootscope-in-a-service-bad

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!