我有一个服务,说:

factory('aService', ['$rootScope', '$resource', function ($rootScope, $resource) {
  var service = {
    foo: []
  };

  return service;
}]);

我想用foo来控制一个在HTML中呈现的列表:

<div ng-controller="FooCtrl">
  <div ng-repeat="item in foo">{{ item }}</div>
</div>

以便控制器检测aService。我已经拼凑了这个模式,其中我添加aService到控制器的$scope,然后使用$scope.$watch():

function FooCtrl($scope, aService) {                                                                                                                              
  $scope.aService = aService;
  $scope.foo = aService.foo;

  $scope.$watch('aService.foo', function (newVal, oldVal, scope) {
    if(newVal) { 
      scope.foo = newVal;
    }
  });
}

这感觉有点冗长,我一直在每个使用服务变量的控制器中重复这一点。有没有更好的方法来监视共享变量?


当前回答

我在寻找类似的东西时偶然发现了这个问题,但我认为它值得对正在发生的事情进行彻底的解释,以及一些额外的解决方案。

当你使用的angular表达式出现在HTML中时,angular会自动为$scope设置一个$watch。每当$scope. foo时,它都会更新HTML。foo的变化。

<div ng-controller="FooCtrl">
  <div ng-repeat="item in foo">{{ item }}</div>
</div>

这里没有提到的问题是,有两件事正在影响aService。Foo使更改不会被检测到。这两种可能性是:

项服务。Foo每次都被设置为一个新的数组,导致对它的引用过时。 项服务。Foo的更新方式是在更新时不会触发$digest循环。


问题1:过时的引用

考虑第一种可能性,假设应用了$摘要,如果aService。Foo总是相同的数组,自动设置的$watch将检测到变化,如下面的代码片段所示。

解决方案1-a:确保数组或对象在每次更新时都是相同的对象

angular.module('myApp', []) .factory('aService', [ '$interval', function($interval) { var service = { foo: [] }; // Create a new array on each update, appending the previous items and // adding one new item each time $interval(function() { if (service.foo.length < 10) { var newArray = [] Array.prototype.push.apply(newArray, service.foo); newArray.push(Math.random()); service.foo = newArray; } }, 1000); return service; } ]) .factory('aService2', [ '$interval', function($interval) { var service = { foo: [] }; // Keep the same array, just add new items on each update $interval(function() { if (service.foo.length < 10) { service.foo.push(Math.random()); } }, 1000); return service; } ]) .controller('FooCtrl', [ '$scope', 'aService', 'aService2', function FooCtrl($scope, aService, aService2) { $scope.foo = aService.foo; $scope.foo2 = aService2.foo; } ]); <!DOCTYPE html> <html> <head> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script> <link rel="stylesheet" href="style.css" /> <script src="script.js"></script> </head> <body ng-app="myApp"> <div ng-controller="FooCtrl"> <h1>Array changes on each update</h1> <div ng-repeat="item in foo">{{ item }}</div> <h1>Array is the same on each udpate</h1> <div ng-repeat="item in foo2">{{ item }}</div> </div> </body> </html>

如您所见,ng-repeat应该附加到aService。当aService. foo不更新。但是ng-repeat附加到aService2. foo上。foo。这是因为我们引用了aService。foo已经过时,但是我们对aService2. foo的引用。Foo不是。我们用$scope创建了对初始数组的引用。foo = aService.foo;,然后被服务在下一次更新时丢弃,这意味着$scope。Foo不再引用我们想要的数组。

然而,虽然有几种方法可以确保初始引用保持适当,但有时可能需要更改对象或数组。或者,如果服务属性引用字符串或数字之类的原语,该怎么办?在这种情况下,我们不能仅仅依靠参考资料。那么我们能做什么呢?

前面给出的几个答案已经给出了这个问题的一些解决方案。然而,我个人更倾向于使用Jin和评论中的tallweeks所建议的简单方法:

引用aService即可。Foo在HTML标记中

解决方案1-b:将服务附加到作用域,并引用{service}。HTML中的{property}。

意思是,只要这样做:

HTML:

<div ng-controller="FooCtrl">
  <div ng-repeat="item in aService.foo">{{ item }}</div>
</div>

JS:

function FooCtrl($scope, aService) {
    $scope.aService = aService;
}

angular.module('myApp', []) .factory('aService', [ '$interval', function($interval) { var service = { foo: [] }; // Create a new array on each update, appending the previous items and // adding one new item each time $interval(function() { if (service.foo.length < 10) { var newArray = [] Array.prototype.push.apply(newArray, service.foo); newArray.push(Math.random()); service.foo = newArray; } }, 1000); return service; } ]) .controller('FooCtrl', [ '$scope', 'aService', function FooCtrl($scope, aService) { $scope.aService = aService; } ]); <!DOCTYPE html> <html> <head> <script data-require="angular.js@1.4.7" data-semver="1.4.7" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.js"></script> <link rel="stylesheet" href="style.css" /> <script src="script.js"></script> </head> <body ng-app="myApp"> <div ng-controller="FooCtrl"> <h1>Array changes on each update</h1> <div ng-repeat="item in aService.foo">{{ item }}</div> </div> </body> </html>

这样,$watch将解析aService。Foo在每个$digest上,它将得到正确更新的值。

这就是你想要的变通方法,但是不那么迂回。你在控制器中添加了一个不必要的$watch,它会在foo发生变化时显式地将它放在$scope上。当你附加aService而不是aService时,你不需要额外的$watch。foo到$scope,并显式绑定到aService。Foo的标记。


现在,假设应用了$digest周期,这一切都很好。在上面的例子中,我使用了Angular的$interval服务来更新数组,它会在每次更新后自动启动一个$digest循环。但是,如果服务变量(不管出于什么原因)在“Angular世界”中没有得到更新呢?换句话说,我们没有一个$digest周期被自动激活每当服务属性改变?


问题2:缺少$摘要

这里的许多解决方案都可以解决这个问题,但我同意Code Whisperer的观点:

我们使用Angular这样的框架的原因是不需要自己编造观察者模式

因此,我更愿意继续使用aService。在HTML标记中引用foo,如上面的第二个例子所示,并且不必在Controller中注册一个额外的回调。

解决方案2:在$rootScope中使用setter和getter。

我很惊讶还没有人建议使用setter和getter。这个功能是在ECMAScript5中引入的,因此到现在已经存在了很多年。当然,这意味着,如果出于某种原因,您需要支持非常老的浏览器,那么这个方法将不起作用,但我觉得getter和setter在JavaScript中没有得到充分利用。在这种特殊情况下,它们可能非常有用:

factory('aService', [
  '$rootScope',
  function($rootScope) {
    var realFoo = [];

    var service = {
      set foo(a) {
        realFoo = a;
        $rootScope.$apply();
      },
      get foo() {
        return realFoo;
      }
    };
  // ...
}

angular.module('myApp', []) .factory('aService', [ '$rootScope', function($rootScope) { var realFoo = []; var service = { set foo(a) { realFoo = a; $rootScope.$apply(); }, get foo() { return realFoo; } }; // Create a new array on each update, appending the previous items and // adding one new item each time setInterval(function() { if (service.foo.length < 10) { var newArray = []; Array.prototype.push.apply(newArray, service.foo); newArray.push(Math.random()); service.foo = newArray; } }, 1000); return service; } ]) .controller('FooCtrl', [ '$scope', 'aService', function FooCtrl($scope, aService) { $scope.aService = aService; } ]); <!DOCTYPE html> <html> <head> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script> <link rel="stylesheet" href="style.css" /> <script src="script.js"></script> </head> <body ng-app="myApp"> <div ng-controller="FooCtrl"> <h1>Using a Getter/Setter</h1> <div ng-repeat="item in aService.foo">{{ item }}</div> </div> </body> </html>

这里我在服务函数中添加了一个“私有”变量:realFoo。这个get分别使用get foo()和set foo()函数在服务对象上更新和检索。

注意set函数中使用了$rootScope.$apply()。这确保了Angular能够感知service.foo的任何变化。如果你得到'inprog'错误,请参阅这个有用的参考页面,或者如果你使用Angular >= 1.3,你可以使用$rootScope.$applyAsync()。

同样,如果aService。Foo的更新非常频繁,因为这会严重影响性能。如果性能是一个问题,您可以使用setter设置一个类似于这里其他答案的观察者模式。

其他回答

据我所知,你不需要做那么复杂的事情。您已经将foo从服务分配到作用域,并且由于foo是一个数组(反过来又是一个对象,它是通过引用分配的!). 所以,你所需要做的就是这样:

function FooCtrl($scope, aService) {                                                                                                                              
  $scope.foo = aService.foo;

 }

如果这个Ctrl中的其他变量依赖于foo的变化,那么是的,你需要一个手表来观察foo并对该变量进行更改。但只要是简单的参考观看是不必要的。希望这能有所帮助。

看看这个活塞:这是我能想到的最简单的例子?

http://jsfiddle.net/HEdJF/

<div ng-app="myApp">
    <div ng-controller="FirstCtrl">
        <input type="text" ng-model="Data.FirstName"><!-- Input entered here -->
        <br>Input is : <strong>{{Data.FirstName}}</strong><!-- Successfully updates here -->
    </div>
    <hr>
    <div ng-controller="SecondCtrl">
        Input should also be here: {{Data.FirstName}}<!-- How do I automatically updated it here? -->
    </div>
</div>



// declare the app with no dependencies
var myApp = angular.module('myApp', []);
myApp.factory('Data', function(){
   return { FirstName: '' };
});

myApp.controller('FirstCtrl', function( $scope, Data ){
    $scope.Data = Data;
});

myApp.controller('SecondCtrl', function( $scope, Data ){
    $scope.Data = Data;
});

在这样的场景中,多个/未知对象可能对更改感兴趣,请使用$rootScope。$broadcast来自正在更改的项。

而不是创建自己的侦听器注册表(必须在各种$ destroyed上进行清理),您应该能够从相关服务进行$broadcast。

您仍然必须在每个侦听器中编写$on处理程序,但该模式与多次调用$digest分离,从而避免了长时间运行的监视器的风险。

通过这种方式,侦听器也可以从DOM和/或不同的子作用域进出,而不需要服务更改其行为。

**更新:示例**

Broadcasts would make the most sense in "global" services that could impact countless other things in your app. A good example is a User service where there are a number of events that could take place such as login, logout, update, idle, etc. I believe this is where broadcasts make the most sense because any scope can listen for an event, without even injecting the service, and it doesn't need to evaluate any expressions or cache results to inspect for changes. It just fires and forgets (so make sure it's a fire-and-forget notification, not something that requires action)

.factory('UserService', [ '$rootScope', function($rootScope) {
   var service = <whatever you do for the object>

   service.save = function(data) {
     .. validate data and update model ..
     // notify listeners and provide the data that changed [optional]
     $rootScope.$broadcast('user:updated',data);
   }

   // alternatively, create a callback function and $broadcast from there if making an ajax call

   return service;
}]);

当save()函数完成并且数据有效时,上面的服务将向每个作用域广播一条消息。或者,如果它是$resource或ajax提交,则将广播调用移动到回调中,以便在服务器响应时触发它。广播特别适合这种模式,因为每个侦听器只需要等待事件,而不需要检查每个$摘要上的作用域。监听器看起来是这样的:

.controller('UserCtrl', [ 'UserService', '$scope', function(UserService, $scope) {

  var user = UserService.getUser();

  // if you don't want to expose the actual object in your scope you could expose just the values, or derive a value for your purposes
   $scope.name = user.firstname + ' ' +user.lastname;

   $scope.$on('user:updated', function(event,data) {
     // you could inspect the data to see if what you care about changed, or just update your own scope
     $scope.name = user.firstname + ' ' + user.lastname;
   });

   // different event names let you group your code and logic by what happened
   $scope.$on('user:logout', function(event,data) {
     .. do something differently entirely ..
   });

 }]);

这样做的好处之一是不需要多块手表。如果您像上面的例子一样组合字段或派生值,则必须同时注意firstname和lastname属性。只有当用户对象在更新时被替换时,观察getUser()函数才会工作,如果用户对象仅仅更新了它的属性,它就不会触发。在这种情况下,你必须进行深度观察,这是更密集的。

$broadcast将消息从它所调用的作用域发送到任何子作用域。因此从$rootScope调用它将在每个作用域上触发。例如,如果从控制器的作用域执行$broadcast,它只会在继承自控制器作用域的作用域中触发。$emit走向相反的方向,其行为类似于DOM事件,因为它在作用域链中冒泡。

请记住,在某些情况下,$broadcast很有意义,而在某些情况下,$watch是更好的选择——特别是在具有非常特定的watch表达式的孤立作用域中。

我编写了两个简单的实用程序服务,它们帮助我跟踪服务属性的更改。

如果您想跳过冗长的解释,可以直接访问jsfiddle

WatchObj

mod.service('WatchObj', ['$rootScope', WatchObjService]); function WatchObjService($rootScope) { // returns watch function // obj: the object to watch for // fields: the array of fields to watch // target: where to assign changes (usually it's $scope or controller instance) // $scope: optional, if not provided $rootScope is use return function watch_obj(obj, fields, target, $scope) { $scope = $scope || $rootScope; //initialize watches and create an array of "unwatch functions" var watched = fields.map(function(field) { return $scope.$watch( function() { return obj[field]; }, function(new_val) { target[field] = new_val; } ); }); //unregister function will unregister all our watches var unregister = function unregister_watch_obj() { watched.map(function(unregister) { unregister(); }); }; //automatically unregister when scope is destroyed $scope.$on('$destroy', unregister); return unregister; }; }

该服务在控制器中的使用方式如下: 假设你有一个服务“testService”,它的属性是“prop1”,“prop2”,“prop3”。您希望监视并将其分配给范围“prop1”和“prop2”。对于手表服务,它看起来像这样:

应用程序控制器(“检测”、“检测服务”、“观察”、“检测”); 功能测试表 扫帚美元。prop1 =测试服务。 扫帚美元。prop2 =测试服务。 扫帚美元。prop3 =测试服务。 观察(测试服务,[prop1, prop2], $scope, $scope]); )

应用 Watch obj很棒,但如果您的服务中有异步代码,它就不够了。在这种情况下,我使用了第二个实用程序,如下所示:

mod.service('apply', ['$timeout', ApplyService]); 函数ApplyService($timeout) { 返回函数apply() { 美元超时(函数(){}); }; }

我将在异步代码的末尾触发它以触发$digest循环。 像这样:

app.service('TestService', ['apply', TestService]); 函数TestService(应用){ 这一点。Apply = Apply; } TestService.prototype。Test3 =函数(){ setTimeout(函数(){ 这一点。Prop1 = 'changed_test_2'; 这一点。Prop2 = 'changed2_test_2'; 这一点。Prop3 = 'changed3_test_2'; this.apply ();//触发$digest循环 } .bind ()); }

所以,所有这些加在一起看起来就像这样(你可以运行它或打开小提琴):

// TEST app code var app = angular.module('app', ['watch_utils']); app.controller('TestWatch', ['$scope', 'TestService', 'WatchObj', TestWatchCtrl]); function TestWatchCtrl($scope, testService, watch) { $scope.prop1 = testService.prop1; $scope.prop2 = testService.prop2; $scope.prop3 = testService.prop3; watch(testService, ['prop1', 'prop2'], $scope, $scope); $scope.test1 = function() { testService.test1(); }; $scope.test2 = function() { testService.test2(); }; $scope.test3 = function() { testService.test3(); }; } app.service('TestService', ['apply', TestService]); function TestService(apply) { this.apply = apply; this.reset(); } TestService.prototype.reset = function() { this.prop1 = 'unchenged'; this.prop2 = 'unchenged2'; this.prop3 = 'unchenged3'; } TestService.prototype.test1 = function() { this.prop1 = 'changed_test_1'; this.prop2 = 'changed2_test_1'; this.prop3 = 'changed3_test_1'; } TestService.prototype.test2 = function() { setTimeout(function() { this.prop1 = 'changed_test_2'; this.prop2 = 'changed2_test_2'; this.prop3 = 'changed3_test_2'; }.bind(this)); } TestService.prototype.test3 = function() { setTimeout(function() { this.prop1 = 'changed_test_2'; this.prop2 = 'changed2_test_2'; this.prop3 = 'changed3_test_2'; this.apply(); }.bind(this)); } //END TEST APP CODE //WATCH UTILS var mod = angular.module('watch_utils', []); mod.service('apply', ['$timeout', ApplyService]); function ApplyService($timeout) { return function apply() { $timeout(function() {}); }; } mod.service('WatchObj', ['$rootScope', WatchObjService]); function WatchObjService($rootScope) { // target not always equals $scope, for example when using bindToController syntax in //directives return function watch_obj(obj, fields, target, $scope) { // if $scope is not provided, $rootScope is used $scope = $scope || $rootScope; var watched = fields.map(function(field) { return $scope.$watch( function() { return obj[field]; }, function(new_val) { target[field] = new_val; } ); }); var unregister = function unregister_watch_obj() { watched.map(function(unregister) { unregister(); }); }; $scope.$on('$destroy', unregister); return unregister; }; } <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script> <div class='test' ng-app="app" ng-controller="TestWatch"> prop1: {{prop1}} <br>prop2: {{prop2}} <br>prop3 (unwatched): {{prop3}} <br> <button ng-click="test1()"> Simple props change </button> <button ng-click="test2()"> Async props change </button> <button ng-click="test3()"> Async props change with apply </button> </div>

这是我的一般方法。

mainApp.service('aService',[function(){
        var self = this;
        var callbacks = {};

        this.foo = '';

        this.watch = function(variable, callback) {
            if (typeof(self[variable]) !== 'undefined') {
                if (!callbacks[variable]) {
                    callbacks[variable] = [];
                }
                callbacks[variable].push(callback);
            }
        }

        this.notifyWatchersOn = function(variable) {
            if (!self[variable]) return;
            if (!callbacks[variable]) return;

            angular.forEach(callbacks[variable], function(callback, key){
                callback(self[variable]);
            });
        }

        this.changeFoo = function(newValue) {
            self.foo = newValue;
            self.notifyWatchersOn('foo');
        }

    }]);

在控制器中

function FooCtrl($scope, aService) {
    $scope.foo;

    $scope._initWatchers = function() {
        aService.watch('foo', $scope._onFooChange);
    }

    $scope._onFooChange = function(newValue) {
        $scope.foo = newValue;
    }

    $scope._initWatchers();

}

FooCtrl.$inject = ['$scope', 'aService'];