AngularJSのコントローラー間で通信する正しい方法は何ですか?


473

コントローラ間で通信する正しい方法は何ですか?

私は現在、次のような恐ろしいファッジを使用していますwindow

function StockSubgroupCtrl($scope, $http) {
    $scope.subgroups = [];
    $scope.handleSubgroupsLoaded = function(data, status) {
        $scope.subgroups = data;
    }
    $scope.fetch = function(prod_grp) {
        $http.get('/api/stock/groups/' + prod_grp + '/subgroups/').success($scope.handleSubgroupsLoaded);
    }
    window.fetchStockSubgroups = $scope.fetch;
}

function StockGroupCtrl($scope, $http) {
    ...
    $scope.select = function(prod_grp) {
        $scope.selectedGroup = prod_grp;
        window.fetchStockSubgroups(prod_grp);
    }
}

36
まったく意味がありませんが、Angularでは、ネイティブJSウィンドウオブジェクトの代わりに常に$ windowを使用する必要があります。このようにして、テストでそれをスタブ化できます:)
Dan M

1
この問題に関する私からの以下の回答のコメントをご覧ください。$ broadcastは$ emitよりも高価ではありません。そこで参照したjsperfリンクを参照してください。
zumalifeguard

回答:


457

編集:この回答で対処された問題は、angular.js バージョン1.2.7で解決されています。$broadcast未登録のスコープでのバブリングを回避し、$ emitと同じくらい高速に実行されるようになりました。 $ broadcastのパフォーマンスは、角度が1.2.16の$ emitと同じです。

だから、今することができます:

  • $broadcastからの使用$rootScope
  • イベントについて知る必要が$on ある地元の人から$scope聞いてください

以下の元の回答

$rootScope.$broadcast+ $scope.$onではなく$rootScope.$emit+ を使用することを強くお勧めします$rootScope.$on。前者は@numanによって引き起こされる深刻なパフォーマンスの問題を引き起こす可能性があります。これは、イベントがすべてのスコープを通過するためです。

ただし、後者($rootScope.$emit+ を使用$rootScope.$on)ではこの問題が発生しないため、高速通信チャネルとして使用できます。

の角度のドキュメントから$emit

登録されていることを通知するスコープ階層を通じてイベント名を上方にディスパッチします

上記のスコープがないため$rootScope、バブリングは発生していません。EventBusとして$rootScope.$emit()/ を使用しても完全に安全$rootScope.$on()です。

ただし、コントローラ内から使用する場合、1つの問題があります。$rootScope.$on()コントローラ内から直接バインドする場合、ローカル$scopeが破壊されたときに自分でバインドをクリーンアップする必要があります。これは、コントローラーが(サービスとは対照的に)アプリケーションの存続期間中に複数回インスタンス化される可能性があるためです。その結果、バインディングが合計され、最終的に場所全体にメモリリークが発生します。

登録を解除するには、$scope$destroyイベントをリッスンし、から返された関数を呼び出します$rootScope.$on

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

            var unbind = $rootScope.$on('someComponent.someCrazyEvent', function(){
                console.log('foo');
            });

            $scope.$on('$destroy', unbind);
        }
    ]);

他のEventBus実装にも当てはまるので、リソースをクリーンアップする必要があるというのは、実際には角度によるものではありません。

ただし、それらの場合に、あなたの人生を楽にすることができます。たとえば、サルのパッチ$rootScopeを適用して、$onRootScopeで発行されたイベントをサブスクライブし$rootScope、ローカル$scopeが破棄されたときにハンドラを直接クリーンアップすることもできます。

その$rootScopeような$onRootScopeメソッドを提供するためにサルのパッチを適用する最もクリーンな方法は、デコレータを使用することです(runブロックはおそらくそれでもうまくいきますが、誰にも言わないでください)。

確認するために$onRootScope上で列挙するときに、プロパティが予期しない表示されません。$scope私たちが使用Object.defineProperty()して設定enumerableしますfalse。ES5シムが必要になる場合があることに注意してください。

angular
    .module('MyApp')
    .config(['$provide', function($provide){
        $provide.decorator('$rootScope', ['$delegate', function($delegate){

            Object.defineProperty($delegate.constructor.prototype, '$onRootScope', {
                value: function(name, listener){
                    var unsubscribe = $delegate.$on(name, listener);
                    this.$on('$destroy', unsubscribe);

                    return unsubscribe;
                },
                enumerable: false
            });


            return $delegate;
        }]);
    }]);

このメソッドを配置すると、上記のコントローラーコードを次のように簡略化できます。

angular
    .module('MyApp')
    .controller('MyController', ['$scope', function MyController($scope) {

            $scope.$onRootScope('someComponent.someCrazyEvent', function(){
                console.log('foo');
            });
        }
    ]);

したがって、このすべての最終結果として、$rootScope.$emit+ を使用することを強くお勧めします$scope.$onRootScope

ところで、私は角度のあるチームに角度のあるコア内の問題に対処するよう説得しようとしています。ここで議論が行われています:https : //github.com/angular/angular.js/issues/4574

次のjsperfは、$broadcastわずか100 $scopeの適切なシナリオで、perfの影響がどの程度テーブルにもたらすかを示しています。

http://jsperf.com/rootscope-emit-vs-rootscope-broadcast

jsperfの結果


2番目のオプションを実行しようとしていますが、エラーが発生します。UncaughtTypeError:プロパティを再定義できません:Object.definePropertyを実行している場所で$ onRootScopeを正しく実行しています...
Scott

ここに貼り付けたときに、何かがおかしくなったのかもしれません。私はそれをプロダクションで使用し、うまく機能します。私は明日見ていきます:)
クリストフ

@Scott私はそれを貼り付けましたが、コードはすでに正しく、私たちが本番で使用しているものとまったく同じです。サイトにタイプミスがないことを再確認できますか?トラブルシューティングに役立つコードをどこかに表示できますか?
クリストフ

@Christophは非DOMオブジェクトのObject.definePropertyをサポートしていないため、IE8でデコレーターを実行する良い方法はありますか?
joshschreuder 2014年

59
これは問題に対する非常に巧妙な解決策でしたが、もはや必要ありません。Angularの最新バージョン(1.2.16)、そしておそらく以前のバージョンでは、この問題が修正されています。$ broadcastが理由なくすべての子孫コントローラーを訪問することはありません。実際にイベントを聴いている人のみを訪問します。上記のjsperfを更新して、問題が修正されたことを示しました:jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
zumalifeguard

107

ここでのトップの答えは、@ zumalifeguardが言及したように、もはや存在しないAngularの問題(少なくともバージョン1.2.16以降、「おそらく以前のバージョン」)からの回避策でした。しかし、私は実際の解決策なしにこれらすべての答えを読み残しました。

今の答えは

  • $broadcastからの使用$rootScope
  • イベントについて知る必要が$on ある地元の人から$scope聞いてください

だから公開する

// EXAMPLE PUBLISHER
angular.module('test').controller('CtrlPublish', ['$rootScope', '$scope',
function ($rootScope, $scope) {

  $rootScope.$broadcast('topic', 'message');

}]);

そしてサブスクライブ

// EXAMPLE SUBSCRIBER
angular.module('test').controller('ctrlSubscribe', ['$scope',
function ($scope) {

  $scope.$on('topic', function (event, arg) { 
    $scope.receiver = 'got your ' + arg;
  });

}]);

プランカー

あなたがローカルにリスナーを登録した場合$scope、それがされることにより、自動的に破棄$destroy自体に関連するコントローラが削除されたとき。


1
この同じパターンをcontrollerAs構文で使用できるかどうか知っていますか?$rootScopeサブスクライバーで使用してイベントを聞くことができましたが、別のパターンがあるかどうかだけ気になりました。
edhedges 2014

3
@edhedges $scope明示的に注入できると思います。ジョンパパは、イベント$scopeを彼のコントローラーから「除外する」という彼の通常のルールの1つの「例外」であると書いています(彼がController Asまだ言及しているように$scope、それはボンネットのすぐ下にあるため、引用符を使用します)。
2014

ボンネットの下にあるということは、それでも注射でそれを得ることができるということですか?
edhedges 2014

2
@edhedges controller asリクエストに応じて、構文の代替案で回答を更新しました。それがあなたの言った意味だと思います。
最高級の

3
@dsdsdsdsd、サービス/ファクトリー/プロバイダーは永遠に留まります。Angularアプリには常に1つ(シングルトン)しかありません。一方、コントローラーは機能に関連付けられています。コンポーネント/ディレクティブ/ ng-controllerは(クラスから作成されたオブジェクトのように)繰り返すことができ、必要に応じて行き来します。必要がなくなったときに、コントロールとそのコントローラーが存在し続ける必要があるのはなぜですか。これがまさにメモリリークの定義です。
2016年


42

definePropertyにはブラウザーの互換性の問題があるため、サービスの使用について考えることができると思います。

angular.module('myservice', [], function($provide) {
    $provide.factory('msgBus', ['$rootScope', function($rootScope) {
        var msgBus = {};
        msgBus.emitMsg = function(msg) {
        $rootScope.$emit(msg);
        };
        msgBus.onMsg = function(msg, scope, func) {
            var unbind = $rootScope.$on(msg, func);
            scope.$on('$destroy', unbind);
        };
        return msgBus;
    }]);
});

次のようにコントローラで使用します。

  • コントローラー1

    function($scope, msgBus) {
        $scope.sendmsg = function() {
            msgBus.emitMsg('somemsg')
        }
    }
  • コントローラー2

    function($scope, msgBus) {
        msgBus.onMsg('somemsg', $scope, function() {
            // your logic
        });
    }

7
スコープが破棄されたときの自動サブスクリプション解除の+1。
Federico Nafria、2014年

6
私はこのソリューションが好きです。私が行った2つの変更:(1)ユーザーが「データ」を発行メッセージに渡すことを許可します(2)「スコープ」の渡しをオプションにして、シングルトンサービスとコントローラーで使用できるようにします。:あなたは、これらの変更は、ここで実装見ることができますgist.github.com/turtlemonvh/10686980/...
turtlemonvh


15

イベントはスコープ階層を上下にバブルし、複雑なアプリケーションのパフォーマンスのボトルネックになりやすいため、実際には発行とブロードキャストの使用は非効率的です。

サービスを利用することをお勧めします。これが最近私のプロジェクトの1つに実装した方法です-https://gist.github.com/3384419

基本的な考え方-pubsub /イベントバスをサービスとして登録します。次に、イベント/トピックをサブスクライブまたは発行する必要がある場所にそのイベントバスを挿入します。


7
そして、コントローラーが不要になった場合、どのようにして自動でサブスクライブを解除しますか?これを行わないと、閉鎖のためにコントローラがメモリから削除されることはなく、メッセージを感知し続けることになります。これを回避するには、手動で削除する必要があります。$ onの使用は発生しません。
Renan Tomal Fernandes、2012

1
それは公平な点です。アプリケーションをどのように構築するかで解決できると思います。私の場合、私は単一ページのアプリを持っているので、それはより扱いにくい問題です。とは言っても、angularがこのようなものを配線/配線解除できるコンポーネントライフサイクルフックを持っていれば、これははるかにきれいになると思います。
numan salati

6
誰もそれを以前に述べなかったので、私はこれをここに残します。rootScopeをEventBusとして使用することは、上向きにバブルするだけなので効率的ではありません$rootScope.$emit()。しかし、その上にスコープ$rootScopeがないため、恐れることは何もありません。したがって、単に使用$rootScope.$emit()$rootScope.$on()ていて、システム全体に高速なEventBusがある場合。
クリストフ

1
注意する必要がある唯一のことは$rootScope.$on()、コントローラー内で使用する場合は、イベントバインディングをクリーンアップする必要があるということです。$rootScope直接バインドしているため、自動的に破棄されます。
クリストフ

Angularの最新バージョン(1.2.16)、そしておそらく以前のバージョンでは、この問題が修正されています。$ broadcastが理由なくすべての子孫コントローラーを訪問することはありません。実際にイベントを聴いている人のみを訪問します。上記のjsperfを更新して、問題が修正されたことを示しました:jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
zumalifeguard

14

サービス内でgetメソッドとsetメソッドを使用すると、コントローラー間で非常に簡単にメッセージを渡すことができます。

var myApp = angular.module("myApp",[]);

myApp.factory('myFactoryService',function(){


    var data="";

    return{
        setData:function(str){
            data = str;
        },

        getData:function(){
            return data;
        }
    }


})


myApp.controller('FirstController',function($scope,myFactoryService){
    myFactoryService.setData("Im am set in first controller");
});



myApp.controller('SecondController',function($scope,myFactoryService){
    $scope.rslt = myFactoryService.getData();
});

HTML HTMLでは、このように確認できます

<div ng-controller='FirstController'>  
</div>

<div ng-controller='SecondController'>
    {{rslt}}
</div>

+1言われたことの一度だけ明らかな方法の1つ-すばらしい!$ broadcastの便利な代替手段であるset(key、value)およびget(key)メソッドを使用して、より一般的なバージョンを実装しました。
TonyWilk 2016年

8

元のコードについて-スコープ間でデータを共有したいようです。$ scope間でデータまたは状態を共有するには、ドキュメントでサービスの使用を提案します。

  • コントローラー間で共有されるステートレスまたはステートフルコードを実行するには—代わりに角度サービスを使用します。
  • 他のコンポーネントのライフサイクルをインスタンス化または管理する(たとえば、サービスインスタンスを作成する)。

参照:Angular Docsリンクはこちら


5

私は実際にPostal.jsをコントローラー間のメッセージバスとして使い始めました。

AMQPスタイルのバインディングなどのメッセージバス、郵便がiFrameとWebソケットを統合する方法など、多くのメリットがあります。

デコレータを使用してPostalをセットアップしました$scope.$bus...

angular.module('MyApp')  
.config(function ($provide) {
    $provide.decorator('$rootScope', ['$delegate', function ($delegate) {
        Object.defineProperty($delegate.constructor.prototype, '$bus', {
            get: function() {
                var self = this;

                return {
                    subscribe: function() {
                        var sub = postal.subscribe.apply(postal, arguments);

                        self.$on('$destroy',
                        function() {
                            sub.unsubscribe();
                        });
                    },
                    channel: postal.channel,
                    publish: postal.publish
                };
            },
            enumerable: false
        });

        return $delegate;
    }]);
});

これはトピックに関するブログ投稿へのリンクです...
http://jonathancreamer.com/an-angular-event-bus-with-postal-js/


3

これは、Factory / Servicesと単純な依存性注入(DI)を使用して行う方法です。

myApp = angular.module('myApp', [])

# PeopleService holds the "data".
angular.module('myApp').factory 'PeopleService', ()->
  [
    {name: "Jack"}
  ]

# Controller where PeopleService is injected
angular.module('myApp').controller 'PersonFormCtrl', ['$scope','PeopleService', ($scope, PeopleService)->
  $scope.people = PeopleService
  $scope.person = {} 

  $scope.add = (person)->
    # Simply push some data to service
    PeopleService.push angular.copy(person)
]

# ... and again consume it in another controller somewhere...
angular.module('myApp').controller 'PeopleListCtrl', ['$scope','PeopleService', ($scope, PeopleService)->
  $scope.people = PeopleService
]

1
2つのコントローラーは通信せず、1つの同じサービスのみを使用します。それは同じことではありません。
グレッグ

@Greg共有サービスを用意し、必要に応じて$ watchesを追加することで、少ないコードで同じことを実現できます。
Capaj 2015年

3

$rootscope.emit相互通信を実現するためにどのように使用するかが気に入りました。グローバルスペースを汚染することなく、クリーンでパフォーマンスに優れたソリューションを提案します。

module.factory("eventBus",function (){
    var obj = {};
    obj.handlers = {};
    obj.registerEvent = function (eventName,handler){
        if(typeof this.handlers[eventName] == 'undefined'){
        this.handlers[eventName] = [];  
    }       
    this.handlers[eventName].push(handler);
    }
    obj.fireEvent = function (eventName,objData){
       if(this.handlers[eventName]){
           for(var i=0;i<this.handlers[eventName].length;i++){
                this.handlers[eventName][i](objData);
           }

       }
    }
    return obj;
})

//Usage:

//In controller 1 write:
eventBus.registerEvent('fakeEvent',handler)
function handler(data){
      alert(data);
}

//In controller 2 write:
eventBus.fireEvent('fakeEvent','fakeData');

メモリリークの場合は、イベントリスナーから登録を解除するメソッドを追加する必要があります。とにかく良いささいなサンプル
Raffaeu 2016年

2

ここにすばやく汚い方法があります。

// Add $injector as a parameter for your controller

function myAngularController($scope,$injector){

    $scope.sendorders = function(){

       // now you can use $injector to get the 
       // handle of $rootScope and broadcast to all

       $injector.get('$rootScope').$broadcast('sinkallships');

    };

}

以下は、兄弟コントローラー内に追加する関数の例です。

$scope.$on('sinkallships', function() {

    alert('Sink that ship!');                       

});

そしてもちろんここにあなたのHTMLがあります:

<button ngclick="sendorders()">Sink Enemy Ships</button>

16
なぜ注射しないのです$rootScopeか?
Pieter Herroelen 14

1

角度1.5から始まり、コンポーネントベースの開発に重点を置いています。コンポーネントが相互作用するための推奨される方法は、 'require'プロパティの使用とプロパティバインディング(入力/出力)によるものです。

コンポーネントは別のコンポーネント(ルートコンポーネントなど)を必要とし、そのコントローラーへの参照を取得します。

angular.module('app').component('book', {
    bindings: {},
    require: {api: '^app'},
    template: 'Product page of the book: ES6 - The Essentials',
    controller: controller
});

次に、子コンポーネントでルートコンポーネントのメソッドを使用できます。

$ctrl.api.addWatchedBook('ES6 - The Essentials');

これは、ルートコンポーネントコントローラー関数です。

function addWatchedBook(bookName){

  booksWatched.push(bookName);

}

以下は完全なアーキテクチャの概要です:コンポーネント通信


0

このhello関数にはモジュールのどこからでもアクセスできます

コントローラー1

 $scope.save = function() {
    $scope.hello();
  }

2番目のコントローラー

  $rootScope.hello = function() {
    console.log('hello');
  }

詳細はこちら


7
パーティーには少し遅れますが、これを行わないでください。ルートスコープに関数を配置することは、関数をグローバルにすることに似ており、あらゆる種類の問題を引き起こす可能性があります。
Dan Pantry、2015

0

サービスを作成して通知を使用します。

  1. 通知サービスでメソッドを作成する
  2. 通知サービスで通知をブロードキャストする汎用メソッドを作成します。
  3. ソースコントローラーからnotificationService.Methodを呼び出します。必要に応じて、対応するオブジェクトを渡して永続化します。
  4. メソッド内では、通知サービスにデータを永続化し、汎用の通知メソッドを呼び出します。
  5. 宛先コントローラーで、ブロードキャストイベントをリッスン($ scope.on)し、通知サービスのデータにアクセスします。

どの時点でも、通知サービスはシングルトンなので、永続的なデータを提供できます。

お役に立てれば


0

AngularJSビルドインサービス$rootScopeを使用して、このサービスを両方のコントローラーに挿入できます。その後、$ rootScopeオブジェクトで発生するイベントをリッスンできます。

$ rootScopeは、$emit and $broadcastイベント(カスタムイベントの場合もある)のディスパッチ$rootScope.$onと、イベントリスナーを追加するための関数を使用する2つのイベントディスパッチャーを提供します。


0

$rootscopeApplication全体からのアクセスであり、負荷が増加するため、Serviceを使用する必要があります。データが少ない場合は、rootparamsを使用します。


0
function mySrvc() {
  var callback = function() {

  }
  return {
    onSaveClick: function(fn) {
      callback = fn;
    },
    fireSaveClick: function(data) {
      callback(data);
    }
  }
}

function controllerA($scope, mySrvc) {
  mySrvc.onSaveClick(function(data) {
    console.log(data)
  })
}

function controllerB($scope, mySrvc) {
  mySrvc.fireSaveClick(data);
}

0

$ emitと$ broadcastである角度イベントを使用することでそれを行うことができます。私たちの知識によると、これは最良で効率的かつ効果的な方法です。

まず、1つのコントローラーから関数を呼び出します。

var myApp = angular.module('sample', []);
myApp.controller('firstCtrl', function($scope) {
    $scope.sum = function() {
        $scope.$emit('sumTwoNumber', [1, 2]);
    };
});
myApp.controller('secondCtrl', function($scope) {
    $scope.$on('sumTwoNumber', function(e, data) {
        var sum = 0;
        for (var a = 0; a < data.length; a++) {
            sum = sum + data[a];
        }
        console.log('event working', sum);

    });
});

$ scopeの代わりに$ rootScopeを使用することもできます。それに応じてコントローラーを使用してください。

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