Qual é a maneira correta de se comunicar entre os controladores no AngularJS?

473

Qual é a maneira correta de se comunicar entre controladores?

Atualmente, estou usando um fudge horrível envolvendo 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);
    }
}
fadedbee
fonte
36
Totalmente discutível, mas em Angular, você sempre deve usar $ window em vez do objeto nativo da janela JS. Desta forma, você pode stub-lo em seus testes :)
Dan M
1
Por favor, veja o comentário na resposta abaixo de mim em relação a este problema. $ broadcast não é mais mais caro que $ emit. Veja o link jsperf que referenciei lá.
Zumalifeguard

Respostas:

457

Editar : o problema abordado nesta resposta foi resolvido na versão 1.2.7 do angular.js . $broadcastagora evita borbulhar em escopos não registrados e roda tão rápido quanto $ emitem. As performances $ broadcast são idênticas a $ emitem com o angular 1.2.16

Então agora você pode:

  • use a $broadcastpartir do$rootScope
  • ouça usando $on do local$scope que precisa saber sobre o evento

Resposta original abaixo

Eu recomendo não usar $rootScope.$broadcast+, $scope.$onmas sim $rootScope.$emit+ $rootScope.$on. O primeiro pode causar sérios problemas de desempenho, conforme levantado pelo @numan. Isso ocorre porque o evento passará por todos os escopos.

No entanto, o último (usando $rootScope.$emit+ $rootScope.$on) não sofre com isso e pode, portanto, ser usado como um canal de comunicação rápido!

A partir da documentação angular de $emit:

Distribui um nome de evento para cima pela hierarquia de escopo, notificando o registro

Como não há escopo acima $rootScope, não há bolhas acontecendo. É totalmente seguro usar $rootScope.$emit()/ $rootScope.$on()como um EventBus.

No entanto, há um problema ao usá-lo nos Controladores. Se você se vincular diretamente a $rootScope.$on()partir de dentro de um controlador, precisará limpar a ligação quando seu local $scopefor destruído. Isso ocorre porque os controladores (em contraste com os serviços) podem ser instanciados várias vezes ao longo da vida útil de um aplicativo, o que resultaria em ligações que acabariam criando eventualmente vazamentos de memória em todo o lugar :)

Para cancelar o registro, basta ouvir em seu $scope's $destroyevento e, em seguida, chamar a função que foi devolvido pelo $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);
        }
    ]);

Eu diria que isso não é realmente uma coisa específica angular, pois também se aplica a outras implementações do EventBus, que você precisa limpar os recursos.

No entanto, você pode facilitar sua vida para esses casos. Por exemplo, você pode usar o patch do macaco $rootScopee atribuir um $onRootScopeque assine os eventos emitidos no $rootScopemas também limpe diretamente o manipulador quando o local $scopefor destruído.

A maneira mais limpa de fazer o patch do macaco $rootScopepara fornecer esse $onRootScopemétodo seria através de um decorador (um bloco de execução provavelmente também funcionará bem, mas pssst, não conte a ninguém)

Para garantir que a $onRootScopepropriedade não apareça inesperada ao enumerar $scope, usamos Object.defineProperty()e configuramos enumerablecomo false. Lembre-se de que você pode precisar de um calço 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;
        }]);
    }]);

Com este método, o código do controlador acima pode ser simplificado para:

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

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

Portanto, como resultado final de tudo isso, recomendo que você use $rootScope.$emit+ $scope.$onRootScope.

Aliás, estou tentando convencer a equipe angular a resolver o problema dentro do núcleo angular. Há uma discussão em andamento aqui: https://github.com/angular/angular.js/issues/4574

Aqui está um jsperf que mostra o quanto de um impacto de desempenho $broadcasttraz para a mesa em um cenário decente com apenas 100 $scope's.

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

resultados jsperf

Christoph
fonte
Estou tentando fazer a sua segunda opção, mas estou recebendo um erro: TypeError não capturado: Não é possível redefinir a propriedade: $ onRootScope exatamente onde estou fazendo o Object.defineProperty ....
Scott
Talvez eu estraguei alguma coisa quando colei aqui. Eu o uso na produção e funciona muito bem. Vou dar uma olhada amanhã :)
Christoph
@ Scott Eu colei, mas o código já estava correto e é exatamente o que usamos na produção. Você pode verificar se não possui um erro de digitação no seu site? Posso ver seu código em algum lugar para ajudar na solução de problemas?
Christoph
@Christoph Existe uma boa maneira de fazer o decorador no IE8, pois ele não suporta Object.defineProperty em objetos não-DOM?
joshschreuder
59
Essa foi uma solução muito inteligente para o problema, mas não é mais necessária. A versão mais recente do Angular (1.2.16), e provavelmente anterior, tem esse problema corrigido. Agora $ broadcast não visitará todos os controladores descendentes por nenhuma razão. Ele visitará apenas aqueles que realmente estão ouvindo o evento. Eu atualizei o JSPerf mencionado acima para demonstrar que o problema foi corrigido: jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
zumalifeguard
107

A resposta principal aqui foi uma solução alternativa para um problema angular que não existe mais (pelo menos nas versões> 1.2.16 e "provavelmente anterior") como o @zumalifeguard mencionou. Mas fiquei lendo todas essas respostas sem uma solução real.

Parece-me que a resposta agora deve ser

  • use a $broadcastpartir do$rootScope
  • ouça usando $on do local$scope que precisa saber sobre o evento

Então para publicar

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

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

}]);

E se inscrever

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

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

}]);

Plunkers

Se você registrar o ouvinte no local $scope, ele será destruído automaticamente por $destroysi só quando o controlador associado é removido.

chique
fonte
1
Você sabe se esse mesmo padrão pode ser usado com a controllerAssintaxe? Consegui usar $rootScopeo assinante para ouvir o evento, mas fiquei curioso para saber se havia um padrão diferente.
Ededges
3
@edhedges Eu acho que você pode injetar o $scopeexplicitamente. John Papa escreve que os eventos são uma "exceção" à regra usual de manter $scope"fora" de seus controladores (eu uso aspas porque, como ele menciona Controller Asainda o faz $scope, está logo abaixo do capô).
poshest
Por baixo do capô, você quer dizer que ainda consegue injetá-lo?
Ededges
2
@edhedges Atualizei minha resposta com uma controller asalternativa de sintaxe conforme solicitado. Espero que seja isso que você quis dizer.
chique
3
@dsdsdsdsd, serviços / fábricas / provedores ficarão por aqui para sempre. Sempre há um e apenas um deles (singletons) em um aplicativo Angular. Os controladores, por outro lado, estão ligados à funcionalidade: components / diretives / ng-controller, que podem ser repetidos (como objetos feitos de uma classe) e eles vêm e vão conforme necessário. Por que você deseja que um controle e seu controlador continuem existindo quando não é mais necessário? Essa é a própria definição de vazamento de memória.
poshest
42

Como defineProperty tem um problema de compatibilidade do navegador, acho que podemos pensar em usar um serviço.

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

e use-o no controlador como este:

  • controlador 1

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

    function($scope, msgBus) {
        msgBus.onMsg('somemsg', $scope, function() {
            // your logic
        });
    }
Singo
fonte
7
+1 para cancelar a assinatura automática quando o escopo for destruído.
Federico Nafria
6
Eu gosto desta solução. 2 alterações que fiz: (1) permitem ao usuário passar 'dados' para a mensagem de emissão (2) tornam a passagem do 'escopo' opcional para que possa ser usada nos serviços singleton e nos controladores. Você pode ver essas mudanças implementadas aqui: gist.github.com/turtlemonvh/10686980/...
turtlemonvh
20

O GridLinked publicou uma solução PubSub que parece ter sido projetada muito bem. O serviço pode ser encontrado aqui .

Também um diagrama de seu serviço:

Serviço de Mensagens

Ryan Schumacher
fonte
15

Na verdade, o uso de emissão e transmissão é ineficiente porque o evento borbulha para cima e para baixo na hierarquia do escopo, o que pode facilmente se degradar em agrupamento de desempenho para um aplicativo complexo.

Eu sugeriria usar um serviço. Aqui está como eu o implementei recentemente em um dos meus projetos - https://gist.github.com/3384419 .

Ideia básica - registre um pubsub / evento como um serviço. Em seguida, injete o eventbus sempre que precisar para assinar ou publicar eventos / tópicos.

numan salati
fonte
7
E quando um controlador não é mais necessário, como você o cancela automaticamente? Se você não fizer isso, por causa do fechamento, o controlador nunca será removido da memória e você continuará sentindo mensagens para ele. Para evitar isso, você precisará removê-lo manualmente. Usar $ nisto não ocorrerá.
Renan Tomal Fernandes
1
isso é um ponto justo. Eu acho que pode ser resolvido pela maneira como você arquiteta seu aplicativo. no meu caso, eu tenho um aplicativo de página única, por isso é um problema mais gerenciável. Dito isto, acho que isso seria muito mais limpo se a angular tivesse ganchos de ciclo de vida de componentes nos quais você pudesse conectar / desconectar coisas como essa.
Numan salati
6
Acabei de deixar isso aqui, como ninguém afirmou antes. Usar o rootScope como um EventBus não é ineficiente, pois $rootScope.$emit()apenas borbulha para cima. No entanto, como não há escopo acima do, $rootScopenão há nada a temer. Portanto, se você estiver apenas usando $rootScope.$emit()e $rootScope.$on()terá um EventBus rápido em todo o sistema.
21313 Christoph
1
A única coisa que você precisa estar ciente é que, se você usar $rootScope.$on()dentro do seu controlador, precisará limpar a ligação do evento, caso contrário eles serão resumidos, pois criará um novo cada vez que o controlador for instanciado e eles não é destruído automaticamente para você, pois você está vinculando $rootScopediretamente.
Christoph
A versão mais recente do Angular (1.2.16), e provavelmente anterior, tem esse problema corrigido. Agora $ broadcast não visitará todos os controladores descendentes por nenhuma razão. Ele visitará apenas aqueles que realmente estão ouvindo o evento. Eu atualizei o JSPerf mencionado acima para demonstrar que o problema foi corrigido: jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
zumalifeguard
14

Usando métodos get e set em um serviço, você pode transmitir mensagens entre controladores com muita facilidade.

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

em HTML HTML, você pode verificar assim

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

<div ng-controller='SecondController'>
    {{rslt}}
</div>
Reconectar carga
fonte
+1 Um desses métodos óbvios quando você é informado - excelente! Eu implementei uma versão mais geral com os métodos set (key, value) e get (key) - uma alternativa útil ao $ broadcast.
TonyWilk
8

Em relação ao código original - parece que você deseja compartilhar dados entre escopos. Para compartilhar Dados ou Estado entre o escopo $, os documentos sugerem o uso de um serviço:

  • Para executar código sem estado ou sem estado compartilhado entre controladores - Use serviços angulares.
  • Instanciar ou gerenciar o ciclo de vida de outros componentes (por exemplo, para criar instâncias de serviço).

Ref: Link para Documento Angular aqui

pkbyron
fonte
5

Na verdade, comecei a usar o Postal.js como um barramento de mensagens entre controladores.

Existem muitos benefícios para ele, como um barramento de mensagens, como ligações no estilo AMQP, a maneira como o postal pode integrar w / iFrames e soquetes da web e muito mais.

Eu usei um decorador para configurar o 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;
    }]);
});

Aqui está um link para uma postagem de blog sobre o tópico ...
http://jonathancreamer.com/an-angular-event-bus-with-postal-js/

jcreamer898
fonte
3

É assim que eu faço com Fábrica / Serviços e injeção simples de dependência (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
]
Oto Brglez
fonte
1
Seus dois controladores não se comunicam, eles usam apenas um mesmo serviço. Isso não é a mesma coisa.
Greg
@Greg, você pode conseguir a mesma coisa com menos código, tendo um serviço compartilhado e adicionando $ watches, quando necessário.
Capaj
3

Gostei da maneira como $rootscope.emitfoi usado para alcançar a intercomunicação. Sugiro a solução limpa e com desempenho eficiente, sem poluir o espaço 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');
shikhar chauhan
fonte
Para vazamento de memória, você deve adicionar um método extra para cancelar o registro dos ouvintes do evento. De qualquer forma boa amostra trivial
Raffaeu
2

Aqui está o caminho rápido e sujo.

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

    };

}

Aqui está um exemplo de função para adicionar em qualquer um dos controladores irmãos:

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

    alert('Sink that ship!');                       

});

e, claro, aqui está o seu HTML:

<button ngclick="sendorders()">Sink Enemy Ships</button>
Peter Drinnan
fonte
16
Por que você não injeta $rootScope?
Pieter Herroelen
1

Iniciando o angular 1.5 e seu foco no desenvolvimento baseado em componentes. A maneira recomendada para os componentes interagirem é através do uso da propriedade 'require' e através de ligações de propriedade (entrada / saída).

Um componente exigiria outro componente (por exemplo, o componente raiz) e obteria uma referência ao seu controlador:

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

Você pode usar os métodos do componente raiz no componente filho:

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

Esta é a função do controlador de componente raiz:

function addWatchedBook(bookName){

  booksWatched.push(bookName);

}

Aqui está uma visão geral da arquitetura completa: Comunicações de componentes

kevinius
fonte
0

Você pode acessar esta função hello em qualquer lugar do módulo

Controlador um

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

segundo controlador

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

Mais informações aqui

Prashobh
fonte
7
Um pouco atrasado para a festa, mas: não faça isso. Colocar uma função no escopo raiz é semelhante a tornar uma função global, o que pode causar todos os tipos de problemas.
Dan Pantry
0

Vou criar um serviço e usar a notificação.

  1. Crie um método no serviço de notificação
  2. Crie um método genérico para transmitir a notificação no Notification Service.
  3. Do controlador de origem, chame o notificationService.Method. Também passo o objeto correspondente para persistir, se necessário.
  4. Dentro do método, persisto os dados no serviço de notificação e chamo o método de notificação genérico.
  5. No controlador de destino, escuto ($ scope.on) o evento de transmissão e acesso dados do Serviço de Notificação.

Como a qualquer momento o Serviço de Notificação é único, ele deve poder fornecer dados persistentes.

Espero que isto ajude

rahul
fonte
0

Você pode usar o serviço embutido do AngularJS $rootScope e injetar esse serviço nos dois controladores. Você pode ouvir eventos disparados no objeto $ rootScope.

O $ rootScope fornece dois expedidores de eventos chamados $emit and $broadcastresponsáveis ​​pelo envio de eventos (podem ser eventos personalizados) e usam a $rootScope.$onfunção para adicionar ouvintes de eventos.

Shivang Gupta
fonte
0

Você deve usar o Serviço, porque $rootscopeé o acesso de todo o Aplicativo, e aumenta a carga, ou você pode usar os parâmetros raiz se seus dados não forem maiores.

abhaygarg12493
fonte
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);
}
Amin Rahimi
fonte
0

Você pode fazer isso usando eventos angulares que são $ emitem e $ transmitem. De acordo com nosso conhecimento, esta é a melhor maneira, eficiente e eficaz.

Primeiro chamamos uma função de um controlador.

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

    });
});

Você também pode usar $ rootScope no lugar de $ scope. Use seu controlador de acordo.

Peeyush Kumar
fonte