Apa cara yang benar untuk berkomunikasi antar pengendali di AngularJS?


473

Apa cara yang benar untuk berkomunikasi antar pengendali?

Saat ini saya menggunakan fudge mengerikan yang melibatkan 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
Benar-benar diperdebatkan, tetapi dalam Angular, Anda harus selalu menggunakan $ window alih-alih objek JS window asli. Dengan cara ini Anda dapat mematikannya dalam tes Anda :)
Dan M

1
Silakan lihat komentar dalam jawaban di bawah ini dari saya sehubungan dengan masalah ini. $ broadcast tidak lagi lebih mahal dari $ emit. Lihat tautan jsperf yang saya referensikan di sana.
zumalifeguard

Jawaban:


457

Sunting : Masalah yang dibahas dalam jawaban ini telah diatasi dalam angular.js versi 1.2.7 . $broadcastsekarang hindari menggelegak atas cakupan yang tidak terdaftar dan berjalan secepat $ emit. Pertunjukan $ broadcast identik dengan $ emit dengan sudut 1.2.16

Jadi, sekarang Anda dapat:

  • gunakan $broadcastdari$rootScope
  • dengarkan menggunakan $on dari lokal$scope yang perlu tahu tentang acara tersebut

Jawaban Asli Di Bawah Ini

Saya sangat menyarankan untuk tidak menggunakan $rootScope.$broadcast+ $scope.$onmelainkan $rootScope.$emit+ $rootScope.$on. Yang pertama dapat menyebabkan masalah kinerja serius seperti yang diangkat oleh @numan. Itu karena acara tersebut akan menyebar melalui semua ruang lingkup.

Namun, yang terakhir (menggunakan $rootScope.$emit+ $rootScope.$on) tidak menderita ini dan karena itu dapat digunakan sebagai saluran komunikasi yang cepat!

Dari dokumentasi sudut $emit:

Mengirim nama acara ke atas melalui hierarki lingkup yang memberitahukan yang terdaftar

Karena tidak ada ruang lingkup di atas $rootScope, tidak ada gelembung yang terjadi. Sangat aman untuk menggunakan $rootScope.$emit()/ $rootScope.$on()sebagai EventBus.

Namun, ada satu gotcha saat menggunakannya dari dalam Controllers. Jika Anda langsung mengikat $rootScope.$on()dari dalam controller, Anda harus membersihkan sendiri mengikat ketika lokal Anda $scopehancur. Ini karena pengontrol (berbeda dengan layanan) dapat dipakai berkali-kali selama masa aplikasi yang akan menghasilkan binding yang akhirnya membuat kebocoran memori di semua tempat :)

Untuk unregister, hanya mendengarkan pada Anda $scope's $destroyacara dan kemudian memanggil fungsi yang dikembalikan oleh $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);
        }
    ]);

Saya akan mengatakan, itu bukan hal yang sangat spesifik karena berlaku untuk implementasi EventBus lain juga, bahwa Anda harus membersihkan sumber daya.

Namun, Anda dapat membuat hidup Anda lebih mudah untuk kasus-kasus itu. Misalnya, Anda dapat menambal monyet $rootScopedan memberikannya $onRootScopeyang berlangganan acara yang dipancarkan di $rootScopetetapi juga secara langsung membersihkan pawang ketika lokal $scopedihancurkan.

Cara paling bersih untuk menambal monyet $rootScopeuntuk memberikan $onRootScopemetode seperti itu akan melalui dekorator (blok run mungkin akan melakukannya dengan baik juga tapi pssst, jangan bilang siapa-siapa)

Untuk memastikan $onRootScopeproperti tidak muncul secara tak terduga saat menghitung lebih dari, $scopekami menggunakan Object.defineProperty()dan mengatur enumerableuntuk false. Perlu diingat bahwa Anda mungkin memerlukan shim 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;
        }]);
    }]);

Dengan metode ini, kode pengontrol dari atas dapat disederhanakan menjadi:

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

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

Jadi sebagai hasil akhir dari semua ini saya sangat menyarankan Anda untuk menggunakan $rootScope.$emit+ $scope.$onRootScope.

Btw, saya mencoba meyakinkan tim sudut untuk mengatasi masalah dalam inti sudut. Ada diskusi yang terjadi di sini: https://github.com/angular/angular.js/issues/4574

Berikut adalah jsperf yang menunjukkan berapa banyak dampak perf yang $broadcastdibawa ke meja dalam skenario yang layak hanya dengan 100 $scope's.

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

hasil jsperf


Saya mencoba melakukan opsi ke-2 Anda, tetapi saya mendapatkan kesalahan: UnEught TypeError: Tidak dapat mendefinisikan ulang properti: $ onRootScope tepat di mana saya melakukan Object.defineProperty ....
Scott

Mungkin saya mengacaukan sesuatu ketika saya menempelkannya di sini. Saya menggunakannya dalam produksi dan itu bekerja dengan baik. Saya akan lihat besok :)
Christoph

@Scott Saya menempelkannya tetapi kode sudah benar dan persis apa yang kami gunakan dalam produksi. Bisakah Anda mengecek, bahwa Anda tidak memiliki kesalahan ketik di situs Anda? Bisakah saya melihat kode Anda di suatu tempat untuk membantu pemecahan masalah?
Christoph

@Christoph apakah ada cara yang baik untuk melakukan dekorator di IE8, karena tidak mendukung Object.defineProperty pada objek non-DOM?
joshschreuder

59
Ini adalah solusi yang sangat cerdas untuk masalah ini, tetapi tidak lagi diperlukan. Versi terbaru dari Angular (1.2.16), dan mungkin sebelumnya, memperbaiki masalah ini. Sekarang $ broadcast tidak akan mengunjungi setiap pengendali keturunan tanpa alasan. Itu hanya akan mengunjungi mereka yang benar-benar mendengarkan acara tersebut. Saya memperbarui jsperf yang dirujuk di atas untuk menunjukkan bahwa masalahnya sekarang telah diperbaiki: jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
zumalifeguard

107

The atas jawabannya di sini adalah bekerja di sekitar dari masalah sudut yang tidak ada lagi (setidaknya dalam versi> 1.2.16 dan "mungkin sebelumnya") sebagai @zumalifeguard telah disebutkan. Tetapi saya pergi membaca semua jawaban ini tanpa solusi yang sebenarnya.

Bagi saya sepertinya jawabannya sekarang

  • gunakan $broadcastdari$rootScope
  • dengarkan menggunakan $on dari lokal$scope yang perlu tahu tentang acara tersebut

Jadi untuk mempublikasikan

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

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

}]);

Dan berlangganan

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

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

}]);

Plunker

Jika Anda mendaftarkan pendengar di lokal $scope, itu akan dihancurkan secara otomatis dengan $destroysendirinya ketika pengontrol terkait dihapus.


1
Apakah Anda tahu jika pola yang sama ini dapat digunakan dengan controllerAssintaks? Saya dapat menggunakan $rootScopepelanggan untuk mendengarkan acara tersebut, tetapi saya hanya ingin tahu apakah ada pola yang berbeda.
edhedges

3
@edhedges Saya kira Anda bisa menyuntikkan $scopesecara eksplisit. John Papa menulis tentang peristiwa yang menjadi "pengecualian" dari aturannya yang biasa untuk mencegah $scope"pengontrol" nya (saya menggunakan tanda kutip karena ketika ia menyebutkan Controller Asmasih memiliki $scope, itu hanya di bawah kap).
poshest

Di bawah kap, maksud Anda bahwa Anda masih bisa mendapatkannya melalui suntikan?
edhedges

2
@edhedges Saya memperbarui jawaban saya dengan controller asalternatif sintaksis seperti yang diminta. Saya harap itulah yang Anda maksudkan.
poshest

3
@dsdsdsdsd, layanan / pabrik / penyedia akan tetap ada selamanya. Selalu ada satu dan hanya satu dari mereka (lajang) dalam aplikasi Angular. Pengontrol di sisi lain, terikat dengan fungsi: komponen / arahan / ng-controller, yang dapat diulang (seperti objek yang dibuat dari kelas) dan mereka datang dan pergi sesuai kebutuhan. Mengapa Anda ingin kontrol dan pengontrolnya tetap ada saat Anda tidak membutuhkannya lagi? Itulah definisi kebocoran memori.
poshest


42

Karena defineProperty memiliki masalah kompatibilitas browser, saya pikir kita dapat berpikir tentang menggunakan layanan.

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

dan gunakan di controller seperti ini:

  • pengontrol 1

    function($scope, msgBus) {
        $scope.sendmsg = function() {
            msgBus.emitMsg('somemsg')
        }
    }
  • pengontrol 2

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

7
+1 untuk berhenti berlangganan otomatis ketika ruang lingkup hancur.
Federico Nafria

6
Saya suka solusi ini. 2 perubahan yang saya buat: (1) memungkinkan pengguna untuk mengirimkan 'data' ke pesan emit (2) menjadikan kelulusan 'ruang lingkup' opsional sehingga ini dapat digunakan dalam layanan tunggal serta pengontrol. Anda dapat melihat perubahan itu diterapkan di sini: gist.github.com/turtlemonvh/10686980/…
turtlemonvh


15

Sebenarnya menggunakan memancarkan dan menyiarkan tidak efisien karena peristiwa menggelembung naik dan turunnya hierarki ruang lingkup yang dapat dengan mudah menurunkan ke kinerja bottlement untuk aplikasi yang kompleks.

Saya akan menyarankan untuk menggunakan layanan. Inilah cara saya baru-baru ini mengimplementasikannya di salah satu proyek saya - https://gist.github.com/3384419 .

Ide dasar - daftarkan bus pubsub / event sebagai layanan. Kemudian menyuntikkan eventbus itu di mana pun Anda perlu berlangganan atau menerbitkan acara / topik


7
Dan ketika pengontrol tidak diperlukan lagi, bagaimana Anda otomatis berhenti berlangganan? Jika Anda tidak melakukan ini, karena penutupan pengontrol tidak akan pernah dihapus dari memori dan Anda masih akan merasakan pesan untuk itu. Untuk menghindari ini, Anda harus menghapusnya secara manual. Menggunakan $ pada ini tidak akan terjadi.
Renan Tomal Fernandes

1
itu poin yang adil. Saya pikir itu bisa diselesaikan dengan bagaimana Anda merancang aplikasi Anda. dalam kasus saya, saya punya satu aplikasi halaman jadi ini masalah yang lebih mudah ditangani. karena itu, saya pikir ini akan jauh lebih bersih jika sudut memiliki komponen kait siklus di mana Anda dapat kawat / tidak diinginkan hal-hal seperti ini.
numan salati

6
Saya hanya meninggalkan ini di sini karena tidak ada yang menyatakannya sebelumnya. Menggunakan rootScope sebagai EventBus tidak tidak efisien karena $rootScope.$emit()hanya gelembung ke atas. Namun, karena tidak ada ruang lingkup di atas, $rootScopetidak ada yang perlu ditakutkan. Jadi jika Anda hanya menggunakan $rootScope.$emit()dan $rootScope.$on()Anda akan memiliki sistem EventBus lebar cepat.
Christoph

1
Satu-satunya hal yang perlu Anda perhatikan adalah bahwa jika Anda menggunakan $rootScope.$on()di dalam controller Anda, Anda harus membersihkan acara yang mengikat karena jika tidak mereka akan meringkas karena itu membuat yang baru setiap kali controller instantiated dan mereka tidak dihancurkan secara otomatis untuk Anda karena Anda mengikat $rootScopesecara langsung.
Christoph

Versi terbaru dari Angular (1.2.16), dan mungkin sebelumnya, memperbaiki masalah ini. Sekarang $ broadcast tidak akan mengunjungi setiap pengendali keturunan tanpa alasan. Itu hanya akan mengunjungi mereka yang benar-benar mendengarkan acara tersebut. Saya memperbarui jsperf yang dirujuk di atas untuk menunjukkan bahwa masalahnya sekarang telah diperbaiki: jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
zumalifeguard

14

Menggunakan metode get and set dalam layanan, Anda dapat mengirimkan pesan antar pengontrol dengan sangat mudah.

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

dalam HTML HTML Anda dapat memeriksa seperti ini

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

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

+1 Salah satu metode yang jelas-sekali-Anda-diberitahu-itu - luar biasa! Saya telah mengimplementasikan versi yang lebih umum dengan metode set (key, value) dan get (key) - alternatif yang berguna untuk $ broadcast.
TonyWilk

8

Mengenai kode asli - tampaknya Anda ingin berbagi data antar cakupan. Untuk membagikan Data atau Status antara $ scope, saran dokter menggunakan layanan:

  • Untuk menjalankan kode stateless atau stateful yang digunakan bersama di seluruh pengontrol - Gunakan layanan sudut.
  • Untuk membuat instantiate atau mengelola siklus hidup komponen lain (misalnya, untuk membuat instance layanan).

Ref: Angular Documents tautan di sini


5

Saya sebenarnya sudah mulai menggunakan Postal.js sebagai bus pesan antara pengontrol.

Ada banyak manfaat sebagai bus pesan seperti binding gaya AMQP, cara pos dapat mengintegrasikan w / iFrames dan soket web, dan banyak hal lainnya.

Saya menggunakan dekorator untuk menyiapkan pos di $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;
    }]);
});

Berikut ini tautan ke posting blog tentang topik ini ...
http://jonathancreamer.com/an-angular-event-bus-with-postal-js/


3

Ini adalah bagaimana saya melakukannya dengan Pabrik / Layanan dan injeksi ketergantungan sederhana (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
Dua pengendali Anda tidak berkomunikasi, mereka hanya menggunakan satu layanan yang sama. Itu bukan hal yang sama.
Greg

@Reg, Anda dapat mencapai hal yang sama dengan lebih sedikit kode dengan memiliki layanan bersama dan menambahkan $ jam bila diperlukan.
Capaj

3

Saya menyukai cara bagaimana $rootscope.emitdigunakan untuk mencapai komunikasi antar. Saya menyarankan solusi yang efektif dan bersih tanpa mencemari ruang global.

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

Untuk kebocoran memori Anda harus menambahkan metode tambahan untuk membatalkan pendaftaran dari pendengar acara. Pokoknya contoh sepele yang baik
Raffaeu

2

Inilah cara cepat dan kotor.

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

    };

}

Berikut adalah contoh fungsi untuk ditambahkan di dalam salah satu pengontrol saudara:

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

    alert('Sink that ship!');                       

});

dan tentu saja inilah HTML Anda:

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

16
Mengapa Anda tidak menyuntikkan saja $rootScope?
Pieter Herroelen

1

Mulai sudut 1,5 dan fokus pengembangan berbasis komponen itu. Cara yang disarankan untuk komponen untuk berinteraksi adalah melalui penggunaan properti 'wajib' dan melalui binding properti (input / output).

Komponen akan memerlukan komponen lain (misalnya komponen root) dan mendapatkan referensi ke pengontrolnya:

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

Anda kemudian dapat menggunakan metode komponen root di komponen anak Anda:

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

Ini adalah fungsi pengontrol komponen root:

function addWatchedBook(bookName){

  booksWatched.push(bookName);

}

Berikut ini adalah gambaran arsitektur lengkap: Komunikasi Komponen


0

Anda dapat mengakses fungsi halo ini di mana saja di modul

Kontroler satu

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

pengontrol kedua

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

Info lebih lanjut di sini


7
Agak terlambat ke pesta tapi: jangan lakukan ini. Menempatkan fungsi pada ruang lingkup root sama dengan membuat fungsi global, yang dapat menyebabkan segala macam masalah.
Dan Pantry

0

Saya akan membuat layanan dan menggunakan notifikasi.

  1. Buat metode di Layanan Pemberitahuan
  2. Buat metode umum untuk menyiarkan pemberitahuan di Layanan Notifikasi.
  3. Dari pengontrol sumber, panggil notificationService.Method. Saya juga melewatkan objek yang sesuai untuk bertahan jika diperlukan.
  4. Dalam metode ini, saya menyimpan data dalam layanan notifikasi dan memanggil metode notifikasi generik.
  5. Di pengontrol tujuan saya mendengarkan ($ scope.on) untuk acara siaran dan mengakses data dari Layanan Notifikasi.

Karena pada titik mana pun, Layanan Notifikasi adalah singleton, maka ia harus dapat menyediakan data yang tetap ada.

Semoga ini membantu


0

Anda dapat menggunakan layanan bawaan AngularJS $rootScope dan menyuntikkan layanan ini di kedua pengontrol Anda. Anda kemudian dapat mendengarkan acara yang dipecat pada objek $ rootScope.

$ rootScope menyediakan dua pengirim acara yang disebut $emit and $broadcastyang bertanggung jawab untuk mengirim acara (mungkin acara khusus) dan menggunakan $rootScope.$onfungsi untuk menambah pendengar acara.


0

Anda harus menggunakan Layanan, karena $rootscopeakses dari seluruh Aplikasi, dan itu menambah beban, atau Anda menggunakan rootparams jika data Anda tidak lebih.


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

Anda dapat melakukannya dengan menggunakan peristiwa sudut yang $ emit dan $ broadcast. Sesuai pengetahuan kami, ini adalah cara terbaik, efisien dan efektif.

Pertama kita memanggil fungsi dari satu pengontrol.

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

    });
});

Anda juga dapat menggunakan $ rootScope sebagai pengganti $ scope. Gunakan pengontrol Anda sesuai.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.