AngularJS'deki kontrolörler arasında iletişim kurmanın doğru yolu nedir?


473

Kontrolörler arasında iletişim kurmanın doğru yolu nedir?

Şu anda aşağıdakileri içeren korkunç bir şekerleme kullanıyorum 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
Tamamen tartışmalı, ancak Açısal'da her zaman yerel JS penceresi nesnesi yerine $ penceresi kullanmalısınız. Bu şekilde testlerinizde saplayabilirsiniz :)
Dan M

1
Lütfen bu konu ile ilgili olarak aşağıdaki yanıttaki açıklamaya bakınız. $ yayın artık $ emit daha pahalı değil. Orada referans verdiğim jsperf bağlantısına bakın.
zumalifeguard

Yanıtlar:


457

Düzenleme : Bu yanıtta ele alınan sorun angular.js sürüm 1.2.7'de giderilmiştir . $broadcastşimdi kayıt dışı kapsamlar üzerinde kabarcık oluşmasını önler ve $ emit kadar hızlı çalışır. $ yayın performansları açısal 1.2.16 ile $ emit ile aynıdır.

Şimdi şunları yapabilirsiniz:

  • kullanmak $broadcastgelen$rootScope
  • etkinlik hakkında bilmeniz gereken $on yerelden$scope kullanarak dinleyin

Aşağıda Orijinal Yanıt

Çok kullanmamayı tavsiye $rootScope.$broadcast+ $scope.$onziyade ancak $rootScope.$emit+ $rootScope.$on. Birincisi @ insan tarafından ortaya konulduğu gibi ciddi performans sorunlarına neden olabilir. Bunun nedeni, olayın tüm kapsamlar boyunca patlayacağıdır.

Ancak, ikincisi ( $rootScope.$emit+ kullanarak $rootScope.$on) bundan muzdarip değildir ve bu nedenle hızlı bir iletişim kanalı olarak kullanılabilir!

Açısal dokümanlarından $emit:

Kayıtlıyı bildiren kapsam hiyerarşisinde yukarı doğru bir olay adı gönderir

Yukarıda bir kapsam $rootScopeolmadığından, kabarcıklanma olmaz. EventBus olarak $rootScope.$emit()/ kullanmak tamamen güvenlidir $rootScope.$on().

Ancak, Kontrolörler içinden kullanırken bir tane var. $rootScope.$on()Bir denetleyiciden doğrudan bağlanırsanız , $scopeyereliniz yok edildiğinde bağlamayı kendiniz temizlemeniz gerekir . Bunun nedeni, denetleyicilerin (hizmetlerin aksine), uygulamanın ömrü boyunca birden çok kez örneklenebilmesidir;

Unregister için, sadece dinlemek $scopebireyin $destroyolay ve o sırada döndürüldü işlevini çağırmak $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);
        }
    ]);

Diğer EventBus uygulamaları için de geçerli olduğu için gerçekten açısal bir şey değil, kaynakları temizlemeniz gerektiğini söyleyebilirim.

Ancak, olabilir bu durumlar için hayatınızı kolaylaştıracak. Örneğin, maymun yaması $rootScopeverebilir ve ona $onRootScopeyayılan olaylara abone olan, $rootScopeancak yerel $scopeimha edildiğinde doğrudan işleyiciyi temizleyen bir öğe verebilirsiniz .

$rootScopeBöyle bir $onRootScopeyöntemi sağlamak için maymun yamasının en temiz yolu bir dekoratör aracılığıyla olacaktır (bir çalışma bloğu muhtemelen iyi de yapar, ancak pssst, kimseye söyleme)

Emin olmak için $onRootScopeüzerinde numaralandırılıyor mülkiyet beklenmedik görünmüyor $scopekullandığımız Object.defineProperty()ve set enumerableiçin false. Bir ES5 şimine ihtiyacınız olabileceğini unutmayın.

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

Bu yöntem uygulandığında, yukarıdan kontrolör kodu basitleştirilebilir:

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

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

Tüm bunların nihai sonucu olarak $rootScope.$emit+ kullanmanızı şiddetle tavsiye ederim $scope.$onRootScope.

Btw, açısal ekibi problemi açısal çekirdek içinde ele almaya ikna etmeye çalışıyorum. Burada bir tartışma var: https://github.com/angular/angular.js/issues/4574

İşte sadece 100'lerle $broadcastiyi bir senaryoda mükemmel bir etkinin ne kadarını masaya getirdiğini gösteren bir jsperf $scope.

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

jsperf sonuçları


Ben senin ikinci seçeneği yapmaya çalışıyorum, ama bir hata alıyorum: Yakalanmayan TypeError: özelliği yeniden tanımlanamıyor: $ onRootScope doğru Object.defineProperty yaptığım ....
Scott

Belki buraya yapıştırdığımda bir şeyler berbat ettim. Üretimde kullanıyorum ve harika çalışıyor. Yarın bir göz atacağım :)
Christoph

@Scott yapıştırdım ama kod zaten doğruydu ve tam olarak üretimde kullandığımız şey. Sitenizde yazım hatası olup olmadığını iki kez kontrol edebilir misiniz? Sorun gidermeye yardımcı olması için kodunuzu bir yerde görebilir miyim?
Christoph

@Christoph, DOM olmayan nesnelerde Object.defineProperty'yi desteklemediği için IE8'de dekoratörü yapmanın iyi bir yolu var mı?
joshschreuder

59
Bu, probleme çok zekice bir çözümdü, ama artık gerekli değil. Angular'ın (1.2.16) en son sürümü ve muhtemelen daha önce bu sorunu çözdü. Şimdi $ broadcast sebepsiz her torunu denetlemeyecektir. Sadece gerçekten etkinliği dinleyenleri ziyaret edecek. Sorunun düzeltildiğini göstermek için yukarıda başvurulan jsperf'i güncelledim: jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
zumalifeguard

107

Buradaki en iyi yanıt , @zumalifeguard'ın belirttiği gibi artık var olmayan (en azından> 1.2.16 ve "muhtemelen daha önce" sürümlerinde) bir Açısal problemden kaynaklanıyordu. Ama tüm bu cevapları gerçek bir çözüm olmadan okudum.

Bana öyle geliyor ki cevap şimdi olmalı

  • kullanmak $broadcastgelen$rootScope
  • etkinlik hakkında bilmeniz gereken $on yerelden$scope kullanarak dinleyin

Yayınlamak için

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

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

}]);

Ve abone olun

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

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

}]);

Plunkers

Yerel üzerinde dinleyici kayıt varsa $scope, bu edilecektir tarafından otomatik tahrip $destroykendisi ilişkili kontrolör kaldırıldığında.


1
Aynı modelin controllerAssözdizimi ile kullanılabileceğini biliyor musunuz ? $rootScopeAbonede etkinliği dinlemek için kullanabildim , ancak sadece farklı bir model olup olmadığını merak ettim.
edhedges

3
@edhedges $scopeAçıkça enjekte edebilirsiniz sanırım . John Papa yazıyor olaylar tutulması her zamanki kuralın bir "istisna" olma konusunda $scopeyaptığı denetleyicilerin "out" (diye bahseder gibi çünkü tırnak kullanabilirsiniz Controller Ashala vardır $scope, sadece kaputun altında).
poshest

Kaputun altından, enjeksiyonla hala bulabileceğiniz anlamına mı geliyorsunuz?
edhedges

2
@edhedges Yanıtımı controller asistendiği gibi bir sözdizimi alternatifiyle güncelledim . Umarım demek istediğin buydu.
poshest

3
@dsdsdsdsd, hizmetler / fabrikalar / sağlayıcılar sonsuza kadar kalacak. Açısal bir uygulamada her zaman bunlardan sadece bir tanesi (tekton) vardır. Diğer taraftan kontrolörler işlevselliğe bağlıdır: tekrarlanabilir (bir sınıftan yapılmış nesneler gibi) bileşenler / direktifler / ng-kontrolörü ve gerektiğinde gelir ve giderler. Neden artık ihtiyacınız olmadığında bir kontrolün ve kontrolörünün varlığını sürdürmesini istiyorsunuz? Bu bir bellek sızıntısının tanımıdır.
poshest


42

DefineProperty'nin tarayıcı uyumluluk sorunu olduğundan, bir hizmet kullanmayı düşünebiliriz.

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

ve denetleyicide şöyle kullanın:

  • denetleyici 1

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

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

7
Kapsam yok edildiğinde otomatik abonelik iptali için +1.
Federico Nafria

6
Bu çözümü seviyorum. Yaptığım 2 değişiklik: (1) kullanıcının yayma mesajına 'veri' iletmesine izin vermek (2) 'kapsamın' geçişini isteğe bağlı yapmak, böylece tekil servislerde ve kontrolörlerde kullanılabilir. Bu değişiklikleri burada görebilirsiniz: gist.github.com/turtlemonvh/10686980/…
turtlemonvh


15

Gerçekte emit ve broadcast kullanmak verimsizdir çünkü olay, karmaşık bir uygulama için kolayca performans düşüşüne dönüşebilen kapsam hiyerarşisini yukarı ve aşağı kabarcıklar.

Bir hizmet kullanmanızı öneririm. Kısa bir süre önce projelerimden birine nasıl uyguladığım - https://gist.github.com/3384419 .

Temel fikir - pubsub / event bus'ı hizmet olarak kaydedin. Ardından, olay / konulara abone olmanız veya yayınlamanız gereken yere o olayı enjekte edin.


7
Ve artık bir denetleyiciye ihtiyaç duyulmadığında, aboneliği otomatik olarak nasıl iptal edersiniz? Bunu yapmazsanız, kapatma nedeniyle denetleyici asla bellekten kaldırılmaz ve yine de ona mesajlar algılarsınız. Bundan kaçınmak için sonra manuel olarak kaldırmanız gerekir. Bu işlem için $ kullanılması gerçekleşmez.
Renan Tomal Fernandes

1
bu adil bir nokta. bence uygulamanızı nasıl tasarladığınıza göre çözülebilir. benim durumumda, ben tek bir sayfa uygulaması var, bu onun daha yönetilebilir bir sorun. Bunu söyledikten sonra, eğer köşeli böyle şeyler tel / unwire olabilir bileşen yaşam döngüsü kanca olsaydı bu çok daha temiz olacağını düşünüyorum.
numan salati

6
Bunu daha önce kimse söylemediği için burada bırakıyorum. RootScope'u EventBus olarak kullanmak, yalnızca yukarıya doğru kabardığı için verimsiz değildir$rootScope.$emit() . Ancak, yukarıda bir kapsam $rootScopeolmadığından korkacak hiçbir şey yoktur. Yani sadece kullanıyorsanız $rootScope.$emit()ve $rootScope.$on()hızlı bir sistem çapında EventBus olacak.
Christoph

1
Dikkat etmeniz gereken tek şey $rootScope.$on(), kontrol cihazınızın içinde kullanıyorsanız , olay bağlamasını temizlemeniz gerekeceğidir, aksi takdirde kontrol cihazı her başlatıldığında yeni bir tane oluşturduğundan toplanırlar. $rootScopedoğrudan bağlandığınız için sizin için otomatik olarak imha edilir .
Christoph

Angular'ın (1.2.16) en son sürümü ve muhtemelen daha önce bu sorunu çözdü. Şimdi $ broadcast sebepsiz her torunu denetlemeyecektir. Sadece gerçekten etkinliği dinleyenleri ziyaret edecek. Sorunun düzeltildiğini göstermek için yukarıda başvurulan jsperf'i güncelledim: jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
zumalifeguard

14

Bir hizmet içindeki alma ve ayarlama yöntemlerini kullanarak denetleyiciler arasında iletileri kolayca iletebilirsiniz.

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'de böyle kontrol edebilirsiniz

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

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

+1 Bir kez söylenen bariz yöntemlerden biri - mükemmel! $ Broadcast için kullanışlı bir alternatif olan set (key, value) ve get (key) yöntemleri ile daha genel bir versiyon uyguladım.
TonyWilk

8

Orijinal kodla ilgili olarak - kapsamlar arasında veri paylaşmak istediğiniz anlaşılıyor. Verileri veya Durumu $ kapsamı arasında paylaşmak için dokümanlar bir hizmet kullanmanızı önerir:

  • Denetleyiciler arasında paylaşılan durum bilgisi olmayan veya durum bilgisi olan kod çalıştırmak için - Bunun yerine açısal servisleri kullanın.
  • Diğer bileşenlerin yaşam döngüsünü başlatmak veya yönetmek için (örneğin, hizmet örnekleri oluşturmak için).

Ref: Açısal Dokümanlar bağlantısı


5

Aslında Postal.js'yi denetleyiciler arasında bir mesaj veri yolu olarak kullanmaya başladım.

AMQP tarzı bağlamalar, postaların w / iFrame'leri ve web soketlerini entegre edebilme şekli ve daha pek çok şey gibi birçok faydası vardır.

Posta ayarlamak için bir dekoratör kullandım $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;
    }]);
});

İşte konuyla ilgili bir blog yazısı bağlantısı ...
http://jonathancreamer.com/an-angular-event-bus-with-postal-js/


3

Fabrika / Hizmetler ve basit bağımlılık enjeksiyonu (DI) ile böyle yapıyorum .

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
İki denetleyiciniz iletişim kurmuyor, yalnızca bir aynı hizmeti kullanıyorlar. Bu aynı şey değil.
Greg

@Greg, paylaşılan bir hizmete sahip olun ve gerektiğinde $ saatler ekleyerek aynı şeyi daha az kodla elde edebilirsiniz.
Çapaj

3

Ben iletişim $rootscope.emitkurmak için nasıl kullanıldığını sevdim . Küresel alanı kirletmeden temiz ve performans etkin bir çözüm öneriyorum.

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

Bellek sızıntısı için, olay dinleyicilerinin kaydını silmek için fazladan bir yöntem eklemeniz gerekir. Neyse iyi önemsiz örnek
Raffaeu

2

İşte hızlı ve kirli yol.

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

    };

}

Kardeş denetleyicilerin herhangi birine eklenecek örnek bir işlev şunlardır:

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

    alert('Sink that ship!');                       

});

ve elbette HTML kodunuz:

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

16
Neden sadece enjekte etmiyorsun $rootScope?
Pieter Herroelen

1

Açısal 1.5 ve bileşen tabanlı geliştirme odağı. Bileşenlerin etkileşime girmesi için önerilen yol 'requir' özelliğinin kullanılması ve özellik bağlamaları (giriş / çıkış) kullanmaktır.

Bir bileşen başka bir bileşen (örneğin kök bileşen) gerektirir ve denetleyicisine bir başvuru alır:

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

Ardından, alt bileşeninizdeki kök bileşenin yöntemlerini kullanabilirsiniz:

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

Bu, kök bileşen denetleyici işlevidir:

function addWatchedBook(bookName){

  booksWatched.push(bookName);

}

İşte tam bir mimari genel bakış: Bileşen İletişimi


0

Bu merhaba işlevine modülün herhangi bir yerinden erişebilirsiniz

Birinci denetleyici

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

ikinci kontrolör

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

Daha fazla bilgi burada


7
Partiye biraz geç ama: bunu yapma. Kök kapsamına bir işlev koymak, her türlü soruna neden olabilecek bir işlevi global hale getirmeye benzer.
Dan Pantry

0

Bir hizmet oluşturacağım ve bildirim kullanacağım.

  1. Bildirim Hizmetinde yöntem oluşturma
  2. Bildirim Hizmeti'nde bildirim yayınlamak için genel bir yöntem oluşturun.
  3. Kaynak denetleyicisinden messageService.Method öğesini arayın. Ayrıca gerekirse devam etmek için ilgili nesneyi iletmek.
  4. Bu yöntemde, bildirim hizmetindeki verileri saklıyorum ve genel bildirim yöntemini çağırıyorum.
  5. Hedef denetleyicide yayın olayını dinlerim ($ scope.on) ve Bildirim Hizmetinden verilere erişirim.

Herhangi bir noktada Bildirim Hizmeti tekil olduğu için, sürekli veri sağlayabilmelidir.

Bu yardımcı olur umarım


0

AngularJS yerleşik hizmetini kullanabilirsiniz $rootScope ve bu hizmeti her iki denetleyicinize de enjekte edebilirsiniz. Daha sonra $ rootScope nesnesinde tetiklenen olayları dinleyebilirsiniz.

$ rootScope, olayların gönderilmesinden $emit and $broadcastsorumlu olan (özel olaylar olabilir) ve $rootScope.$onolay dinleyicisi eklemek için işlevi kullanan iki olay dağıtıcı sağlar .


0

$rootscopeTüm Uygulamadan erişim olduğu için Hizmeti kullanmalısınız ve yükü artırır veya verileriniz daha fazla değilse rootparamları kullanırsınız.


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 ve $ broadcast gibi açısal olayları kullanarak yapabilirsiniz. Bildiğimiz kadarıyla bu en iyi, verimli ve etkili yoldur.

İlk önce bir kontrol cihazından bir fonksiyon çağırıyoruz.

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

    });
});

$ Kapsamı yerine $ rootScope da kullanabilirsiniz. Denetleyicinizi buna göre kullanın.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.