提问



我有服务,说:


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.foo被更新,我已将这种模式拼凑在一起,我将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的暴政和开销,你总是可以使用好的旧观察者模式。


在服务中:


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

  //register an observer
  this.registerObserverCallback = function(callback){
    observerCallbacks.push(callback);
  };

  //call this when you know 'foo' has been changed
  var notifyObservers = function(){
    angular.forEach(observerCallbacks, function(callback){
      callback();
    });
  };

  //example of when you may want to notify observers
  this.foo = someNgResource.query().$then(function(){
    notifyObservers();
  });
});


在控制器中:


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

  aService.registerObserverCallback(updateFoo);
  //service now in control of updating foo
};

其它参考1


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


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


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


这样,听众也可以来自 DOM 和/或不同的子范围,而服务不会改变其行为。


**更新:示例**


广播在全球服务中最有意义,可能会影响您应用中的无数其他内容。一个很好的例子是用户服务,其中可能发生许多事件,例如登录,注销,更新,空闲等。我相信这是广播最有意义的地方,因为任何范围都可以监听事件,甚至注入服务,它不需要评估任何表达式或缓存结果来检查更改。它只是激发和忘记(因此请确保它是即发即弃通知,而不是需要采取行动的事情)


.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()函数完成且数据有效时,上述服务将向每个范围广播消息。或者,如果它是sa $ resource或ajax提交,则将广播调用移动到回调中,以便在服务器响应时触发。广播特别适合该模式,因为每个侦听器只是等待事件而无需检查范围在每一个$ digest上。听众看起来像:


.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调用它将触发每个范围。例如,如果你从控制器的范围广播$,它会触发仅在从控制器范围继承的范围中。 $ emit方向相反,行为类似于DOM事件,因为它会使范围链冒泡。


请记住,在某些情况下,$ broadcast非常有意义,并且有些情况下$ watch是更好的选择 - 特别是在具有非常特定的监视表达的隔离范围中。

其它参考2


我使用与@dtheodot类似的方法,但使用角度承诺而不是传递回调


app.service('myService', function($q) {
    var self = this,
        defer = $q.defer();

    this.foo = 0;

    this.observeFoo = function() {
        return defer.promise;
    }

    this.setFoo = function(foo) {
        self.foo = foo;
        defer.notify(self.foo);
    }
})


然后只需使用myService.setFoo(foo)方法更新服务上的foo。在您的控制器中,您可以将其用作:


myService.observeFoo().then(null, null, function(foo){
    $scope.foo = foo;
})


then的前两个参数是成功和错误回调,第三个是通知回调。


$ q。[69]的参考

其它参考3


没有手表或观察者回调(http://jsfiddle.net/zymotik/853wvv7s/):[70]


JavaScript的:


angular.module("Demo", [])
    .factory("DemoService", function($timeout) {

        function DemoService() {
            var self = this;
            self.name = "Demo Service";

            self.count = 0;

            self.counter = function(){
                self.count++;
                $timeout(self.counter, 1000);
            }

            self.addOneHundred = function(){
                self.count+=100;
            }

            self.counter();
        }

        return new DemoService();

    })
    .controller("DemoController", function($scope, DemoService) {

        $scope.service = DemoService;

        $scope.minusOneHundred = function() {
            DemoService.count -= 100;
        }

    });


HTML


<div ng-app="Demo" ng-controller="DemoController">
    <div>
        <h4>{{service.name}}</h4>
        <p>Count: {{service.count}}</p>
    </div>
</div>


这个JavaScript的工作原理是我们从服务传递一个对象而不是一个值。从服务返回JavaScript对象时,Angular会将监视添加到其所有属性中。


还要注意我使用var self=this,因为我需要在$ timeout执行时保持对原始对象的引用,否则this将引用window对象。

其它参考4


据我所知,你不必做那样详细的事情。您已经将foo从服务分配给范围,因为foo是一个数组(反过来它是一个通过引用分配的对象!)。所以,你需要做的就是这样:


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

 }


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

其它参考5


我偶然发现了这个寻找类似问题的问题,但我认为应该彻底解释发生了什么,以及我的解决方案。


当您使用的角度表达式(例如您使用的角色表达式)出现在HTML中时,Angular会自动为$scope.foo设置$watch,并会在$scope.foo更改时更新HTML。


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


这里没有说明的问题是,aService.foo影响aService.foo的两件事之一,即未发现变化。这两种可能性是:



  1. aService.foo每次都被设置为一个新数组,导致对它的引用过时。

  2. aService.foo正在以更新时未触发$digest周期的方式进行更新。






问题1:过时的参考文献



考虑到第一种可能性,假设正在应用$digest,如果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>



您可以在$ rootScope中插入服务并观察:


myApp.run(function($rootScope, aService){
    $rootScope.aService = aService;
    $rootScope.$watch('aService', function(){
        alert('Watch');
    }, true);
});


在你的控制器中:


myApp.controller('main', function($scope){
    $scope.aService.foo = 'change';
});


其他选择是使用外部库,如:https://github.com/melanke/Watch.JS [71]


适用于:IE 9 +,FF 4 +,SF 5 +,WebKit,CH 7 +,OP 12 +,BESEN,Node.JS,Rhino 1.7+


您可以观察一个,多个或所有对象属性的更改。


例:


var ex3 = {
    attr1: 0,
    attr2: "initial value of attr2",
    attr3: ["a", 3, null]
};   
watch(ex3, function(){
    alert("some attribute of ex3 changes!");
});
ex3.attr3.push("new value");​

其它参考6


您可以在工厂内观察更改,然后广播更改


angular.module('MyApp').factory('aFactory', function ($rootScope) {
    // Define your factory content
    var result = {
        'key': value
    };

    // add a listener on a key        
    $rootScope.$watch(function () {
        return result.key;
    }, function (newValue, oldValue, scope) {
        // This is called after the key "key" has changed, a good idea is to broadcast a message that key has changed
        $rootScope.$broadcast('aFactory:keyChanged', newValue);
    }, true);

    return result;
});


然后在你的控制器中:


angular.module('MyApp').controller('aController', ['$rootScope', function ($rootScope) {

    $rootScope.$on('aFactory:keyChanged', function currentCityChanged(event, value) {
        // do something
    });
}]);


通过这种方式,您可以将所有相关的工厂代码放在其描述中,然后您只能依赖外部的广播

其它参考7


==修订==


非常简单现在$ watch。


笔在这里。[72]


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 + '!');
  });
}]);

其它参考8


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


Array.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;
};

其它参考9


这是我的通用方法。


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

其它参考10


面对一个非常类似的问题,我在范围内看了一个函数,并让函数返回服务变量。我创造了一个小提琴手。你可以在下面找到代码。[73]


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

其它参考11


我来到这个问题,但事实证明我的问题是,当我应该使用angular $ interval提供程序时,我正在使用setInterval。这也是setTimeout的情况(改为使用$ timeout)。我知道这不是OP的问题的答案,但它可能对一些人有所帮助,因为它帮助了我。

其它参考12


我在另一个线程上找到了一个非常好的解决方案,它有类似的问题,但方法完全不同。来源:AngularJS:当$ rootScope值发生变化时,指令内的$ watch无效


基本上那里的解决方案告诉不要使用$watch因为它是非常重的解决方案。 相反他们建议使用$emit$on


我的问题是在服务中观察变量并以指令作出反应。而且用上面的方法很容易!


我的模块/服务示例:


angular.module('xxx').factory('example', function ($rootScope) {
    var user;

    return {
        setUser: function (aUser) {
            user = aUser;
            $rootScope.$emit('user:change');
        },
        getUser: function () {
            return (user) ? user : false;
        },
        ...
    };
});


所以基本上我观看我的user - 每当它被设置为新值I $emit a user:change状态。


现在在我的情况下,在我使用的指令中:


angular.module('xxx').directive('directive', function (Auth, $rootScope) {
    return {
        ...
        link: function (scope, element, attrs) {
            ...
            $rootScope.$on('user:change', update);
        }
    };
});


现在在指令中我听$rootScope 给定的更改 - 我分别做出反应。非常轻松优雅!

其它参考13


对于像我这样只想寻找简单解决方案的人来说,这几乎完全符合您对控制器中使用普通$ watch的期望。
唯一的区别是,它在它的javascript上下文中评估字符串,而不是在特定范围内。你必须将$ rootScope注入到你的服务中,尽管它只用于正确地挂钩摘要周期。


function watch(target, callback, deep) {
    $rootScope.$watch(function () {return eval(target);}, callback, deep);
};

其它参考14


//服务:(这里没什么特别的)


myApp.service('myService', function() {
  return { someVariable:'abc123' };
});


//ctrl:


myApp.controller('MyCtrl', function($scope, myService) {

  $scope.someVariable = myService.someVariable;

  // watch the service and update this ctrl...
  $scope.$watch(function(){
    return myService.someVariable;
  }, function(newValue){
    $scope.someVariable = newValue;
  });
});

其它参考15


有点难看,但我已经将范围变量的注册添加到我的服务中以进行切换:


myApp.service('myService', function() {
    var self = this;
    self.value = false;
    self.c2 = function(){};
    self.callback = function(){
        self.value = !self.value; 
       self.c2();
    };

    self.on = function(){
        return self.value;
    };

    self.register = function(obj, key){ 
        self.c2 = function(){
            obj[key] = self.value; 
            obj.$apply();
        } 
    };

    return this;
});


然后在控制器中:


function MyCtrl($scope, myService) {
    $scope.name = 'Superhero';
    $scope.myVar = false;
    myService.register($scope, 'myVar');
}

其它参考16


我在这里看到一些可怕的观察者模式导致大型应用程序的内存泄漏。


我可能有点晚了,但这很简单。


如果您想观看像数组推送之类的东西,手表功能会监视参考更改(原始类型):


someArray.push(someObj); someArray = someArray.splice(0);


这将更新参考并从任何地方更新手表。包括服务getter方法。
任何原始的东西都会自动更新。

其它参考17


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


控制器


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


我想这会做你想要的。我的控制器通过此实现继续检查我的服务的价值。老实说,这比选定的答案简单得多。

其它参考18


我写了两个简单的实用程序服务,帮助我跟踪服务属性的变化。


如果你想跳过很长的解释,你可以去海峡[[j !]]]]]]]]]]]]]]]]]]]]]]**



  1. 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;
  };
}



看看这个plunker ::这是我能想到的最简单的例子


http://jsfiddle.net/HEdJF/[76]


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