【问题标题】:AngularJS : How to watch service variables?AngularJS:如何观察服务变量?
【发布时间】:2012-09-16 14:34:38
【问题描述】:

我有一个服务,比如说:

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 设置为 true 以深入观察 aService 及其所有属性。
  • $scope.foo= aService.foo 就足够了,你可以丢掉上面那行。而且它在 $watch 中所做的事情没有意义,如果你想为 $scope.foo 分配一个新值,只需这样做......
  • 你能在 html 标记中引用aService.foo 吗? (像这样:plnkr.co/edit/aNrw5Wo4Q0IxR2loipl5?p=preview
  • 我添加了一个没有回调或 $watches 的示例,请参阅下面的答案 (jsfiddle.net/zymotik/853wvv7s)
  • @MikeGledhill,你是对的。我认为这是由于 Javascript 的性质,您可以在许多其他地方看到这种模式(不仅仅是在 Angular 中,一般来说在 JS 中)。一方面,您传输值(并且它没有被绑定),另一方面您传输一个对象(或引用该对象的值......),这就是属性正确更新的原因(就像完美如上面 Zymotik 的示例所示)。

标签: angularjs watch angular-services


【解决方案1】:

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

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

 }

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

【讨论】:

  • 我试过了,但我无法让 $watch 使用原语。相反,我在服务上定义了一个返回原始值的方法:somePrimitive() = function() { return somePrimitive },并且我为该方法分配了一个 $scope 属性:$scope.somePrimitive = aService.somePrimitive;。然后我在 HTML 中使用了 scope 方法:&lt;span&gt;{{somePrimitive()}}&lt;/span&gt;
  • @MarkRajcok 不,不要使用原语。将它们添加到对象中。基元不是可变的,因此 2way 数据绑定将不起作用
  • @JimmyKane,是的,原语不应该用于二维数据绑定,但我认为问题是关于观察服务变量,而不是设置二维绑定。如果您只需要查看服务属性/变量,则不需要对象——可以使用原语。
  • 在此设置中,我可以从范围更改 aService 值。但是范围不会随着 aService 的变化而变化。
  • 这对我也不起作用。简单地分配$scope.foo = aService.foo 不会自动更新范围变量。
【解决方案2】:

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

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
  • 因为有时会犯错误。 ;-)
  • 从构造函数中return this; 的良好做法 ;-)
【解决方案3】:

如果您想避免$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
};

【讨论】:

  • @Moo 监听作用域上的$destory 事件并向aService 添加一个注销方法
  • 这个解决方案有什么优点?它在服务中需要更多代码,并且在控制器中需要相同数量的代码(因为我们还需要在 $destroy 上注销)。我可以说执行速度,但在大多数情况下,这并不重要。
  • 不确定这比 $watch 有什么更好的解决方案,提问者要求一种简单的数据共享方式,看起来更麻烦。我宁愿使用 $broadcast 而不是这个
  • $watch vs 观察者模式只是选择是轮询还是推送,基本上是性能问题,所以在性能问题时使用它。我使用观察者模式,否则我将不得不“深入”观察复杂的对象。我将整个服务附加到 $scope 而不是查看单个服务值。我避免像魔鬼一样使用 Angular 的 $watch,在指令和原生 Angular 数据绑定中发生的事情已经够多了。
  • 我们使用像 Angular 这样的框架的原因是为了不编写我们自己的观察者模式。
【解决方案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;
};

【讨论】:

    【解决方案5】:

    这是我的通用方法。

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

    【讨论】:

      【解决方案6】:

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

      与其创建自己的侦听器注册表(必须在各种 $destroy 上清理),您应该能够从相关服务中$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 表达式的隔离范围内。

      【讨论】:

      • 摆脱 $digest 循环是一件好事,特别是如果您正在观察的更改不是直接立即进入 DOM 的值。
      • 是否可以避免使用 .save() 方法。当您仅监视 sharedService 中单个变量的更新时,这似乎有点过头了。我们可以从 sharedService 中观察变量并在它发生变化时进行广播吗?
      • 我尝试了很多方法在控制器之间共享数据,但这是唯一有效的方法。打得好,先生。
      • 我更喜欢这个而不是其他答案,似乎不那么 hacky,谢谢
      • 只有当您的消费控制器有多个可能的数据源时,这才是正确的设计模式;换句话说,如果您有 MIMO 情况(多输入/多输出)。如果您只是使用一对多模式,则应该使用直接对象引用并让 Angular 框架为您进行双向绑定。 Horkyze 在下面链接了这个,它很好地解释了自动双向绑定,它的局限性:stsc3000.github.io/blog/2013/10/26/…
      【解决方案7】:

      我使用与@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的前两个参数是成功和错误回调,第三个是通知回调。

      Reference for $q.

      【讨论】:

      • 这种方法比 Matt Pileggi 下面描述的 $broadcast 有什么优势?
      • 这两种方法都有其用途。对我来说,广播的好处是人类可读性和在更多地方收听同一事件的可能性。我想主要的缺点是广播正在向所有后代范围发出消息,所以这可能是一个性能问题。
      • 我遇到了一个问题,在服务变量上执行 $scope.$watch 似乎不起作用(我正在观看的范围是从 $rootScope 继承的模式) - 这有效。很酷的技巧,感谢分享!
      • 您将如何使用这种方法进行清理?当作用域被销毁时,是否可以从承诺中删除注册的回调?
      • 好问题。老实说,我不知道。我将尝试对如何从 Promise 中删除通知回调进行一些测试。
      【解决方案8】:

      您可以在 $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");​
      

      【讨论】:

      • 我不敢相信这个答案不是投票最多的!!!这是最优雅的解决方案 (IMO),因为它减少了信息熵并可能减轻了对额外中介处理程序的需求。如果可以的话,我会更多地投票......
      • 将您的所有服务添加到 $rootScope、它的好处和潜在的陷阱,在这里都有详细说明:stackoverflow.com/questions/14573023/…
      【解决方案9】:

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

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

      【讨论】:

        【解决方案10】:

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

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

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

        【讨论】:

          【解决方案11】:

          在面临一个非常相似的问题时,我在范围内观察了一个函数并让该函数返回服务变量。我创建了一个js fiddle。您可以在下面找到代码。

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

          【讨论】:

            【解决方案12】:

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

            【讨论】:

            • 您可以使用setTimeout,或任何其他非Angular函数,但不要忘记使用$scope.$apply()将代码包装在回调中。
            【解决方案13】:

            没有手表或观察者回调 (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' 将引用窗口对象。

            【讨论】:

            • 这是一个很棒的方法!有没有办法只将服务的属性绑定到范围而不是整个服务?只做$scope.count = service.count 是行不通的。
            • 您还可以将属性嵌套在(任意)对象内,以便通过引用传递。 $scope.data = service.data&lt;p&gt;Count: {{ data.count }}&lt;/p&gt;
            • 优秀的方法!虽然此页面上有很多强大的功能性答案,但这是迄今为止 a) 最容易实现的,b) 阅读代码时最容易理解的。这个答案应该比现在高很多。
            • 感谢 @CodeMoose,我今天为那些刚接触 AngularJS/JavaScript 的人进一步简化了它。
            • 愿上帝保佑你。我会说,我已经浪费了数百万小时。因为我在 1.5 和 angularjs 从 1 到 2 的转变中苦苦挣扎,并且还想共享数据
            【解决方案14】:

            我在另一个线程上找到了一个非常棒的解决方案,它有类似的问题,但方法完全不同。来源:AngularJS : $watch within directive is not working when $rootScope value is changed

            基本上那里的解决方案告诉不要使用$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);
                    }
                };
            });
            

            现在在 directive 中,我聆听 $rootScopeon 给定的变化 - 我分别做出反应。非常简单优雅!

            【讨论】:

              【解决方案15】:

              ==更新==

              现在在 $watch 中非常简单。

              Pen here.

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

              【讨论】:

              • 我喜欢 PostmanService,但是如果我需要监听多个变量,我必须如何更改控制器上的 $watch 函数?
              • 嗨绝地,感谢您的提醒!我更新了笔和答案。我建议为此添加另一个监视功能。因此,我向 PostmanService 添加了一个新功能。我希望这会有所帮助:)
              • 实际上,是的:)如果您分享问题的更多细节,也许我可以帮助您。
              【解决方案16】:

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

              当 HTML 中出现您使用的 Angular 表达式时,Angular 会自动为 $scope.foo 设置一个 $watch,并在 $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:过时的引用

              考虑第一种可能性,假设 $digest 被应用,如果 aService.foo 始终是同一个数组,则自动设置的 $watch 将检测到更改,如下面的代码 sn-p 所示。

              解决方案 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>

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

              然而,虽然有多种方法可以确保初始引用保持完整,但有时可能需要更改对象或数组。或者,如果服务属性引用像 StringNumber 这样的原语怎么办?在这些情况下,我们不能简单地依赖参考。那么我们可以做什么呢?

              之前给出的几个答案已经为该问题提供了一些解决方案。不过,我个人更赞成在 cmets 中使用Jinthetallweeks 建议的简单方法:

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

              解决方案 1-b:将服务附加到范围,并在 HTML 中引用 {service}.{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 将解析每个$digest 上的aService.foo,从而获得正确更新的值。

              这是您尝试使用解决方法所做的一种方式,但方式要少得多。您在控制器中添加了一个不必要的$watch,它在更改时明确地将foo 放在$scope 上。当您将 aService 而不是 aService.foo 附加到 $scope 并在标记中显式绑定到 aService.foo 时,您不需要额外的 $watch


              假设正在应用$digest 循环,这一切都很好。在上面的示例中,我使用 Angular 的 $interval 服务来更新数组,每次更新后它会自动启动 $digest 循环。但是,如果服务变量(无论出于何种原因)没有在“Angular 世界”中得到更新怎么办。换句话说,我们会在服务属性更改时自动激活$digest 循环?


              问题2:缺少$digest

              这里的许多解决方案都会解决这个问题,但我同意Code Whisperer

              我们使用像 Angular 这样的框架的原因是为了不编写我们自己的观察者模式

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

              解决方案 2:使用带有 $rootScope.$apply() 的 setter 和 getter

              我很惊讶还没有人建议使用settergetter。此功能是在 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。分别使用service 对象上的get foo()set foo() 函数更新和检索此get。

              注意在 set 函数中使用$rootScope.$apply()。这确保了 Angular 将知道对 service.foo 的任何更改。如果您收到“inprog”错误,请参阅 this useful reference page,或者如果您使用 Angular >= 1.3,您可以使用 $rootScope.$applyAsync()

              如果aService.foo 的更新非常频繁,也请注意这一点,因为这可能会显着影响性能。如果性能是一个问题,您可以使用 setter 设置类似于此处其他答案的观察者模式。

              【讨论】:

              • 这是正确且最简单的解决方案。正如@NanoWizard 所说,$digest 监视 services 而不是属于服务本身的属性。
              【解决方案17】:

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

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

              如果您想观看类似数组推送之类的内容,请使用 watch 函数观看参考更改(原始类型):

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

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

              【讨论】:

                【解决方案18】:

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

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

                【讨论】:

                  【解决方案19】:

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

                  控制器

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

                  我认为这会满足您的需求。我的控制器通过这个实现不断检查我的服务的价值。老实说,这比选择的答案要简单得多。

                  【讨论】:

                  • 为什么它被否决了.. 我也多次使用过类似的技术,它已经奏效了。
                  【解决方案20】:

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

                  如果想跳过冗长的解释,可以直奔jsfiddle

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

                  该服务在控制器中的使用方式如下: 假设您有一个具有属性“prop1”、“prop2”、“prop3”的服务“testService”。您想观察并分配给范围“prop1”和“prop2”。使用 watch 服务,它看起来像这样:

                  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. 申请 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));
                  }

                  所以,所有这些看起来都会像这样(你可以运行它或open 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>

                  【讨论】:

                    【解决方案21】:

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

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

                    【讨论】:

                      猜你喜欢
                      • 2018-04-10
                      • 2015-08-20
                      • 1970-01-01
                      • 2016-03-13
                      • 2015-11-20
                      • 1970-01-01
                      • 1970-01-01
                      • 2015-12-27
                      • 2013-10-18
                      相关资源
                      最近更新 更多