AngularJS: Como assistir variáveis ​​de serviço?


414

Eu tenho um serviço, diga:

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

  return service;
}]);

E eu gostaria de usar foopara controlar uma lista que é renderizada em HTML:

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

Para que o controlador detecte quando aService.fooé atualizado, juntei esse padrão em que adiciono aService ao controlador $scopee depois uso $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;
    }
  });
}

Isso parece demorado, e eu tenho repetido isso em todos os controladores que usam as variáveis ​​do serviço. Existe uma maneira melhor de realizar a observação de variáveis ​​compartilhadas?


1
Você pode passar um terceiro parâmetro para $ watch definido como true para deep watch aService e todas as suas propriedades.
SirTophamHatt

7
$ scope.foo = aService.foo é suficiente, você pode perder a linha acima. E o que ele faz dentro $ relógio não faz sentido, se você quiser atribuir um novo valor para US $ scope.foo apenas fazê-lo ...
Jin

4
Você poderia apenas fazer referência aService.foona marcação html? (assim: plnkr.co/edit/aNrw5Wo4Q0IxR2loipl5?p=preview )
thetallweeks


1
@MikeGledhill, você está certo. Eu acho que é devido à natureza do Javascript, você pode ver esse padrão em muitos outros lugares (não apenas no Angular, mas no JS em geral). Por um lado, você transfere o valor (e não fica vinculado) e, por outro, transfere um objeto (ou o valor que faz referência ao objeto ...), e é por isso que as propriedades são atualizadas corretamente (como perfeitamente mostrado no exemplo de Zymotik acima).
Christophe Vidal

Respostas:


277

Você sempre pode usar o bom e velho padrão de observador se quiser evitar a tirania e a sobrecarga $watch.

No serviço:

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

E no controlador:

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

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

21
@Moo ouvir o $destoryevento sobre o alcance e adicione um método de cancelar o registro paraaService
Jamie

13
Quais são os profissionais dessa solução? Ele precisa de mais código em um serviço e um pouco da mesma quantidade de código em um controlador (já que também precisamos cancelar o registro em $ destroy). Eu poderia dizer sobre velocidade de execução, mas na maioria dos casos isso não importa.
21413 Alex

6
não tendo certeza de que essa é uma solução melhor do que $ watch, o questionador estava pedindo uma maneira simples de compartilhar os dados, e parece ainda mais complicado. Eu prefiro usar $ broadcast do que isso #
217 Jin Jin

11
$watchO padrão de observador vs é simplesmente escolher entre pesquisar ou enviar e é basicamente uma questão de desempenho; portanto, use-o quando o desempenho for importante. Uso o padrão de observador quando, caso contrário, teria que "aprofundar" observar objetos complexos. Anexo serviços inteiros ao escopo $ em vez de observar valores de serviço únicos. Eu evito o $ watch da angular como o diabo, isso acontece o suficiente nas diretivas e na ligação de dados angulares nativa.
dtheodor

107
A razão pela qual estamos usando uma estrutura como Angular é não criar nossos próprios padrões de observadores.
Code Whisperer

230

Em um cenário como este, em que vários objetos desconhecidos podem estar interessados ​​em alterações, use $rootScope.$broadcast partir do item que está sendo alterado.

Em vez de criar seu próprio registro de ouvintes (que precisam ser limpos com várias destruições de $), você deve poder $broadcast serviço em questão.

Você ainda deve codificar os $onmanipuladores em cada ouvinte, mas o padrão é dissociado de várias chamadas para$digest e, assim, evita o risco de observadores de longa duração.

Dessa forma, também, os ouvintes podem ir e vir do DOM e / ou escopos filho diferentes sem que o serviço altere seu comportamento.

** atualização: exemplos **

As transmissões fazem mais sentido nos serviços "globais" que podem afetar inúmeras outras coisas no seu aplicativo. Um bom exemplo é um serviço de usuário, no qual existem vários eventos que podem ocorrer, como logon, logout, atualização, ocioso etc. Eu acredito que é aqui que as transmissões fazem mais sentido porque qualquer escopo pode escutar um evento, sem mesmo injetando o serviço, e ele não precisa avaliar nenhuma expressão ou resultados de cache para inspecionar alterações. Ele é acionado e esquecido (por isso, verifique se é uma notificação de acionar e esquecer, não algo que exija ação)

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

O serviço acima transmitia uma mensagem para todos os escopos quando a função save () era concluída e os dados eram válidos. Como alternativa, se for um recurso $ ou um envio de ajax, mova a chamada de transmissão para o retorno de chamada para que seja acionada quando o servidor responder. As transmissões se adequam muito bem a esse padrão, porque todo ouvinte apenas espera o evento sem a necessidade de inspecionar o escopo em cada $ digest. O ouvinte ficaria assim:

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

 }]);

Um dos benefícios disso é a eliminação de vários relógios. Se você estivesse combinando campos ou obtendo valores como o exemplo acima, seria necessário observar as propriedades firstname e lastname. Observar a função getUser () funcionaria apenas se o objeto do usuário fosse substituído nas atualizações, não seria acionado se o objeto do usuário apenas tivesse suas propriedades atualizadas. Nesse caso, você teria que fazer uma inspeção profunda e isso é mais intenso.

$ broadcast envia a mensagem do escopo em que ela é chamada para qualquer escopo filho. Portanto, chamá-lo de $ rootScope será acionado em todos os escopos. Se você fosse $ broadcast do escopo do seu controlador, por exemplo, ele seria acionado apenas nos escopos herdados do escopo do seu controlador. $ emit vai na direção oposta e se comporta de maneira semelhante a um evento DOM, na medida em que borbulha a cadeia de escopo.

Lembre-se de que há cenários em que $ broadcast faz muito sentido e há cenários em que $ watch é uma opção melhor - especialmente se em um escopo isolado com uma expressão de observação muito específica.


1
Sair do ciclo $ digest é uma coisa boa, especialmente se as alterações que você estiver assistindo não forem um valor que irá direta e imediatamente para o DOM.
XML

Existe alguma maneira de evitar o método .save (). Parece exagero quando você está apenas monitorando a atualização de uma única variável no sharedService. Podemos assistir a variável de dentro do sharedService e transmitir quando ela muda?
JerryKur

Tentei várias maneiras de compartilhar dados entre controladores, mas essa é a única que funcionou. Bem jogado, senhor.
abettermap 28/02

Eu prefiro este para as outras respostas, parece menos hacky , graças
JMK

9
Esse é o padrão de design correto apenas se o seu controlador consumidor tiver várias fontes possíveis de dados; em outras palavras, se você tiver uma situação MIMO (Multiple Input / Multiple Output). Se você está apenas usando um padrão de um para muitos, deve usar a referência direta a objetos e permitir que a estrutura Angular faça a ligação bidirecional para você. Horkyze ligada isso abaixo, e é uma boa explicação do automático de ligação de duas vias, e é limitações: stsc3000.github.io/blog/2013/10/26/...
Charles

47

Estou usando uma abordagem semelhante à @dtheodot, mas usando promessa angular em vez de passar retornos de chamada

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

Então, sempre que usar myService.setFoo(foo), use o método para atualizar o fooserviço. No seu controlador, você pode usá-lo como:

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

Os dois primeiros argumentos de thensão retornos de chamada de sucesso e erro, o terceiro é notificar retorno de chamada.

Referência para $ q.


Qual seria a vantagem desse método em relação à transmissão de $ descrita abaixo por Matt Pileggi?
Fabio

Bem, ambos os métodos têm seus usos. As vantagens da transmissão para mim seriam legibilidade humana e possibilidade de ouvir em mais locais o mesmo evento. Eu acho que a principal desvantagem é que a transmissão está emitindo mensagem para todos os escopos descendentes, por isso pode ser um problema de desempenho.
Krym

2
Eu estava tendo um problema em que executar $scope.$watchuma variável de serviço não parecia funcionar (o escopo que eu estava assistindo era um modal herdado de $rootScope) - isso funcionou. Truque legal, obrigado por compartilhar!
Seiyria

4
Como você se limparia com essa abordagem? É possível remover o retorno de chamada registrado da promessa quando o escopo é destruído?
Abris

Boa pergunta. Sinceramente, não sei. Vou tentar fazer alguns testes sobre como você pode remover a notificação de retorno de chamada da promessa.
Krym

41

Sem relógios ou retornos de chamada do observador ( 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>

Esse JavaScript funciona quando estamos devolvendo um objeto do serviço, e não um valor. Quando um objeto JavaScript é retornado de um serviço, o Angular adiciona relógios a todas as suas propriedades.

Observe também que estou usando 'var self = this', pois preciso manter uma referência ao objeto original quando o tempo limite $ for executado, caso contrário, 'this' se referirá ao objeto da janela.


3
Essa é uma ótima abordagem! Existe uma maneira de vincular apenas uma propriedade de um serviço ao escopo em vez de todo o serviço? Apenas fazer $scope.count = service.countnão funciona.
jvannistelrooy

Você também pode aninhar a propriedade dentro de um objeto (arbitrário) para que seja passado por referência. $scope.data = service.data <p>Count: {{ data.count }}</p>
Alex Ross

1
Excelente abordagem! Embora existam muitas respostas funcionais e fortes nesta página, essa é de longe a) a mais fácil de implementar eb) a mais fácil de entender ao ler o código. Esta resposta deve ser muito maior do que é atualmente.
CodeMoose

Obrigado @CodeMoose, eu simplifiquei ainda mais hoje os novos no AngularJS / JavaScript.
Zymotik 16/10/2015

2
Que Deus te abençoe. Eu perdi milhões de horas, eu diria. Porque eu estava lutando com 1,5 e AngularJS mudando 1-2 e também queria compartilhar dados
Amna

29

Eu me deparei com essa pergunta procurando algo semelhante, mas acho que ela merece uma explicação completa do que está acontecendo, além de algumas soluções adicionais.

Quando uma expressão angular como a que você usou está presente no HTML, o Angular configura automaticamente um $watchpara $scope.fooe atualiza o HTML sempre $scope.fooque for alterado.

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

O problema não dito aqui é que uma das duas coisas está afetando, de aService.foo modo que as alterações não são detectadas. Essas duas possibilidades são:

  1. aService.foo está sendo definido para uma nova matriz a cada vez, fazendo com que a referência a ela fique desatualizada.
  2. aService.fooestá sendo atualizado de forma que um $digestciclo não seja acionado na atualização.

Problema 1: referências desatualizadas

Considerando a primeira possibilidade, assumindo que a $digestestá sendo aplicada, se aService.foosempre foi a mesma matriz, o conjunto automaticamente$watch detectaria as alterações, conforme mostrado no snippet de código abaixo.

Solução 1-a: verifique se a matriz ou o objeto é o mesmo objeto em cada atualização

Como você pode ver, o ng-repeat supostamente anexado ao aService.foonão é atualizado quando aService.foomuda, mas o ng-repeat anexado ao aService2.foo faz . Isso ocorre porque nossa referência a aService.fooestá desatualizada, mas nossa referência a aService2.foonão é. Criamos uma referência à matriz inicial com $scope.foo = aService.foo;, que foi descartada pelo serviço na próxima atualização, o que significa$scope.foo não faz mais referência à matriz que queríamos mais.

No entanto, embora existam várias maneiras de garantir que a referência inicial seja mantida intacta, às vezes pode ser necessário alterar o objeto ou a matriz. Ou, e se a propriedade service fizer referência a um primitivo como a Stringou Number? Nesses casos, não podemos simplesmente confiar em uma referência. Então, o que pode fazer?

Várias das respostas dadas anteriormente já fornecem algumas soluções para esse problema. No entanto, sou pessoalmente a favor do uso do método simples sugerido por Jin e nas semanas seguintes nos comentários:

basta referenciar aService.foo na marcação html

Solução 1-b: anexe o serviço ao escopo e faça referência {service}.{property}no HTML.

Significado, basta fazer o seguinte:

HTML:

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

JS:

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

Dessa forma, a $watchresolução será resolvida aService.fooem cada$digest , obtendo o valor atualizado corretamente.

Isso é o que você estava tentando fazer com a solução alternativa, mas de uma maneira muito menos complicada. Você adicionou uma desnecessária $watchno controlador que coloca explicitamente foona $scopesempre que muda. Você não precisa desse extra $watchquando anexar em aServicevez de aService.fooao $scopee vincular explicitamente à aService.foomarcação.


Agora está tudo bem desde que um $digestciclo esteja sendo aplicado. Nos meus exemplos acima, usei o $intervalserviço da Angular para atualizar as matrizes, que inicia automaticamente um $digestloop após cada atualização. Mas e se as variáveis ​​de serviço (por qualquer motivo) não estiverem sendo atualizadas dentro do "mundo Angular". Em outras palavras, não temos um $digestciclo sendo ativado automaticamente sempre que a propriedade do serviço muda?


Problema 2: ausente $digest

Muitas das soluções aqui resolverão esse problema, mas eu concordo com o Code Whisperer :

A razão pela qual estamos usando uma estrutura como Angular é não criar nossos próprios padrões de observadores

Portanto, eu preferiria continuar usando o aService.foo referência na marcação HTML, como mostrado no segundo exemplo acima, e não precisar registrar um retorno de chamada adicional no Controller.

Solução 2: use um levantador e um levantador com $rootScope.$apply()

Fiquei surpreso que ninguém ainda tenha sugerido o uso de um setter e getter . Esse recurso foi introduzido no ECMAScript5 e, portanto, existe há anos. Claro, isso significa que, por qualquer motivo, você precisa oferecer suporte a navegadores realmente antigos, esse método não funcionará, mas eu sinto que getters e setters são muito subutilizados no JavaScript. Nesse caso em particular, eles podem ser bastante úteis:

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

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

Aqui eu adicionei uma variável 'privada' na função de serviço: realFoo. Este get é atualizado e recuperado usando as funções get foo()e set foo()respectivamente noservice objeto.

Observe o uso de $rootScope.$apply()na função definida. Isso garante que o Angular esteja ciente de quaisquer alterações no service.foo. Se você receber erros 'inprog', consulte esta página de referência útil ou se você usa Angular> = 1.3, pode simplesmente usar $rootScope.$applyAsync().

Também tenha cuidado com isso, se aService.fooestiver sendo atualizado com muita frequência, pois isso pode afetar significativamente o desempenho. Se o desempenho for um problema, você pode configurar um padrão de observador semelhante às outras respostas aqui usando o configurador.


3
Esta é a solução correta e mais fácil. Como o @NanoWizard diz, o $ digest procura servicesnão propriedades que pertencem ao próprio serviço.
Sarpdoruk Tahmaz 02/12/2015

28

Até onde eu sei, você não precisa fazer algo tão elaborado quanto isso. Você já atribuiu foo do serviço ao seu escopo e, como foo é uma matriz (e, por sua vez, um objeto, ele é atribuído por referência!). Então, tudo o que você precisa fazer é algo como isto:

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

 }

Se alguma outra variável nesse mesmo Ctrl depende da mudança de foo, então sim, você precisaria de um relógio para observar foo e fazer alterações nessa variável. Mas desde que seja uma simples referência, a observação é desnecessária. Espero que isto ajude.


35
Eu tentei e não consegui $watchtrabalhar com um primitivo. Em vez disso, eu definido um método do serviço que iria devolver o valor primitivo: somePrimitive() = function() { return somePrimitive }E eu atribuída uma propriedade $ escopo para esse método: $scope.somePrimitive = aService.somePrimitive;. Então eu usei o método âmbito do HTML: <span>{{somePrimitive()}}</span>
Mark Rajcok

4
@MarkRajcok Não, não use primitivos. Adicione-os em um objeto. Primitivos não são mutables e dados, de modo 2way ligação não funcionará
Jimmy Kane

3
@ JimmyKane, sim, as primitivas não devem ser usadas para ligação de dados bidirecional, mas acho que a pergunta era sobre observar variáveis ​​de serviço, não configurar a ligação bidirecional. Se você precisar assistir apenas uma propriedade / variável de serviço, um objeto não será necessário - uma primitiva pode ser usada.
fácil

3
Nesta configuração, sou capaz de alterar os valores de aService do escopo. Mas o escopo não muda em resposta à mudança do aService.
Ouwen Huang

4
Isso também não funciona para mim. Simplesmente atribuir $scope.foo = aService.foonão atualiza a variável de escopo automaticamente.
Darwin tecnologia

9

Você pode inserir o serviço no $ rootScope e assistir:

myApp.run(function($rootScope, aService){
    $rootScope.aService = aService;
    $rootScope.$watch('aService', function(){
        alert('Watch');
    }, true);
});

No seu controlador:

myApp.controller('main', function($scope){
    $scope.aService.foo = 'change';
});

Outra opção é usar uma biblioteca externa como: https://github.com/melanke/Watch.JS

Funciona com: IE 9+, FF 4+, SF 5+, WebKit, CH 7+, OP 12+, BESEN, Node.JS, Rhino 1.7+

Você pode observar as alterações de um, muitos ou todos os atributos do objeto.

Exemplo:

var ex3 = {
    attr1: 0,
    attr2: "initial value of attr2",
    attr3: ["a", 3, null]
};   
watch(ex3, function(){
    alert("some attribute of ex3 changes!");
});
ex3.attr3.push("new value");​

2
Não acredito que essa resposta não é a mais votada !!! Essa é a solução mais elegante (IMO), pois reduz a entropia de informações e provavelmente atenua a necessidade de manipuladores de mediação adicionais. Eu votaria isso mais se eu pudesse ...
Cody

Adicionando todos os seus serviços ao $ rootScope, seus benefícios e possíveis armadilhas, são detalhados aqui: stackoverflow.com/questions/14573023/…
Zymotik

6

Você pode assistir as alterações dentro da própria fábrica e depois transmitir uma alteração

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

Então no seu controlador:

angular.module('MyApp').controller('aController', ['$rootScope', function ($rootScope) {

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

Dessa maneira, você coloca todo o código de fábrica relacionado em sua descrição e só pode confiar na transmissão externa


6

== ATUALIZADO ==

Muito simples agora em $ watch.

Caneta aqui .

HTML:

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

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

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

</div>

JavaScript:

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

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

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

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

1
Eu gosto do PostmanService, mas como devo alterar a função $ watch no controlador se precisar ouvir mais de uma variável?
jedi

Oi Jedi, obrigado pelo aviso! Eu atualizei a caneta e a resposta. Eu recomendo adicionar outra função de relógio para isso. Então, adicionei um novo recurso ao PostmanService. Espero que isso ajude :)
hayatbiralem

Na verdade, sim, é :) Se você compartilhar mais detalhes do problema, talvez eu possa ajudá-lo.
hayatbiralem

4

Com base na resposta do dtheodor, você pode usar algo semelhante ao abaixo para garantir que você não esqueça de cancelar o registro do retorno de chamada ... $scopePorém, alguns podem se opor a passar o para um serviço.

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 é um método de extensão que se parece com isso:

/**
 * Removes the given item the current array.
 *
 * @param  {Object}  item   The item to remove.
 * @return {Boolean}        True if the item is removed.
 */
Array.prototype.remove = function (item /*, thisp */) {
    var idx = this.indexOf(item);

    if (idx > -1) {
        this.splice(idx, 1);

        return true;
    }
    return false;
};

2

Aqui está minha abordagem genérica.

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

    }]);

No seu controlador

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

    $scope._initWatchers = function() {
        aService.watch('foo', $scope._onFooChange);
    }

    $scope._onFooChange = function(newValue) {
        $scope.foo = newValue;
    }

    $scope._initWatchers();

}

FooCtrl.$inject = ['$scope', 'aService'];

2

Para aqueles como eu, apenas procurando uma solução simples, isso faz quase exatamente o que você espera do uso normal de $ watch nos controladores. A única diferença é que ele avalia a string no seu contexto javascript e não em um escopo específico. Você precisará injetar $ rootScope em seu serviço, embora ele seja usado apenas para se conectar aos ciclos de compilação corretamente.

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

2

enquanto enfrentava um problema muito semelhante, assisti a uma função no escopo e a função retornou a variável de serviço. Eu criei um violino js . você pode encontrar o código abaixo.

    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

Eu vim para essa pergunta, mas o problema foi que eu estava usando setInterval quando deveria estar usando o provedor de intervalo angular $. Este também é o caso de setTimeout (use $ timeout). Sei que não é a resposta para a pergunta do OP, mas pode ajudar alguns, como me ajudou.


Você pode usar setTimeout, ou qualquer outra função não-Angular, mas não se esqueça de incluir o código no retorno de chamada $scope.$apply().
magnetronnie

2

Eu encontrei uma solução realmente ótima no outro thread com um problema semelhante, mas uma abordagem totalmente diferente. Fonte: AngularJS: $ watch dentro da diretiva não está funcionando quando o valor $ rootScope é alterado

Basicamente, a solução não diz para NÃO usar $watch, pois é uma solução muito pesada. Em vez disso, eles propõem usar $emite $on.

Meu problema era assistir uma variável em meu serviço e reagir na diretiva . E com o método acima, é muito fácil!

Meu exemplo de módulo / serviço:

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

Então, basicamente, assisto meu user - sempre que definido como novo valor, $emitum user:changestatus.

Agora, no meu caso, na diretiva eu usei:

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

Agora na directiva eu escuto na $rootScopee sobre a mudança dada - reajo respectivamente. Muito fácil e elegante!


1

// service: (nada de especial aqui)

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

// ctrl:

myApp.controller('MyCtrl', function($scope, myService) {

  $scope.someVariable = myService.someVariable;

  // watch the service and update this ctrl...
  $scope.$watch(function(){
    return myService.someVariable;
  }, function(newValue){
    $scope.someVariable = newValue;
  });
});

1

Um pouco feio, mas adicionei o registro de variáveis ​​de escopo ao meu serviço para alternar:

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

E então no controlador:

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

Obrigado. Uma pequena pergunta: por que você volta thisdesse serviço em vez de self?
shrekuu 12/11

4
Porque erros são cometidos às vezes. ;-)
nclu 12/11/14

Boa prática para return this;fora de seus construtores ;-)
Cody

1

Dê uma olhada neste plunker :: este é o exemplo mais simples que eu poderia pensar

http://jsfiddle.net/HEdJF/

<div ng-app="myApp">
    <div ng-controller="FirstCtrl">
        <input type="text" ng-model="Data.FirstName"><!-- Input entered here -->
        <br>Input is : <strong>{{Data.FirstName}}</strong><!-- Successfully updates here -->
    </div>
    <hr>
    <div ng-controller="SecondCtrl">
        Input should also be here: {{Data.FirstName}}<!-- How do I automatically updated it here? -->
    </div>
</div>



// declare the app with no dependencies
var myApp = angular.module('myApp', []);
myApp.factory('Data', function(){
   return { FirstName: '' };
});

myApp.controller('FirstCtrl', function( $scope, Data ){
    $scope.Data = Data;
});

myApp.controller('SecondCtrl', function( $scope, Data ){
    $scope.Data = Data;
});

0

Eu vi alguns padrões terríveis de observadores aqui que causam vazamentos de memória em aplicativos grandes.

Eu posso me atrasar um pouco, mas é tão simples assim.

A função watch observa as alterações de referência (tipos primitivos) se você quiser assistir algo como push de matriz, basta usar:

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

Isso atualizará a referência e o relógio de qualquer lugar. Incluindo um método de obtenção de serviços. Qualquer coisa que seja primitiva será atualizada automaticamente.


0

Estou atrasado para a parte, mas achei uma maneira melhor de fazer isso do que a resposta postada acima. Em vez de atribuir uma variável para armazenar o valor da variável de serviço, criei uma função anexada ao escopo, que retorna a variável de serviço.

controlador

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

Eu acho que isso fará o que você quiser. Meu controlador continua verificando o valor do meu serviço com esta implementação. Honestamente, isso é muito mais simples que a resposta selecionada.


por que foi rebaixado. Eu também usei técnicas semelhantes muitas vezes e funcionou.
indefinido

0

Escrevi dois serviços utilitários simples que me ajudam a rastrear alterações nas propriedades do serviço.

Se você quiser pular a longa explicação, pode ir direto ao jsfiddle

  1. WatchObj

mod.service('WatchObj', ['$rootScope', WatchObjService]);

function WatchObjService($rootScope) {
  // returns watch function
  // obj: the object to watch for
  // fields: the array of fields to watch
  // target: where to assign changes (usually it's $scope or controller instance)
  // $scope: optional, if not provided $rootScope is use
  return function watch_obj(obj, fields, target, $scope) {
    $scope = $scope || $rootScope;
    //initialize watches and create an array of "unwatch functions"
    var watched = fields.map(function(field) {
      return $scope.$watch(
        function() {
          return obj[field];
        },
        function(new_val) {
          target[field] = new_val;
        }
      );
    });
    //unregister function will unregister all our watches
    var unregister = function unregister_watch_obj() {
      watched.map(function(unregister) {
        unregister();
      });
    };
    //automatically unregister when scope is destroyed
    $scope.$on('$destroy', unregister);
    return unregister;
  };
}

Este serviço é usado no controlador da seguinte maneira: Suponha que você tenha um serviço "testService" com as propriedades 'prop1', 'prop2', 'prop3'. Você deseja assistir e atribuir ao escopo 'prop1' e 'prop2'. Com o serviço de relógio, ficará assim:

app.controller('TestWatch', ['$scope', 'TestService', 'WatchObj', TestWatchCtrl]);

function TestWatchCtrl($scope, testService, watch) {
  $scope.prop1 = testService.prop1;
  $scope.prop2 = testService.prop2;
  $scope.prop3 = testService.prop3;
  watch(testService, ['prop1', 'prop2'], $scope, $scope);
}

  1. O Watch obj é ótimo, mas não é suficiente se você tiver código assíncrono em seu serviço. Nesse caso, eu uso um segundo utilitário que se parece com isso:

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

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

Eu o acionaria no final do meu código assíncrono para acionar o loop $ digest. Curtiu isso:

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

Então, tudo isso ficará assim (você pode executá-lo ou abrir o violino ):

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

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.