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

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


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.


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

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


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.


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>

+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


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/


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
]

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

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>

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


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


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


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.


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.


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

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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.