AngularJS:如何观察服务变量?


414

我有服务,说:

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

这感觉很老套,我一直在使用该服务变量的每个控制器中重复进行此操作。有没有更好的方法来监视共享变量?


1
您可以将第三个参数传递给设置为true的$ watch以深入监视aService及其所有属性。
SirTophamHatt 2013年

7
$ scope.foo = aService.foo就足够了,您可以丢掉上面的行。如果您想为$ scope.foo分配一个新值,在$ watch内部执行的操作就没有意义了
Jin

4
您可以aService.foo在html标记中引用吗?(例如:plnkr.co/edit/aNrw5Wo4Q0IxR2loipl5?p=preview
2014年

1
我已经添加了不进行回调或$手表为例,见下文(答案jsfiddle.net/zymotik/853wvv7s
Zymotik

1
@MikeGledhill,您是对的。我认为这是由于Javascript的性质所致,您可以在许多其他地方看到这种模式(不仅在Angular中,而且在JS中也是如此)。一方面,您转移了值(并且它没有绑定),另一方面,您转移了一个对象(或引用该对象的值...),这就是为什么正确地更新属性的原因(就像完美的一样)如上面的Zymotik示例所示)。
Christophe Vidal

Answers:


277

如果您想避免的暴政和开销,可以始终使用旧的观察者模式$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
};

21
@Moo侦听$destory示波器上的事件,并向aService
Jamie

13
该解决方案的优点是什么?它在服务中需要更多代码,而在控制器中需要多少代码(因为我们还需要在$ destroy上注销)。对于执行速度,我可以说,但是在大多数情况下,这并不重要。
Alex Che

6
不确定这是不是比$ watch更好的解决方案,发问者要求一种简单的数据共享方式,因此看起来更加麻烦。我宁愿使用$ broadcast而不是使用它
Jin

11
$watchvs观察者模式只是选择是轮询还是推,基本上是性能问题,因此在性能重要时使用它。当我不得不“深入”观察复杂对象时,我使用观察者模式。我将整个服务附加到$ scope而不是查看单个服务的值。我避免像恶魔般使用angular的$ watch,在指令和本机angular数据绑定中发生了足够多的事情。
dtheodor 2014年

107
我们之所以使用像Angular这样的框架,是为了不搞乱我们自己的观察者模式。
窃窃私语者

230

在这种情况下,如果多个/未知对象可能对更改感兴趣,请使用$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()函数完成并且数据有效时,以上服务将向每个范围广播一条消息。或者,如果是$ 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调用它会在每个作用域上触发。例如,如果要从控制器的作用域进行$ broadcast,则它将仅在从控制器作用域继承的作用域中触发。$ emit的方向相反,其行为类似于DOM事件,因为它使作用域链向上起泡。

请记住,在某些情况下$ broadcast有意义,在某些情况下$ watch是更好的选择-尤其是在隔离范围内具有非常特定的watch表达式的情况下。


1
摆脱$ digest循环是一件好事,尤其是如果您正在查看的更改不是直接直接进入DOM的值。
XML

有没有避免.save()方法的方法。当您仅监视sharedService中的单个变量的更新时,似乎有点过头了。我们可以从sharedService内部观看变量,并在更改时进行广播吗?
JerryKur 2014年

我尝试了多种方法在控制器之间共享数据,但这是唯一可行的方法。先生,打得好。
abettermap

与其他答案相比,我更喜欢这个,似乎不太客气了,谢谢
JMK 2015年

9
仅当使用方控制器具有多个可能的数据源时,这才是正确的设计模式。换句话说,如果您遇到MIMO情况(多个输入/多个输出)。如果您仅使用一对多模式,则应使用直接对象引用,并让Angular框架为您完成双向绑定。Horkyze这个链接下面,它的自动双向绑定一个很好的解释,它的局限性:stsc3000.github.io/blog/2013/10/26/...
查尔斯

47

我正在使用与@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的参考。


这种方法相对于马特·皮勒吉(Matt Pileggi)在下面描述的$ broadcast有什么好处?
法比奥2014年

两种方法都有其用途。对我来说,广播的优势是可读性强,并且可以在更多地方收听同一事件。我猜主要的缺点是广播正在向所有后代范围发送消息,因此这可能是性能问题。
Krym 2014年

2
我遇到了一个问题,即$scope.$watch对服务变量执行操作似乎不起作用(我正在观察的范围是从继承的模式$rootScope)-这行得通。很酷的技巧,感谢分享!
Seiyria 2014年

4
使用这种方法后,您将如何清理自己?销毁作用域时,是否可以从promise中删除已注册的回调?
Abris

好问题。老实说我不知道​​。我将尝试进行一些测试,以了解如何从promise中删除notify回调。
Krym 2015年

41

没有监视或观察者回调(http://jsfiddle.net/zymotik/853wvv7s/):

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”将引用窗口对象。


3
这是一个很棒的方法!有没有一种方法可以将服务的属性绑定到作用域,而不是整个服务?只做$scope.count = service.count不起作用。
jvannistelrooy

您还可以将属性嵌套在(任意)对象内部,以便通过引用传递该属性。$scope.data = service.data <p>Count: {{ data.count }}</p>
亚历克斯·罗斯

1
极好的方法!尽管此页面上有很多强大的功能性答案,但这到目前为止是a)最容易实现的,b)阅读代码时最容易理解的。这个答案应该比现在高很多。
CodeMoose

感谢@CodeMoose,我今天对AngularJS / JavaScript的新功能进行了进一步简化。
Zymotik

2
愿上帝保佑你。我会说浪费了百万小时。因为我在1.5中挣扎,angularjs从1变到2,并且还想共享数据
Amna,

29

我偶然发现了这个问题,寻找了类似的东西,但是我认为它应该对正在发生的事情以及一些其他解决方案进行透彻的解释。

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

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

这里未说的问题是,有两件事之一正在影响aService.foo ,使得未检测到更改。这两种可能性是:

  1. aService.foo 每次都将其设置为一个新数组,从而导致对该数组的引用已过时。
  2. aService.foo正在以$digest不触发更新周期的方式进行更新。

问题1:过时的引用

考虑到第一种可能性,假设应用了a $digest,如果aService.foo始终使用相同的数组,则自动设置$watch将检测到更改,如下面的代码片段所示。

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

正如你所看到的,NG-重复理应连接到aService.foo时不更新aService.foo变化,但附加了NG-重复aService2.foo 。这是因为我们的引用aService.foo已过时,但我们的引用aService2.foo却未过。我们使用创建了对初始数组的引用,该引用$scope.foo = aService.foo;随后在下一次更新时被服务丢弃,这意味着$scope.foo不再引用我们想要的数组。

但是,尽管有多种方法可以确保初始引用保持完整,但有时可能需要更改对象或数组。或者,如果服务属性引用了诸如a String或a之类的原语Number怎么办?在这些情况下,我们不能仅仅依靠参考。那么,什么可以做什么?

先前给出的几个答案已经为该问题提供了一些解决方案。但是,我个人赞成使用Jinthetallweeks在评论中建议的简单方法:

只需在html标记中引用aService.foo

解决方案1-b:将服务附加到合并范围,并{service}.{property}在HTML中引用。

含义,只需执行以下操作:

HTML:

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

JS:

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

这样,$watchaService.foo在each上解析$digest,这将获取正确的更新值。

这是您尝试使用的解决方法,但是回合方式要少得多。您$watch在控制器中添加了不必要的内容,该内容foo$scope更改时会显式地添加。$watch当您附加aService而不是附加aService.foo$scope,并aService.foo在标记中显式绑定时,则不需要额外的附加内容。


现在,假设$digest正在应用一个循环,这一切都很好。在上面的示例中,我使用了Angular的$interval服务来更新数组,该数组$digest在每次更新后自动启动循环。但是,如果服务变量(无论出于何种原因)没有在“ Angular world”中更新,该怎么办。换句话说,我们拥有$digest每当服务属性的变化能够自动启动周期?


问题2:丢失 $digest

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

我们之所以使用Angular之类的框架,是为了不构成我们自己的观察者模式

因此,我希望继续aService.foo在HTML标记中使用引用,如上面的第二个示例所示,而不必在Controller中注册其他回调。

解决方案2:将setter和getter与 $rootScope.$apply()

令我惊讶的是,还没有人建议使用二传手吸气剂。此功能是ECMAScript5中引入的,因此已经存在了很多年。当然,这意味着如果出于某种原因需要支持真正的旧浏览器,则此方法将不起作用,但是我觉得getter和setter在JavaScript中的使用率非常低。在这种特殊情况下,它们可能非常有用:

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

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

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

注意$rootScope.$apply()在set函数中的使用。这样可以确保Angular知道对的任何更改service.foo。如果出现“ inprog”错误,请参见此有用的参考页,或者如果使用Angular> = 1.3,则可以使用$rootScope.$applyAsync()

如果aService.foo要经常更新,也要小心,因为这可能会严重影响性能。如果性能是一个问题,则可以使用设置器来设置与其他答案类似的观察者模式。


3
这是正确,最简单的解决方案。正如@NanoWizard所说的那样,$ digest services不会监视属于该服务本身的属性。
Sarpdoruk Tahmaz 2015年

28

据我所知,您不必做那么复杂的事情。您已经将服务中的foo分配给了您的作用域,并且由于foo是一个数组(进而是一个对象,它是通过引用分配的!)。因此,您需要做的就是这样:

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

 }

如果在同一Ctrl中有其他变量依赖于foo的更改,那么是的,您将需要观察到foo并对该变量进行更改。但是只要是简单的参考参考,就没有必要。希望这可以帮助。


35
我尝试过,但无法$watch使用原始语言。相反,我在服务上定义了一个将返回原始值的方法:somePrimitive() = function() { return somePrimitive }我为该方法分配了$ scope属性 $scope.somePrimitive = aService.somePrimitive;。然后,我在HTML中使用了scope方法: <span>{{somePrimitive()}}</span>
Mark Rajcok 2012年

4
@MarkRajcok不,不要使用基元。将它们添加到对象中。基元不是可变的,因此2way数据绑定将不起作用
Jimmy Kane

3
@JimmyKane,是的,原语不应该用于2路数据绑定,但是我认为问题在于监视服务变量,而不是设置2路数据绑定。如果只需要监视服务属性/变量,则不需要对象-可以使用基元。
Mark Rajcok 2013年

3
在此设置中,我可以从范围中更改aService值。但是范围不会随着aService的更改而更改。
欧文·黄

4
这对我也不起作用。简单分配$scope.foo = aService.foo不会自动更新范围变量。
达尔文技术公司

9

您可以将服务插入$ 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

适用于: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");​

2
我不能相信这个答案不是最糟糕的!!!这是最优雅的解决方案(IMO),因为它减少了信息熵,并可能减轻了对其他中介程序处理程序的需求。如果可以的话,我会投票更多...
Cody 2015年

添加您的所有服务于$ rootScope,它的好处,它的潜在的陷阱,详述有些位置:stackoverflow.com/questions/14573023/...
Zymotik

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

这样,您可以将所有相关的工厂代码放在其描述中,然后只能依靠外部广播


6

==更新==

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

1
我喜欢PostmanService,但是如果我需要侦听多个变量,该如何更改控制器上的$ watch功能?
jedi

嗨,绝地,谢谢大家的注意!我更新了笔和答案。我建议为此添加另一个监视功能。因此,我向PostmanService添加了新功能。我希望这会
有所

实际上,是的:)如果您分享更多问题的详细信息,也许我可以为您提供帮助。
hayatbiralem '16

4

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

2

这是我的通用方法。

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

2

对于像我这样的人,他们只是寻找一个简单的解决方案,这几乎可以满足您在控制器中使用普通$ watch的期望。唯一的区别是,它在javascript上下文中而不是在特定范围内评估字符串。您必须将$ rootScope注入服务,尽管它仅用于正确地插入摘要周期。

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

2

面对一个非常相似的问题时,我在范围内观察了一个函数,并让该函数返回了服务变量。我创建了一个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++;
    });
});

2

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


您可以使用setTimeout或任何其他非Angular函数,但不要忘记使用来将代码包装在回调中$scope.$apply()
magnetronnie

2

我在另一个线程上找到了一个非常好的解决方案,具有类似的问题,但方法完全不同。来源: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 -每当它被设置为新值我就是$emit一个user:change状态。

现在,就我而言,在指令中我使用了:

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

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


1

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

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

1

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

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

谢谢。一个小问题:为什么您this从该服务中返回而不是self
shrekuu 2014年

4
因为有时会犯错误。;-)
nclu 2014年

return this;对您的构造函数不行的好习惯;-)
Cody

1

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

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

0

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

我可能会晚一点,但是就这么简单。

如果您想观看数组推送之类的内容,则监视功能监视参考更改(原始类型),只需使用:

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

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


0

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

控制者

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

我认为这会做您想要的。我的控制器通过此实现不断检查我的服务的价值。老实说,这比所选答案要简单得多。


为什么它被否决了..我也使用了类似的技术很多次,并且已经奏效了。
未定义的

0

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

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

  1. 守望先锋

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

可以通过以下方式在控制器中使用该服务:假设您具有属性“ prop1”,“ prop2”,“ prop3”的服务“ testService”。您要监视并将其分配给范围“ prop1”和“ prop2”。有了手表服务,它将看起来像这样:

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

  1. apply Watch obj很不错,但是如果您的服务中有异步代码,这还不够。对于这种情况,我使用了第二个实用程序,如下所示:

mod.service('apply', ['$timeout', ApplyService]);

function ApplyService($timeout) {
  return function apply() {
    $timeout(function() {});
  };
}

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

app.service('TestService', ['apply', TestService]);

function TestService(apply) {
  this.apply = apply;
}
TestService.prototype.test3 = function() {
  setTimeout(function() {
    this.prop1 = 'changed_test_2';
    this.prop2 = 'changed2_test_2';
    this.prop3 = 'changed3_test_2';
    this.apply(); //trigger $digest loop
  }.bind(this));
}

因此,所有这些看起来都是这样(您可以运行它或打开fiddle):

// 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>

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.