32

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

5 回答 5

7

尽管从高层次上看,我同意 bmleite 的回答(存在要使用的 $rootScope,并且使用 $watch 似乎适用于您的用例),但我想提出一种替代方法。

用于$rootScope.$broadcast将更改推送到$rootScope.$on侦听器,然后它会重新计算您的c值。

这可以手动完成 - 即当您主动更改ab值时,或者甚至可能在短暂的超时时间来限制更新频率。更进一步的步骤是在您的服务上创建一个“脏”标志,以便c仅在需要时计算。

显然,这种方法意味着更多地参与重新计算您的控制器、指令等 - 但如果您不想将更新绑定到aor的每个可能更改b,那么问题就变成了“在哪里画线”的问题。

于 2013-02-18T21:57:18.980 回答
4

我必须承认,当我第一次阅读您的问题并看到您的示例时,我对自己认为“这是错误的”,但是,在再次查看之后,我意识到它并没有我想象的那么糟糕。

让我们面对事实,$rootScope如果你想在应用程序范围内共享任何东西,那是可以使用的,这是放置它的最佳位置。当然,您需要小心,它是在所有范围之间共享的,因此您不想无意中更改它。但是让我们面对现实吧,这不是真正的问题,在使用嵌套控制器(因为子作用域继承父作用域属性)和非隔离作用域指令时,您已经必须小心了。“问题”已经存在,我们不应该以此为借口不遵循这种方法。

使用$watch似乎也是一个好主意。这是框架已经免费为您提供的东西,并且完全符合您的需要。那么,为什么要重新发明轮子呢?这个想法基本上与“改变”事件方法相同。

在性能级别上,您的方法实际上可能很“繁重”,但它始终取决于您更新ab属性的频率。例如,如果您将aor设置bng-model输入框(例如在您的 jsbin 示例中),c 则每次用户键入内容时都会重新计算……这显然是过度处理。如果您使用软方法并更新a和/或b仅在必要时进行更新,那么您不应该遇到性能问题。c这与使用“更改”事件或 setter&getter 方法重新计算相同。c但是,如果您确实需要实时重新计算(即:$rootScope$watch这将有助于改进它。

恢复,在我看来,你的方法还不错(一点也不!),只需小心$rootScope属性并避免“实时”处理。

于 2013-01-29T16:11:26.917 回答
3

我意识到这是一年半之后的事,但由于我最近做出了同样的决定,我想我会提供一个“对我有用”的替代答案,而不会污染$rootScope任何新的价值观。

它确实如此,但仍然依赖. $rootScope然而,它不是广播消息,而是简单地调用$rootScope.$digest.

基本方法是在您的角度服务中提供单个复杂模型对象作为字段。您可以提供比您认为合适的更多,只需遵循相同的基本方法,并确保每个字段承载一个其引用不变的复杂对象,即不要用新的复杂对象重新分配字段。相反,只修改此模型对象的字段。

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>

每当服务中的值更新时,一切都会自动更新。

请注意,这仅在您将继承自 Object 的类型(例如数组、自定义对象)直接注入作用域时才有效。如果您将原始值(如字符串或数字)直接注入范围(例如$scope.a = aModel.myModel.a),您将获得一个副本放入范围,因此在更新时永远不会收到新值。通常,最佳实践是将整个模型对象注入范围,就像我在示例中所做的那样。

于 2014-06-02T15:35:41.997 回答
2

一般来说,这可能不是一个好主意。如果没有其他原因,重构变得更加困难和繁重,将模型实现公开给所有调用者也是(通常)不好的做法。我们可以轻松解决这两个问题:

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时进行更新,以避免每次调用时重新计算;哪个最好取决于您的实施细节。abget_c

于 2013-01-29T00:54:40.603 回答
1

从我对您的结构的了解来看,拥有ab作为吸气剂可能不是一个好主意,但c应该是一个功能......

所以我可以建议

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

使用这种方法,您当然不能 2 路绑定c到输入变量。但是双向绑定c也没有任何意义,因为如果您设置 的值c,您将如何拆分 和 之间的ab

于 2013-01-29T09:48:13.820 回答