我有一个服务,说:

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

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


当前回答

= = = =更新

在$watch中非常简单。

笔在这里。

HTML:

<div class="container" data-ng-app="app">

  <div class="well" data-ng-controller="FooCtrl">
    <p><strong>FooController</strong></p>
    <div class="row">
      <div class="col-sm-6">
        <p><a href="" ng-click="setItems([ { name: 'I am single item' } ])">Send one item</a></p>
        <p><a href="" ng-click="setItems([ { name: 'Item 1 of 2' }, { name: 'Item 2 of 2' } ])">Send two items</a></p>
        <p><a href="" ng-click="setItems([ { name: 'Item 1 of 3' }, { name: 'Item 2 of 3' }, { name: 'Item 3 of 3' } ])">Send three items</a></p>
      </div>
      <div class="col-sm-6">
        <p><a href="" ng-click="setName('Sheldon')">Send name: Sheldon</a></p>
        <p><a href="" ng-click="setName('Leonard')">Send name: Leonard</a></p>
        <p><a href="" ng-click="setName('Penny')">Send name: Penny</a></p>
      </div>
    </div>
  </div>

  <div class="well" data-ng-controller="BarCtrl">
    <p><strong>BarController</strong></p>
    <p ng-if="name">Name is: {{ name }}</p>
    <div ng-repeat="item in items">{{ item.name }}</div>
  </div>

</div>

JavaScript:

var app = angular.module('app', []);

app.factory('PostmanService', function() {
  var Postman = {};
  Postman.set = function(key, val) {
    Postman[key] = val;
  };
  Postman.get = function(key) {
    return Postman[key];
  };
  Postman.watch = function($scope, key, onChange) {
    return $scope.$watch(
      // This function returns the value being watched. It is called for each turn of the $digest loop
      function() {
        return Postman.get(key);
      },
      // This is the change listener, called when the value returned from the above function changes
      function(newValue, oldValue) {
        if (newValue !== oldValue) {
          // Only update if the value changed
          $scope[key] = newValue;
          // Run onChange if it is function
          if (angular.isFunction(onChange)) {
            onChange(newValue, oldValue);
          }
        }
      }
    );
  };
  return Postman;
});

app.controller('FooCtrl', ['$scope', 'PostmanService', function($scope, PostmanService) {
  $scope.setItems = function(items) {
    PostmanService.set('items', items);
  };
  $scope.setName = function(name) {
    PostmanService.set('name', name);
  };
}]);

app.controller('BarCtrl', ['$scope', 'PostmanService', function($scope, PostmanService) {
  $scope.items = [];
  $scope.name = '';
  PostmanService.watch($scope, 'items');
  PostmanService.watch($scope, 'name', function(newVal, oldVal) {
    alert('Hi, ' + newVal + '!');
  });
}]);

其他回答

这是我的一般方法。

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'];

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

当你使用的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设置一个类似于这里其他答案的观察者模式。

基于dtheodor的回答,你可以使用类似于下面的东西来确保你不会忘记取消注册回调…不过,有些人可能会反对将$scope传递给服务。

factory('aService', function() {
  var observerCallbacks = [];

  /**
   * Registers a function that will be called when
   * any modifications are made.
   *
   * For convenience the callback is called immediately after registering
   * which can be prevented with `preventImmediate` param.
   *
   * Will also automatically unregister the callback upon scope destory.
   */
  this.registerObserver = function($scope, cb, preventImmediate){
    observerCallbacks.push(cb);

    if (preventImmediate !== true) {
      cb();
    }

    $scope.$on('$destroy', function () {
      observerCallbacks.remove(cb);
    });
  };

  function notifyObservers() {
    observerCallbacks.forEach(function (cb) {
      cb();
    });
  };

  this.foo = someNgResource.query().$then(function(){
    notifyObservers();
  });
});

数组中。Remove是一个扩展方法,看起来像这样:

/**
 * Removes the given item the current array.
 *
 * @param  {Object}  item   The item to remove.
 * @return {Boolean}        True if the item is removed.
 */
Array.prototype.remove = function (item /*, thisp */) {
    var idx = this.indexOf(item);

    if (idx > -1) {
        this.splice(idx, 1);

        return true;
    }
    return false;
};

当我面对一个非常相似的问题时,我观察了一个作用域中的函数,并让函数返回服务变量。我已经创建了一个js小提琴。您可以在下面找到代码。

    var myApp = angular.module("myApp",[]);

myApp.factory("randomService", function($timeout){
    var retValue = {};
    var data = 0;

    retValue.startService = function(){
        updateData();
    }

    retValue.getData = function(){
        return data;
    }

    function updateData(){
        $timeout(function(){
            data = Math.floor(Math.random() * 100);
            updateData()
        }, 500);
    }

    return retValue;
});

myApp.controller("myController", function($scope, randomService){
    $scope.data = 0;
    $scope.dataUpdated = 0;
    $scope.watchCalled = 0;
    randomService.startService();

    $scope.getRandomData = function(){
        return randomService.getData();    
    }

    $scope.$watch("getRandomData()", function(newValue, oldValue){
        if(oldValue != newValue){
            $scope.data = newValue;
            $scope.dataUpdated++;
        }
            $scope.watchCalled++;
    });
});

我迟到了,但我找到了一个比上面的答案更好的方法。我没有分配一个变量来保存服务变量的值,而是创建了一个附加到作用域的函数,该函数返回服务变量。

控制器

$scope.foo = function(){
 return aService.foo;
}

我觉得这能满足你的要求。我的控制器通过这个实现不断检查我的服务的值。老实说,这比选择的答案要简单得多。