AngularJS: Как смотреть служебные переменные?

У меня есть сервис, скажем:

factory('aService', ['$rootScope', '$resource', function ($rootScope, $resource) {
  var service = {
    foo: []
  };

  return service;
}]);

И я хотел бы использовать foo для управления списком, который отображается в HTML:

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

Чтобы контроллер обнаружил, когда обновляется aService.foo, я объединил этот шаблон, где я добавляю aService к контроллеру $scope, а затем используйте $scope.$watch():

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

  $scope.$watch('aService.foo', function (newVal, oldVal, scope) {
    if(newVal) { 
      scope.foo = newVal;
    }
  });
}

Это кажется длинным, и я повторяю его в каждом контроллере, который использует служебные переменные. Есть ли лучший способ для просмотра общих переменных?

Ответ 1

Вы всегда можете использовать старый добрый шаблон наблюдателя, если хотите избежать тирании и накладных расходов $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
};

Ответ 2

В сценарии, подобном этому, где несколько /unkown объектов могут быть заинтересованы в изменениях, используйте $rootScope.$broadcast из изменяемого элемента.

Вместо того, чтобы создавать собственный реестр слушателей (которые нужно очистить при различных $-двоих), вы должны иметь возможность $broadcast от рассматриваемой службы.

Вы должны все-таки кодировать обработчики $on в каждом слушателе, но шаблон отделяется от нескольких вызовов до $digest и, таким образом, избегает риска длительных наблюдателей.

Таким образом, слушатели могут приходить и уходить из DOM и/или разных дочерних областей без изменения службы, изменяющей ее поведение.

** update: примеры **

Трансляции будут иметь наибольший смысл в "глобальных" сервисах, которые могут повлиять на множество других вещей в вашем приложении. Хорошим примером является пользовательский сервис, в котором есть несколько событий, которые могут иметь место, например, вход в систему, выход из системы, обновление, простоя и т.д. Я считаю, что это означает, что трансляции имеют наибольший смысл, поскольку любая область может прослушивать событие без даже вводя службу, и не нужно оценивать какие-либо выражения или результаты кэша для проверки изменений. Он просто срабатывает и забывает (поэтому убедитесь, что это уведомление о пожаре и забывании, а не что-то, что требует действия)

.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, поскольку он пузырится по цепочке областей видимости.

Имейте в виду, что существуют сценарии, в которых широковещательная передача $имеет большой смысл, и есть сценарии, где $watch - лучший вариант, особенно если в области выделения с очень конкретным выражением часов.

Ответ 3

Я использую подобный подход как @dtheodot, но используя angular обещание вместо передачи обратных вызовов

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

    this.foo = 0;

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

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

Затем везде, где просто используйте метод myService.setFoo(foo) для обновления foo на службе. В вашем контроллере вы можете использовать его как:

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

Первые два аргумента then являются успешными и обратными вызовами ошибок, третий - уведомлением об обратном вызове.

Ссылка для $q.

Ответ 4

Без часов или обратных вызовов наблюдателя (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 timeout, иначе 'this' будет ссылаться на объект окна.

Ответ 5

Насколько я могу судить, вам не нужно делать что-то столь же сложное. Вы уже назначили foo из службы в свою область действия, и поскольку foo - это массив (и, в свою очередь, объект назначается ссылкой!). Итак, все, что вам нужно сделать, это что-то вроде этого:

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

 }

Если какая-то другая переменная в этом же Ctrl зависит от изменения foo, то да, вам понадобятся часы для наблюдения foo и внесения изменений в эту переменную. Но до тех пор, пока это простое наблюдение, не нужно. Надеюсь это поможет.

Ответ 6

Я наткнулся на этот вопрос, ища что-то подобное, но я думаю, что он заслуживает подробного объяснения того, что происходит, а также моего решения.

Когда выражение angular, такое как тот, который вы использовали, присутствует в HTML, angular автоматически устанавливает $watch для $scope.foo и обновляет HTML при каждом изменении $scope.foo.

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

Ответ 7

Вы можете вставить услугу в $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");​

Ответ 8

Вы можете просматривать изменения внутри самого factory и затем транслировать изменение

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

Таким образом вы помещаете весь связанный код factory в его описание, тогда вы можете полагаться только на трансляцию извне

Ответ 9

== == ОБНОВЛЕНО

Очень просто сейчас в $watch.

Здесь пером.

HTML:

<div class="container" data-ng-app="app">

  <div class="well" data-ng-controller="FooCtrl">
    <p><strong>FooController</strong></p>
    <div class="row">
      <div class="col-sm-6">
        <p><a href="" ng-click="setItems([ { name: 'I am single item' } ])">Send one item</a></p>
        <p><a href="" ng-click="setItems([ { name: 'Item 1 of 2' }, { name: 'Item 2 of 2' } ])">Send two items</a></p>
        <p><a href="" ng-click="setItems([ { name: 'Item 1 of 3' }, { name: 'Item 2 of 3' }, { name: 'Item 3 of 3' } ])">Send three items</a></p>
      </div>
      <div class="col-sm-6">
        <p><a href="" ng-click="setName('Sheldon')">Send name: Sheldon</a></p>
        <p><a href="" ng-click="setName('Leonard')">Send name: Leonard</a></p>
        <p><a href="" ng-click="setName('Penny')">Send name: Penny</a></p>
      </div>
    </div>
  </div>

  <div class="well" data-ng-controller="BarCtrl">
    <p><strong>BarController</strong></p>
    <p ng-if="name">Name is: {{ name }}</p>
    <div ng-repeat="item in items">{{ item.name }}</div>
  </div>

</div>

JavaScript:

var app = angular.module('app', []);

app.factory('PostmanService', function() {
  var Postman = {};
  Postman.set = function(key, val) {
    Postman[key] = val;
  };
  Postman.get = function(key) {
    return Postman[key];
  };
  Postman.watch = function($scope, key, onChange) {
    return $scope.$watch(
      // This function returns the value being watched. It is called for each turn of the $digest loop
      function() {
        return Postman.get(key);
      },
      // This is the change listener, called when the value returned from the above function changes
      function(newValue, oldValue) {
        if (newValue !== oldValue) {
          // Only update if the value changed
          $scope[key] = newValue;
          // Run onChange if it is function
          if (angular.isFunction(onChange)) {
            onChange(newValue, oldValue);
          }
        }
      }
    );
  };
  return Postman;
});

app.controller('FooCtrl', ['$scope', 'PostmanService', function($scope, PostmanService) {
  $scope.setItems = function(items) {
    PostmanService.set('items', items);
  };
  $scope.setName = function(name) {
    PostmanService.set('name', name);
  };
}]);

app.controller('BarCtrl', ['$scope', 'PostmanService', function($scope, PostmanService) {
  $scope.items = [];
  $scope.name = '';
  PostmanService.watch($scope, 'items');
  PostmanService.watch($scope, 'name', function(newVal, oldVal) {
    alert('Hi, ' + newVal + '!');
  });
}]);

Ответ 10

На основе ответа 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;
};

Ответ 11

Вот мой общий подход.

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

Ответ 12

столкнувшись с очень похожими проблемами, я просмотрел функцию в области видимости и возвратил функцию в переменную службы. Я создал 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++;
    });
});

Ответ 13

Я пришел к этому вопросу, но оказалось, что моя проблема заключалась в том, что я использовал setInterval, когда я должен был использовать поставщик angular $interval. Это также относится к setTimeout (вместо этого используется $timeout). Я знаю, что это не ответ на вопрос ОП, но он может помочь некоторым, поскольку это помогло мне.

Ответ 14

Я нашел действительно отличное решение в другом потоке с аналогичной проблемой, но совершенно другой подход. Источник: AngularJS: $watch внутри директивы не работает, когда изменяется значение $rootScope

В основном решение там говорит НЕ TO использовать $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 a user:change.

Теперь в моем случае в директиве я использовал:

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

Теперь в директиве я слушаю $rootScope и on данное изменение - я реагирую соответственно. Очень легкий и элегантный!

Ответ 15

Немного уродливый, но я добавил регистрацию переменных области для моей службы для переключения:

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

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

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

    return this;
});

И затем в контроллере:

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

Ответ 16

Для таких, как я, просто ищет простое решение, это почти то, что вы ожидаете от использования обычных $watch в контроллерах. Единственное различие заключается в том, что он оценивает строку в контексте javascript, а не в определенной области. Вам нужно будет ввести $rootScope в свою службу, хотя он используется только для правильного подключения к дайджесту.

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

Ответ 17

//service: (здесь ничего особенного)

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

Ответ 18

Я видел здесь несколько ужасных шаблонов наблюдателей, которые вызывают утечку памяти в больших приложениях.

Возможно, я немного опоздаю, но это так просто, как это.

Функция часов отслеживает изменения ссылок (примитивные типы), если вы хотите посмотреть что-то вроде push массива, просто используйте:

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

Это обновит ссылку и обновит часы из любого места. Включая метод getter услуг. Все, что примитив будет обновляться автоматически.

Ответ 19

Я опаздываю на эту часть, но я нашел более удобный способ сделать это, чем ответ, опубликованный выше. Вместо того, чтобы назначать переменную для хранения значения служебной переменной, я создал функцию, прикрепленную к области, которая возвращает служебную переменную.

контроллер

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

Я думаю, что это будет делать то, что вы хотите. Мой контроллер продолжает проверять ценность моего сервиса с этой реализацией. Честно говоря, это намного проще, чем выбранный ответ.

Ответ 20

Я написал две простые служебные службы, которые помогают мне отслеживать изменения свойств службы.

Если вы хотите пропустить длинное объяснение, вы можете перейти к 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 $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;
  };
}

Ответ 21

Посмотрите на этот плункер:: это самый простой пример, который я мог бы придумать

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