AngularJS:サービス変数を監視する方法は?


414

私はサービスを持っています:

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に3番目のパラメーターをtrueに設定して、aServiceとそのすべてのプロパティを詳細に監視できます。
SirTophamHatt 2013年

7
$ scope.foo = aService.fooで十分です。上の行を失う可能性があります。そして、それはあなたがちょうどそれを行う$ scope.fooに新しい値を代入したい場合は...、意味がありません$時計内部で何をするか
ジン

4
あなただけaService.fooのHTMLマークアップで参照できますか?(このように:plnkr.co/edit/aNrw5Wo4Q0IxR2loipl5?p=preview
thetallweeks

1
私は(下の答えを参照してください、コールバックまたは$の時計のない例を追加したjsfiddle.net/zymotik/853wvv7s
Zymotik

1
@MikeGledhill、あなたは正しい。これはJavaScriptの性質によるものだと思います。このパターンは、他の多くの場所(Angularだけでなく、JS全般で)で確認できます。一方では値を転送し(バインドされません)、他方ではオブジェクト(またはオブジェクトを参照する値...)を転送します。そのため、プロパティは正しく更新されます(完全に上記のZymotikの例に示されています)。
Christophe Vidal

回答:


277

の専制とオーバーヘッドを回避したい場合は、常に古き良きオブザーバーパターンを使用できます$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
};

21
@Moo $destoryはスコープでイベントをリッスンし、登録解除メソッドを追加しますaService
Jamie

13
このソリューションの長所は何ですか?サービスにはより多くのコードが必要であり、コントローラーには多少同じ量のコードが必要です($ destroyでも登録解除する必要があるため)。実行速度については言えるかもしれませんが、ほとんどの場合それは問題ではありません。
Alex Che

6
これが$ watchよりも優れたソリューションであるかどうかは不明ですが、質問者はデータを共有する簡単な方法を求めていましたが、さらに面倒に見えます。これよりも$ broadcastを使用したい
Jin

11
$watchvsオブザーバーパターンは、単にポーリングするかプッシュするかを選択するだけであり、基本的にはパフォーマンスの問題であるため、パフォーマンスが重要な場合に使用します。それ以外の場合は複雑なオブジェクトを「深く」監視する必要がある場合に、オブザーバーパターンを使用します。単一のサービス値を監視する代わりに、サービス全体を$ scopeにアタッチします。悪魔のようなAngularの$ watchは避けます。ディレクティブやネイティブのAngularデータバインディングで発生することは十分あります。
dtheodor 2014

107
Angularのようなフレームワークを使用しているのは、独自のオブザーバーパターンを作成しないためです。
コードウィスパー14

230

このようなシナリオで、複数の不明なオブジェクトが変更に関心がある可能性がある場合$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 ..
   });

 }]);

この利点の1つは、複数の時計が不要になることです。上記の例のようにフィールドを組み合わせたり、値を導出したりする場合は、firstnameプロパティとlastnameプロパティの両方を監視する必要があります。getUser()関数の監視は、ユーザーオブジェクトが更新時に置き換えられた場合にのみ機能し、ユーザーオブジェクトのプロパティが更新されただけの場合は機能しません。その場合、深い監視をする必要があり、それはより集中的です。

$ broadcastは、呼び出されたスコープからメッセージを子スコープに送信します。したがって、$ rootScopeから呼び出すと、すべてのスコープで起動します。たとえば、コントローラーのスコープから$ broadcastする場合、コントローラーのスコープから継承するスコープでのみ発生します。$ emitは逆の方向に進み、スコープチェーンを浮き上がらせるという点でDOMイベントと同様に動作します。

$ broadcastが多くの意味を成すシナリオがあり、特に$ watchがより適切なオプションであるシナリオがあることに注意してください-特に、非常に具体的なウォッチ式を持つ分離スコープの場合。


1
$ digestサイクルから抜け出すことは良いことです。特に、監視している変更が直接そしてすぐにDOMに入る値でない場合はなおさらです。
XML

.save()メソッドを回避する方法はありますか?sharedServiceで単一の変数の更新を監視しているだけでは、やりすぎのようです。sharedService内から変数を監視し、変更時にブロードキャストできますか?
JerryKur 2014年

コントローラー間でデータを共有する方法をいくつか試しましたが、これが機能した唯一の方法です。よく演奏されました。
abettermap 2015

私は小さいと思われる、他の回答にこれを好むハック、感謝
JMK

9
これは、使用しているコントローラーに複数の可能なデータソースがある場合にのみ正しい設計パターンです。つまり、MIMOの状況(複数入力/複数出力)がある場合です。1対多のパターンを使用している場合は、直接オブジェクト参照を使用し、Angularフレームワークに双方向バインディングを実行させる必要があります。Horkyzeは、以下にこれをリンクし、それが自動的に双方向結合の良い説明だし、それが限界だ:stsc3000.github.io/blog/2013/10/26/...
チャールズ

47

私は@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;
})

最初の2つの引数thenは成功とエラーのコールバック、3番目の引数は通知コールバックです。

$ qのリファレンス。


Matt Pileggiが以下に説明する$ broadcastよりもこの方法の利点は何でしょうか?
ファビオ

どちらの方法にも用途があります。私にとってのブロードキャストの利点は、人間が読みやすく、同じイベントのより多くの場所で聴くことができることです。主な欠点は、ブロードキャストがすべての子孫スコープにメッセージを送信しているため、パフォーマンスの問題になる可能性があることです。
Krym、2014

2
$scope.$watchサービス変数での操作が機能していないように見える問題がありました(私が監視しているスコープはから継承したモーダルでした$rootScope)-これは機能しました。クールなトリック、共有してくれてありがとう!
セイリア2014年

4
このアプローチで自分の後をどのようにクリーンアップしますか?スコープが破棄されたときに登録されたコールバックをpromiseから削除することは可能ですか?
Abris

良い質問。正直わからない。通知コールバックをpromiseから削除する方法について、いくつかのテストを実行してみます。
Krym、2015年

41

ウォッチまたはオブザーバーのコールバックなし(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はそのすべてのプロパティに監視を追加します。

また、$ timeoutの実行時に元のオブジェクトへの参照を保持する必要があるため、「var self = this」を使用していることに注意してください。そうでない場合、「this」はウィンドウオブジェクトを参照します。


3
これは素晴らしいアプローチです!サービス全体ではなく、サービスのプロパティのみをスコープにバインドする方法はありますか?ただするだけで$scope.count = service.countはうまくいきません。
jvannistelrooy 2015年

プロパティを(任意の)オブジェクト内にネストして、参照によって渡されるようにすることもできます。$scope.data = service.data <p>Count: {{ data.count }}</p>
Alex Ross、

1
素晴らしいアプローチ!このページには強力で機能的な答えがたくさんありますが、これははるかにa)実装が最も簡単であり、b)コードを読んで理解するのが最も簡単です。この答えは、現在よりもはるかに高くなるはずです。
CodeMoose、2015年

@CodeMooseに感謝します。AngularJS/ JavaScriptを初めて使用する人のために、今日はさらに単純化しました。
Zymotik、2015年

2
神の祝福がありますように。何百万時間も無駄に費やしました。1.5と苦労していて、angularjsが1から2にシフトしていて、データも共有したかったので
Amna

29

私は同じようなものを探してこの質問に出くわしましたが、何が起こっているのか、いくつかの追加の解決策の完全な説明に値すると思います。

使用したような角度式がHTMLに存在する場合、Angularは自動的に$watchforを設定し、変更がある$scope.fooたびにHTMLを更新します$scope.foo

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

ここで述べられていない問題はaService.foo 、変更が検出されないように、2つのうちの1つが影響していることです。これらの2つの可能性は次のとおりです。

  1. aService.foo 毎回新しい配列に設定され、その配列への参照が古くなっています。
  2. aService.foo更新時に$digestサイクルがトリガーされないように更新されています。

問題1:古くなった参照

最初の可能性を考慮して、$digestが適用されていると仮定すると、aService.foo常に同じ配列であった場合、$watch以下のコードスニペットに示すように、自動的に設定されたものが変更を検出します。

解決策1-a:各更新で配列またはオブジェクトが同じオブジェクトであることを確認します

ご覧のように、おそらくアタッチされaService.fooaService.fooいるng-repeatは変更しても更新されませんが、にアタッチされaService2.foo いるng-repeat は更新されます。これは、私たちへの参照aService.fooが古くなっているためですが、私たちへの参照は古くaService2.fooはありません。を使用して初期配列への参照を作成しました$scope.foo = aService.foo;。これは次の更新時にサービスによって破棄されました。つまり$scope.foo、必要な配列を参照しなくなったということです。

ただし、初期参​​照を確実に維持する方法はいくつかありますが、オブジェクトまたは配列の変更が必要になる場合があります。または、サービスプロパティがStringまたはなどのプリミティブを参照している場合はどうなりますかNumber?これらの場合、参照だけに依存することはできません。では、何ができるでしょうか?

以前に与えられた回答のいくつかは、その問題に対するいくつかの解決策をすでに提供しています。ただし、個人的には、Jinthetallweeksがコメントで提案した単純な方法を使用することに賛成です。

HTMLマークアップでaService.fooを参照するだけです

解決策1-b:サービスをスコープにアタッチ{service}.{property}し、HTMLで参照します。

つまり、次のようにします。

HTML:

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

JS:

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

そうすることで、$watchaService.foo$digestで解決され、正しく更新された値が取得されます。

これはあなたがあなたの回避策でやろうとしていたことの一種ですが、はるかに少ないラウンドでです。それが変更されるたびに$watch明示的にfooオンにするコントローラーに不要を追加しました$scope。あなたは、余分な必要はありません$watchあなたが接続した場合aServiceの代わりをaService.foo$scope、バインドを明示的にaService.fooマークアップインチ


これで、$digestサイクルが適用されていることを前提として、これで問題ありません。上記の例では、Angularの$intervalサービスを使用して配列を更新しました。これにより、$digest更新ごとにループが自動的に開始されます。しかし、(何らかの理由で)サービス変数が「角度の世界」内で更新されない場合はどうでしょうか。言い換えれば、我々はいけない持っている$digestサービスのプロパティを変更するたびにサイクルが自動的に起動されますか?


問題2:行方不明 $digest

ここでの解決策の多くはこの問題を解決しますが、私はCode Whispererに同意します

Angularのようなフレームワークを使用する理由は、独自のオブザーバーパターンを作成しないためです。

したがって、aService.foo上記の2番目の例に示すように、HTMLマークアップで参照を引き続き使用し、コントローラー内に追加のコールバックを登録する必要がないようにします。

解決策2:セッターとゲッターを使用する $rootScope.$apply()

セッターゲッターの使用をまだ誰も提案していないことに驚きました。この機能はECMAScript5で導入されたため、現在数年前から存在しています。もちろん、それは、何らかの理由で本当に古いブラウザーをサポートする必要がある場合、このメソッドは機能しないことを意味しますが、JavaScriptではゲッターとセッターが大幅に活用されていないように感じます。この特定のケースでは、それらは非常に役立ちます。

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

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

ここで、サービス関数に「プライベート」変数を追加しましたrealFoo。これは、オブジェクトのget foo()およびset foo()関数をそれぞれ使用して更新および取得されserviceます。

$rootScope.$apply()set関数での使用に注意してください。これにより、Angularはへの変更を認識しservice.fooます。「inprog」エラーが発生した場合は、この便利なリファレンスページを参照してください。または、Angular> = 1.3を使用して$rootScope.$applyAsync()いる場合は、そのまま使用できます。

またaService.foo、非常に頻繁に更新される場合は、パフォーマンスに大きな影響を与える可能性があるため、注意が必要です。パフォーマンスが問題になる場合は、セッターを使用して、ここで他の回答と同様のオブザーバーパターンを設定できます。


3
これが正しい、最も簡単なソリューションです。@NanoWizardが言うように、$ digest servicesはサービス自体に属するプロパティを監視しません。
Sarpdoruk Tahmaz

28

私の知る限り、あなたはそのような手の込んだことをする必要はありません。あなたはすでにサービスからスコープにfooを割り当てました、そしてfooは配列です(そしてオブジェクトは参照によって割り当てられます!)。だから、あなたがする必要があるのはこのようなものです:

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

 }

この同じCtrlの他のいくつかの変数がfooの変更に依存している場合、そうです、fooを監視してその変数に変更を加えるには、監視が必要になります。しかし、それが単純な参照である限り、ウォッチングは不要です。お役に立てれば。


35
試してみましたが、$watchプリミティブで作業することができませんでした。代わりに、プリミティブ値を返すメソッドをサービスに定義しました。somePrimitive() = function() { return somePrimitive }また、そのメソッドに$ scopeプロパティを割り当てました $scope.somePrimitive = aService.somePrimitive;。:その後、私は、HTML内のスコープの方法を使用 <span>{{somePrimitive()}}</span>
マーク・Rajcok

4
@MarkRajcokいいえ、プリミティブを使用しないでください。それらをオブジェクトに追加します。プリミティブは変更可能ではないため、2wayデータバインディングは機能しません
Jimmy Kane

3
@JimmyKane、はい、プリミティブは双方向のデータバインディングに使用すべきではありませんが、問題は双方向のバインディングを設定するのではなく、サービス変数を監視することでした。サービスのプロパティ/変数のみを監視する必要がある場合は、オブジェクトは不要です。プリミティブを使用できます。
Mark Rajcok、2013

3
このセットアップでは、スコープからaService値を変更できます。ただし、aServiceの変更に応じてスコープは変更されません。
Ouwen Huang

4
これもうまくいきません。単に割り当てるだけで$scope.foo = aService.fooは、スコープ変数は自動的に更新されません。
Darwin Tech、

9

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

1つ、多数、またはすべてのオブジェクト属性の変化を観察できます。

例:

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
私はこの答えが一番投票されていないとは信じられません!!! これは、情報エントロピーを削減し、おそらく追加のメディエーションハンドラーの必要性を軽減するため、最もエレガントなソリューション(IMO)です。できれば、これに賛成票を投じます...
Cody

潜在的な落とし穴を$ rootScope、その恩恵にすべてのサービスを追加し、それはだ、多少ここで詳述されていますstackoverflow.com/questions/14573023/...
Zymotik

6

ファクトリー自体の中で変更を見て、変更をブロードキャストできます

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

このようにして、関連するすべてのファクトリコードをその説明内に配置すると、外部からのブロードキャストのみに依存できます。


6

==更新済み==

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

1
私はPostmanServiceが好きですが、複数の変数をリッスンする必要がある場合、コントローラーの$ watch関数をどのように変更する必要がありますか?
jedi

こんにちは、ジェダイ、頭を上げてくれてありがとう!ペンと答えを更新しました。そのために別の監視機能を追加することをお勧めします。そこで、PostmanServiceに新機能を追加しました。これがお役に立てば幸いです:)
hayatbiralem 16/07/23

実際、そうです:)問題の詳細を共有していただければ、私がお手伝いできます。
hayatbiralem

4

dtheodorの回答に基づいて、以下のようなものを使用して、コールバックの登録を解除することを忘れないようにすることができ$scopeます。

factory('aService', function() {
  var observerCallbacks = [];

  /**
   * Registers a function that will be called when
   * any modifications are made.
   *
   * For convenience the callback is called immediately after registering
   * which can be prevented with `preventImmediate` param.
   *
   * Will also automatically unregister the callback upon scope destory.
   */
  this.registerObserver = function($scope, cb, preventImmediate){
    observerCallbacks.push(cb);

    if (preventImmediate !== true) {
      cb();
    }

    $scope.$on('$destroy', function () {
      observerCallbacks.remove(cb);
    });
  };

  function notifyObservers() {
    observerCallbacks.forEach(function (cb) {
      cb();
    });
  };

  this.foo = someNgResource.query().$then(function(){
    notifyObservers();
  });
});

Array.removeは、次のような拡張メソッドです。

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

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

        return true;
    }
    return false;
};

2

これが私の一般的なアプローチです。

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

2

単純な解決策を探している私のような人にとって、これはコントローラーで通常の$ watchを使用することで期待することとほぼ同じです。唯一の違いは、特定のスコープではなく、JavaScriptコンテキストで文字列を評価することです。$ rootScopeをサービスに挿入する必要がありますが、これはダイジェストサイクルに正しくフックするためにのみ使用されます。

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

2

非常によく似た問題に直面しながら、スコープ内の関数を監視し、その関数にサービス変数を返させました。私はjsフィドルを作成しました。以下のコードを見つけることができます。

    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

私はこの質問に来ましたが、問題は、角度$ intervalプロバイダーを使用しているはずのときにsetIntervalを使用していることでした。これは、setTimeoutの場合にも当てはまります(代わりに$ timeoutを使用してください)。それがOPの質問への答えではないことは知っていますが、それが私を助けたので、それはいくつかを助けるかもしれません。


setTimeout、またはその他の非Angular関数を使用できますが、でコールバックのコードをラップすることを忘れないでください$scope.$apply()
Magnetronnie

2

私は他のスレッドで同様の問題を抱えているが、まったく異なるアプローチで本当に素晴らしい解決策を見つけました。ソース:AngularJS:$ rootScope値が変更されると、ディレクティブ内の$ watchが機能しません

基本的に、そこにあるソリューションは非常に重いソリューションであるため、使用しないように指示し$watchています。代わりに$emitおよびを使用することを提案します$on

私の問題は、サービス内の変数を監視しディレクティブで反応することでした。そして、上記の方法でそれは非常に簡単です!

私のモジュール/サービスの例:

angular.module('xxx').factory('example', function ($rootScope) {
    var user;

    return {
        setUser: function (aUser) {
            user = aUser;
            $rootScope.$emit('user:change');
        },
        getUser: function () {
            return (user) ? user : false;
        },
        ...
    };
});

したがって、基本的に私 は自分の状況を監視します。userそれが新しい値に設定されるたびに$emit、私はuser:changeステータスになります。

今私の場合、使用したディレクティブで

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

ここで、ディレクティブで、$rootScopeと指定さた変更をリッスンします-それぞれに反応します。とても簡単でエレガント!


1

//サービス:(ここでは特別なものはありません)

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

少し醜いですが、トグルのためにスコープ変数の登録をサービスに追加しました:

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

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

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

    return this;
});

そしてコントローラーで:

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

ありがとう。少し質問:なぜthisそのサービスからではなく、なぜそのサービスから戻るのselfですか?
shrekuu 2014年

4
間違いが時々起こるからです。;-)
nclu 2014年

return this;コンストラクターから抜け出すための良い方法;-)
コーディ

1

このプランカーを見てください::これは私が考えることができる最も簡単な例です

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

大規模なアプリケーションでメモリリークを引き起こす恐ろしいオブザーバーパターンをいくつか見ました。

私は少し遅れるかもしれませんが、これと同じくらい簡単です。

配列のプッシュなどを監視したい場合は、watch関数が参照の変更(プリミティブ型)を監視します。

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

これにより、参照が更新され、どこからでも時計が更新されます。サービスのgetterメソッドを含みます。プリミティブであるものはすべて自動的に更新されます。


0

私はその部分に遅れていますが、私はこれを行うには、上記の回答よりも良い方法を見つけました。サービス変数の値を保持するために変数を割り当てる代わりに、サービス変数を返すスコープにアタッチされた関数を作成しました。

コントローラ

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

これはあなたがやりたいことをすると思います。私のコントローラーは、この実装でサービスの値をチェックし続けます。正直なところ、これは選択した回答よりもはるかに簡単です。


なぜ反対票が投じられたのか..私も同様のテクニックを何度も使用してきました
未定義

0

サービスのプロパティの変更を追跡するのに役立つ簡単なユーティリティサービスを2つ作成しました。

長い説明をスキップしたい場合は、jsfiddleにアクセスできます。

  1. WatchObj

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

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

このサービスは、コントローラーで次のように使用されます。「prop1」、「prop2」、「prop3」のプロパティを持つサービス「testService」があるとします。監視してスコープ 'prop1'および 'prop2'に割り当てます。監視サービスを使用すると、次のようになります。

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. apply Watch objはすばらしいですが、サービスに非同期コードがある場合は十分ではありません。その場合は、次のような2つ目のユーティリティを使用します。

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

したがって、それらすべてを一緒にすると、次のようになります(実行するか、フィドルを開くことができます)。

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.