AngularJS:如何观察服务变量?

AngularJS : How to watch service variables?

我有一项服务,比如:

1
2
3
4
5
6
7
factory('aService', ['$rootScope', '$resource', function ($rootScope, $resource) {
  var service = {
    foo: []
  };

  return service;
}]);

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

1
  {{ item }}

为了让控制器检测何时更新aService.foo,我将这个模式拼凑在一起,在这里我向控制器的$scope添加一个服务,然后使用$scope.$watch()

1
2
3
4
5
6
7
8
9
10
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的暴政和开销,您可以使用好的老观察者模式。

在服务中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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();
  });
});

在控制器中:

1
2
3
4
5
6
7
8
function FooCtrl($scope, aService){
  var updateFoo = function(){
    $scope.foo = aService.foo;
  };

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


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

与其创建自己的侦听器注册表(必须在各种$destroys上清除),还不如从相关服务中删除cx1(3)。

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

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

**更新:示例**

广播在"全球"服务中最有意义,它可能影响到应用程序中无数其他东西。一个很好的例子是用户服务,其中可能会发生许多事件,如登录、注销、更新、空闲等。我认为这是广播最有意义的地方,因为任何范围都可以监听事件,甚至不需要注入服务,而且不需要评估任何表达式或缓存结果来检查ch安吉斯。它只会触发并忘记(所以要确保它是一个"触发并忘记"通知,而不是需要动作的东西)

1
2
3
4
5
6
7
8
9
10
11
12
13
.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上的作用域。听者看起来像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.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表达式的独立作用域中。


我正在使用类似于@dtheodot的方法,但使用角度承诺而不是通过回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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的服务。在控制器中,您可以将其用作:

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

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

参考$Q.


没有手表或观察员回拨(http://jsfiddle.net/zymotik/853wvv7s/):

JavaScript:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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

1
2
3
4
        <h4>{{service.name}}</h4>
        <p>
Count: {{service.count}}
</p>

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

另外请注意,我使用的是"var self=this",因为在$timeout执行时需要保留对原始对象的引用,否则"this"将引用窗口对象。


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

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

1
  {{ item }}

这里未声明的问题是,有两件事之一正在影响aService.foo,以至于无法检测到变化。这两种可能性是:好的。

  • aService.foo每次都被设置为一个新的数组,导致对它的引用过时。
  • 更新aService.foo时,不会触发$digest周期。
  • 问题1:过时的参考资料

    考虑到第一种可能性,假设应用了$digest,如果aService.foo始终是相同的数组,自动设置的$watch将检测更改,如下面的代码段所示。好的。解决方案1-a:确保数组或对象在每次更新时都是相同的对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    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;
        }
      ]);
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!DOCTYPE html>
    <html>

    <head>
      <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js">
      <link rel="stylesheet" href="style.css" />
      <script src="script.js">
    </head>

    <body ng-app="myApp">
     
        Array changes on each update
        {{ item }}
        Array is the same on each udpate
        {{ item }}
     
    </body>

    </html>

    好的。

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

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

    前面给出的几个答案已经给出了该问题的一些解决方案。但是,我个人赞成使用Jin和The Allweeks在评论中建议的简单方法:好的。

    just reference aService.foo in the html markup

    Ok.

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

    意思是,只要这样做:好的。

    HTML:好的。

    1
      {{ item }}

    JS:好的。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    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;
        }
      ]);
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <!DOCTYPE html>
    <html>

    <head>
      <script data-require="[email protected]" data-semver="1.4.7" src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.js">
      <link rel="stylesheet" href="style.css" />
      <script src="script.js">
    </head>

    <body ng-app="myApp">
     
        Array changes on each update
        {{ item }}
     
    </body>

    </html>

    好的。

    这样,$watch将解析每个$digest上的aService.foo,从而得到正确的更新值。好的。

    这正是你试图用你的工作区来做的,但在方法上却没有那么圆滑。您在控制器中添加了一个不必要的$watch,它会在$scope发生变化时显式地将foo放在该控制器上。当您将aService而不是aService.foo附加到$scope并在标记中显式绑定到aService.foo时,不需要额外的$watch。好的。

    现在,假设应用了$digest循环,这一切都很好。在上面的示例中,我使用Angular的$interval服务来更新数组,每次更新后,该服务都会自动启动$digest循环。但是如果服务变量(无论出于什么原因)在"角度世界"中没有得到更新怎么办?换句话说,当服务属性发生变化时,我们不会自动激活$digest循环?好的。问题2:缺少EDOCX1[0]

    这里的许多解决方案都可以解决这个问题,但我同意代码窃听器:好的。

    The reason why we're using a framework like Angular is to not cook up our own observer patterns

    Ok.

    因此,我宁愿继续在HTML标记中使用aService.foo引用,如上面第二个示例所示,而不必在控制器中注册额外的回调。好的。解决方案2:使用带$rootScope.$apply()的setter和getter

    我很惊讶还没有人建议使用二传手和getter。这种能力是在ecmascript5中引入的,因此已经存在多年了。当然,这意味着,无论出于什么原因,如果您需要支持真正的老浏览器,那么这个方法将不起作用,但我觉得getter和setter在javascript中的使用非常少。在这种特殊情况下,它们可能非常有用:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    factory('aService', [
      '$rootScope',
      function($rootScope) {
        var realFoo = [];

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    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;
        }
      ]);
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <!DOCTYPE html>
    <html>

    <head>
      <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js">
      <link rel="stylesheet" href="style.css" />
      <script src="script.js">
    </head>

    <body ng-app="myApp">
     
        Using a Getter/Setter
        {{ item }}
     
    </body>

    </html>

    好的。

    在这里,我在服务函数中添加了一个"private"变量:realFoo。该get分别使用service对象上的get foo()set foo()函数进行更新和检索。好的。

    注意在set函数中使用$rootScope.$apply()。这样可以确保角可以知道service.foo的任何变化。如果出现"inprog"错误,请参阅此有用的参考页,或者如果使用angular>=1.3,则只需使用$rootScope.$applyAsync()。好的。

    如果aService.foo经常更新,也要注意这一点,因为这可能会显著影响性能。如果性能是一个问题,您可以使用setter设置一个类似于这里其他答案的观察者模式。好的。好啊。


    据我所知,你不必做那么复杂的事情。您已经将foo从服务分配给您的作用域,因为foo是一个数组(而对象又是通过引用分配的!)所以,你需要做的就是这样:

    1
    2
    3
    4
    function FooCtrl($scope, aService) {                                                                                                                              
      $scope.foo = aService.foo;

     }

    如果同一个ctrl中的某个变量取决于foo的变化,那么是的,您需要一个手表来观察foo并对该变量进行更改。但只要是一个简单的参考观察是不必要的。希望这有帮助。


    您可以将服务插入$rootscope并观察:

    1
    2
    3
    4
    5
    6
    myApp.run(function($rootScope, aService){
        $rootScope.aService = aService;
        $rootScope.$watch('aService', function(){
            alert('Watch');
        }, true);
    });

    在控制器中:

    1
    2
    3
    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+

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

    例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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");?


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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;
    });

    然后在控制器中:

    1
    2
    3
    4
    5
    6
    angular.module('MyApp').controller('aController', ['$rootScope', function ($rootScope) {

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

    通过这种方式,您将所有相关的工厂代码都放在其描述中,那么您只能依靠外部广播。


    =更新=

    非常简单,现在在$watch。

    钢笔在这里。

    HTML:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
        <p>
    FooController
    </p>
       
         
            <p>
    Send one item
    </p>
            <p>
    Send two items
    </p>
            <p>
    Send three items
    </p>
         
         
            <p>
    Send name: Sheldon
    </p>
            <p>
    Send name: Leonard
    </p>
            <p>
    Send name: Penny
    </p>
         
       
     

     
        <p>
    BarController
    </p>
        <p ng-if="name">Name is: {{ name }}
    </p>
        {{ item.name }}

    JavaScript:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    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 + '!');
      });
    }]);


    根据气味的回答,您可以使用类似于下面的内容,以确保您不会忘记注销回调…但有些人可能反对将$scope传递给服务。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    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是如下扩展方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
     * 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;
    };

    这是我的一般方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    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');
            }

        }]);

    In Your Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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'];

    对于像我这样只想寻找一个简单的解决方案的人来说,在控制器中使用普通的$watch几乎达到了你所期望的效果。唯一的区别是,它在JavaScript上下文中对字符串进行计算,而不是在特定范围内。您必须将$rootscope注入到您的服务中,尽管它只用于正确地钩住摘要循环。

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

    我来问这个问题,但结果发现我的问题是,当我应该使用angular$interval提供程序时,我使用的是setinterval。这也是setTimeout的情况(改用$timeout)。我知道这不是OP问题的答案,但它可能会帮助一些人,因为它帮助了我。


    我在另一个线程上发现了一个非常好的解决方案,它有一个相似的问题,但方法完全不同。源:当$rootscope值更改时,angularjs:$watch within指令不起作用

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

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

    我的模块/服务示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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状态。

    现在在我的例子中,在我使用的指令中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    angular.module('xxx').directive('directive', function (Auth, $rootScope) {
        return {
            ...
            link: function (scope, element, attrs) {
                ...
                $rootScope.$on('user:change', update);
            }
        };
    });

    现在,在指令中,我分别听取了$rootScope和给定的变化。非常简单优雅!


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
        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++;
        });
    });

    看一下这个破坏者:这是我能想到的最简单的例子。

    http://jsfiddle.net/hedjff/

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
            <input type="text" ng-model="Data.FirstName"><!-- Input entered here -->
            Input is : {{Data.FirstName}}<!-- Successfully updates here -->
       
       
       
            Input should also be here: {{Data.FirstName}}<!-- How do I automatically updated it here? -->
       




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

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

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

    //CTRL:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    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;
    });

    然后在控制器中:

    1
    2
    3
    4
    5
    function MyCtrl($scope, myService) {
        $scope.name = 'Superhero';
        $scope.myVar = false;
        myService.register($scope, 'myVar');
    }


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

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

    watch函数监视引用更改(基元类型),如果要监视类似array push的内容,只需使用:

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

    这将更新引用并从任何地方更新手表。包括服务getter方法。任何原语都将自动更新。


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

    控制器

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

    我想这会满足你的需要。我的控制器一直用这个实现检查我的服务的值。老实说,这比选择的答案简单得多。


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

    如果你想略过冗长的解释,你可以直接去jfiddle。

  • 观察对象
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    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"。有了手表服务,看起来会是这样的:

    1
    2
    3
    4
    5
    6
    7
    8
    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);
    }

  • 应用watch obj很好,但是如果您的服务中有异步代码,那么这是不够的。在这种情况下,我使用第二个实用程序,它看起来像:
  • 1
    2
    3
    4
    5
    6
    7
    mod.service('apply', ['$timeout', ApplyService]);

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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));
    }

    所以,所有这些看起来都是这样的(你可以运行它或者打开小提琴):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    // 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;
      };
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js">
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js">

      prop1: {{prop1}}
      prop2: {{prop2}}
      prop3 (unwatched): {{prop3}}
     
      <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>