首页 文章

AngularJS:如何观察服务变量?

提问于
浏览
401

我有服务,说:

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

这感觉很长,我一直在每个使用服务变量的控制器中重复它 . 有没有更好的方法来完成观察共享变量?

21 回答

  • 2

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

    在这样的场景中,多个/未知对象可能对更改感兴趣,请从正在更改的项目中使用 $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 ..
       });
    
     }]);
    

    其中一个好处是消除了多个 Watch . 如果您正在组合字段或派生上述示例中的值,则必须同时查看firstname和lastname属性 . 只有在更新时替换了用户对象时,才能看到getUser()函数,如果用户对象只更新了其属性,则不会触发它 . 在这种情况下,你必须做一个深度观察,这是更密集的 .

    $ broadcast将消息从它调用的范围发送到任何子范围 . 所以从$ rootScope调用它将触发每个范围 . 例如,如果您从控制器的范围进行$广播,则只会在从控制器范围继承的范围内触发 . $ emit方向相反,行为类似于DOM事件,因为它会使范围链冒泡 .

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

  • 8

    我使用与@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.

  • 0

    没有 Watch 或观察者回调(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'将引用window对象 .

  • 6

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

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

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

  • 1

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

    当一个角度表达式(例如您使用的角色表达式)存在于HTML中,Angular会自动为 $scope.foo 设置 $watch ,并且每当 $scope.foo 更改时都会更新HTML .

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

    这里未说明的问题是两件事中的一件影响 aService.foo ,这样就不会发现变化 . 这两种可能性是:

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

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


    问题1:过时的参考文献

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

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

    angular.module('myApp', [])
      .factory('aService', [
        '$interval',
        function($interval) {
          var service = {
            foo: []
          };
    
          // Create a new array on each update, appending the previous items and 
          // adding one new item each time
          $interval(function() {
            if (service.foo.length < 10) {
              var newArray = []
              Array.prototype.push.apply(newArray, service.foo);
              newArray.push(Math.random());
              service.foo = newArray;
            }
          }, 1000);
    
          return service;
        }
      ])
      .factory('aService2', [
        '$interval',
        function($interval) {
          var service = {
            foo: []
          };
    
          // Keep the same array, just add new items on each update
          $interval(function() {
            if (service.foo.length < 10) {
              service.foo.push(Math.random());
            }
          }, 1000);
    
          return service;
        }
      ])
      .controller('FooCtrl', [
        '$scope',
        'aService',
        'aService2',
        function FooCtrl($scope, aService, aService2) {
          $scope.foo = aService.foo;
          $scope.foo2 = aService2.foo;
        }
      ]);
    
    <!DOCTYPE html>
    <html>
    
    <head>
      <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
      <link rel="stylesheet" href="style.css" />
      <script src="script.js"></script>
    </head>
    
    <body ng-app="myApp">
      <div ng-controller="FooCtrl">
        <h1>Array changes on each update</h1>
        <div ng-repeat="item in foo">{{ item }}</div>
        <h1>Array is the same on each udpate</h1>
        <div ng-repeat="item in foo2">{{ item }}</div>
      </div>
    </body>
    
    </html>
    

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

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

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

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

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

    意思是,这样做:

    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 ,只要它发生变化,它就会在 $scope 上显式放置 foo . 将 aService 而不是 aService.foo 附加到 $scope 时,您不需要额外的 $watch ,并在标记中显式绑定到 aService.foo .


    现在,假设正在应用 $digest 循环,这一切都很好 . 在上面的示例中,我使用Angular的$interval服务来更新数组,这些数组会在每次更新后自动启动 $digest 循环 . 但是,如果服务变量(无论出于何种原因)未在"Angular world"内更新,该怎么办?换句话说,只要服务属性发生变化,我们就不会自动激活 $digest 循环?


    问题2:缺少$ digest

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

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

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

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

    我很惊讶没有人建议使用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>
    

    这里我在服务函数中添加了一个'private'变量: 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设置类似于其他答案的观察者模式 .

  • 2

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

    您可以观察工厂内部的变化,然后播出变化

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

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

  • 2

    ==UPDATED==

    非常简单现在$ 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 + '!');
      });
    }]);
    
  • 223

    dtheodor's 回答的基础上,您可以使用类似于下面的内容来确保您不会忘记取消注册回调...有些人可能会反对将 $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;
    };
    
  • 0

    这是我的通用方法 .

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

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

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

    面对一个非常类似的问题,我在范围内看了一个函数,并让函数返回服务变量 . 我创建了一个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++;
        });
    });
    
  • 2

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

  • 38

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

    Basically 解决方案告诉 NOT TO 使用 $watch 因为它是非常重的解决方案 . Instead 他们建议使用 $emit$on .

    我的问题是在 service 中观察变量并在 directive 中做出反应 . 而且用上面的方法很容易!

    我的模块/服务示例:

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

    现在在指令中我听了 $rootScopeon 给定的变化 - 我分别做出反应 . 非常轻松优雅!

  • 2

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

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

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

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

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

    我可能会有点迟,但这很简单 .

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

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

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

  • 6

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

    controller

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

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

  • 274

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

    如果你想跳过很长的解释,你可以去海峡jsfiddle

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

    此服务在控制器中以下列方式使用:假设您有一个服务“testService”,其属性为“prop1”,“prop2”,“prop3” . 您想要观察并分配范围'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);
    }
    
    • 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));
    }
    

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

    看看这个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;
    });
    

相关问题