¿Cuál es la forma correcta de comunicarse entre controladores en AngularJS?

473

¿Cuál es la forma correcta de comunicarse entre los controladores?

Actualmente estoy usando un horrible dulce de azúcar que involucra 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
fuente
36
Totalmente discutible, pero en Angular, siempre debe usar $ window en lugar del objeto de ventana JS nativo. De esta manera, puede apagarlo en sus pruebas :)
Dan M
1
Vea el comentario en la respuesta a continuación de mí con respecto a este problema. $ broadcast ya no es más caro que $ emit. Vea el enlace jsperf al que hice referencia allí.
zumalifeguard

Respuestas:

457

Editar : El problema abordado en esta respuesta se ha resuelto en angular.js versión 1.2.7 . $broadcastahora evita burbujear sobre ámbitos no registrados y se ejecuta tan rápido como $ emit. Las actuaciones de $ broadcast son idénticas a $ emit con angular 1.2.16

Entonces, ahora puedes:

  • uso $broadcastde la$rootScope
  • escuche usando $on el local$scope que necesita saber sobre el evento

Respuesta original a continuación

Recomiendo no usar $rootScope.$broadcast+ $scope.$onsino más bien $rootScope.$emit+ $rootScope.$on. El primero puede causar serios problemas de rendimiento como lo plantea @numan. Esto se debe a que el evento burbujeará en todos los ámbitos.

Sin embargo, este último (usando $rootScope.$emit+ $rootScope.$on) no sufre esto y, por lo tanto, puede usarse como un canal de comunicación rápido.

De la documentación angular de $emit:

Envía un nombre de evento hacia arriba a través de la jerarquía de alcance que notifica a los registrados

Como no hay un alcance arriba $rootScope, no hay burbujeo. Es totalmente seguro usar $rootScope.$emit()/ $rootScope.$on()como EventBus.

Sin embargo, hay un problema al usarlo desde Controladores. Si se vincula directamente $rootScope.$on()desde dentro de un controlador, tendrá que limpiar el enlace usted mismo cuando su local $scopesea ​​destruido. Esto se debe a que los controladores (en contraste con los servicios) pueden instanciarse varias veces durante la vida útil de una aplicación, lo que daría como resultado que los enlaces se sumen y eventualmente creen pérdidas de memoria por todas partes :)

Para cancelar el registro, simplemente escuchar en su $scope's $destroyevento y luego llamar a la función que devolvió $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);
        }
    ]);

Diría que eso no es realmente algo angular específico, ya que también se aplica a otras implementaciones de EventBus, que tienes que limpiar los recursos.

Sin embargo, puede facilitarle la vida en esos casos. Por ejemplo, podría mono parchear $rootScopey darle un $onRootScopeque se suscribe a los eventos emitidos en el $rootScopepero también limpia directamente el controlador cuando el local$scope se destruye.

La forma más limpia de parchear el mono $rootScopepara proporcionar dicho $onRootScopemétodo sería a través de un decorador (un bloque de ejecución probablemente también funcionará bien, pero pssst, no se lo digas a nadie)

Para asegurarnos de que la $onRootScopepropiedad no se muestre inesperadamente al enumerar $scope, usamos Object.defineProperty()y establecemos enumerableen false. Tenga en cuenta que es posible que necesite una cuña 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;
        }]);
    }]);

Con este método, el código del controlador de arriba se puede simplificar para:

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

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

Como resultado final de todo esto, le recomiendo que use $rootScope.$emit+$scope.$onRootScope .

Por cierto, estoy tratando de convencer al equipo angular para abordar el problema dentro del núcleo angular. Hay una discusión aquí: https://github.com/angular/angular.js/issues/4574

Aquí hay un jsperf que muestra cuánto impacto de rendimiento $broadcasttrae a la mesa en un escenario decente con solo 100 $scope's.

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

resultados jsperf

Christoph
fuente
Estoy tratando de hacer su segunda opción, pero recibo un error: Error de tipo no capturado: No se puede redefinir la propiedad: $ onRootScope justo donde estoy haciendo Object.defineProperty ...
Scott
Quizás arruiné algo cuando lo pegué aquí. Lo uso en producción y funciona muy bien. Voy a echar un vistazo mañana :)
Christoph
@Scott Lo pegué pero el código ya era correcto y es exactamente lo que usamos en la producción. ¿Puede verificar que no tiene un error tipográfico en su sitio? ¿Puedo ver su código en alguna parte para ayudar a solucionar problemas?
Christoph el
@ Christoph, ¿hay una buena manera de hacer el decorador en IE8, ya que no admite Object.defineProperty en objetos que no son DOM?
joshschreuder
59
Esta fue una solución muy inteligente para el problema, pero ya no es necesaria. La última versión de Angular (1.2.16), y probablemente anterior, tiene este problema solucionado. Ahora $ broadcast no visitará todos los controladores descendientes por ninguna razón. Solo visitará a aquellos que realmente estén escuchando el evento. Actualicé el jsperf mencionado anteriormente para demostrar que el problema ahora está solucionado: jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
zumalifeguard
107

La respuesta superior aquí fue una solución a un problema angular que ya no existe (al menos en las versiones> 1.2.16 y "probablemente anterior") como ha mencionado @zumalifeguard. Pero me quedo leyendo todas estas respuestas sin una solución real.

Me parece que la respuesta ahora debería ser

  • uso $broadcastde la$rootScope
  • escuche usando $on el local$scope que necesita saber sobre el evento

Para publicar

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

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

}]);

Y suscríbete

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

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

}]);

Saqueadores

Si registra el oyente en el local $scope, se destruirá automáticamente por $destroysí mismo cuando se elimine el controlador asociado.

poshest
fuente
1
¿Sabes si este mismo patrón se puede usar con la controllerAssintaxis? Pude usar $rootScopeel suscriptor para escuchar el evento, pero tenía curiosidad por saber si había un patrón diferente.
Bordes
3
@edhedges Supongo que podrías inyectarlo $scopeexplícitamente. John Papa escribe sobre los eventos como una "excepción" a su regla habitual de mantener $scope"fuera" de sus controladores (uso citas porque, como él menciona, Controller Astodavía lo tiene $scope, está justo debajo del capó).
poshest
Por debajo del capó, ¿quiere decir que todavía puede llegar a través de la inyección?
Bordes
2
@edhedges Actualicé mi respuesta con una controller asalternativa de sintaxis según lo solicitado. Espero que sea lo que quisiste decir.
poshest
3
@dsdsdsdsd, los servicios / fábricas / proveedores permanecerán para siempre. Siempre hay uno y solo uno de ellos (singletons) en una aplicación Angular. Los controladores, por otro lado, están vinculados a la funcionalidad: componentes / directivas / ng-controller, que pueden repetirse (como objetos hechos de una clase) y van y vienen según sea necesario. ¿Por qué desea que un control y su controlador sigan existiendo cuando ya no lo necesita? Esa es la definición misma de una pérdida de memoria.
más elegante
42

Dado que defineProperty tiene un problema de compatibilidad del navegador, creo que podemos pensar en usar un servicio.

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

y úsalo en un 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
fuente
77
+1 para la baja automática cuando se destruye el alcance.
Federico Nafria
66
Me gusta esta solución 2 cambios que realicé: (1) permitir que el usuario pase 'datos' al mensaje de emisión (2) haga que el paso de 'alcance' sea opcional para que pueda usarse en servicios singleton y en controladores. Puede ver esos cambios implementados aquí: gist.github.com/turtlemonvh/10686980/…
turtlemonvh
20

GridLinked publicó una solución PubSub que parece estar diseñada bastante bien. El servicio se puede encontrar, aquí .

También un diagrama de su servicio:

Servicio de mensajería

Ryan Schumacher
fuente
15

En realidad, el uso de emitir y emitir es ineficiente porque el evento sube y baja en la jerarquía del alcance, lo que puede degradarse fácilmente en una botella de rendimiento para una aplicación compleja.

Sugeriría utilizar un servicio. Así es como lo implementé recientemente en uno de mis proyectos: https://gist.github.com/3384419 .

Idea básica: registrar un pub / club de eventos como servicio. Luego inyecte ese bus de eventos donde necesite suscribirse o publicar eventos / temas.

numan salati
fuente
77
Y cuando ya no se necesita un controlador, ¿cómo se da de baja automáticamente? Si no hace esto, debido al cierre, el controlador nunca se eliminará de la memoria y seguirá sintiendo mensajes. Para evitar esto, deberá eliminarlo manualmente. Usar $ en esto no ocurrirá.
Renan Tomal Fernandes
1
Ese es un punto justo. Creo que puede resolverse mediante la arquitectura de su aplicación. en mi caso, tengo una aplicación de una sola página, así que es un problema más manejable. Dicho esto, creo que esto sería mucho más limpio si angular tuviera ganchos de ciclo de vida de componentes donde pudiera conectar / desconectar cosas como esta.
numan salati
66
Acabo de dejar esto aquí ya que nadie lo dijo antes. Usar rootScope como EventBus no es ineficiente, ya que $rootScope.$emit()solo burbujea hacia arriba. Sin embargo, como no hay un alcance superior, $rootScopeno hay nada que temer. Entonces, si solo está usando $rootScope.$emit()y $rootScope.$on()tendrá un EventBus rápido en todo el sistema.
Christoph
1
Lo único que debe tener en cuenta es que si lo usa $rootScope.$on()dentro de su controlador, deberá limpiar el enlace del evento, ya que de lo contrario se resumirán, ya que se crea uno nuevo cada vez que se crea una instancia del controlador y no lo hacen. ser destruido automáticamente por usted ya que está vinculado $rootScopedirectamente.
Christoph
La última versión de Angular (1.2.16), y probablemente anterior, tiene este problema solucionado. Ahora $ broadcast no visitará todos los controladores descendientes por ninguna razón. Solo visitará a aquellos que realmente estén escuchando el evento. Actualicé el jsperf mencionado anteriormente para demostrar que el problema ahora está solucionado: jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
zumalifeguard
14

Usando métodos get y set dentro de un servicio, puede pasar mensajes entre controladores muy fácilmente.

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

en HTML HTML puedes marcar así

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

<div ng-controller='SecondController'>
    {{rslt}}
</div>
Reconocimiento de carga
fuente
+1 Uno de esos métodos obvios, una vez que te lo dicen, ¡excelente! Implementé una versión más general con los métodos set (key, value) y get (key), una alternativa útil a $ broadcast.
TonyWilk
8

Con respecto al código original, parece que desea compartir datos entre ámbitos. Para compartir datos o estados entre $ scope, los documentos sugieren usar un servicio:

  • Para ejecutar código sin estado o con estado compartido entre los controladores: utilice los servicios angulares en su lugar.
  • Para crear instancias o administrar el ciclo de vida de otros componentes (por ejemplo, para crear instancias de servicio).

Ref: Enlace de Documentos Angulares aquí

pkbyron
fuente
5

De hecho, comencé a usar Postal.js como un bus de mensajes entre controladores.

Tiene muchos beneficios como bus de mensajes, como los enlaces de estilo AMQP, la forma en que postal puede integrarse con iFrames y sockets web, y muchas cosas más.

Utilicé un decorador para configurar Postal en $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;
    }]);
});

Aquí hay un enlace a una publicación de blog sobre el tema ...
http://jonathancreamer.com/an-angular-event-bus-with-postal-js/

jcreamer898
fuente
3

Así es como lo hago con Factory / Services y la inyección de dependencia simple (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
fuente
1
Sus dos controladores no se comunican, solo usan un mismo servicio. Eso no es lo mismo.
Greg
@Greg puede lograr lo mismo con menos código al tener un servicio compartido y agregar $ relojes cuando sea necesario.
Capaj
3

Me gustó la forma en que $rootscope.emitse utilizó para lograr la intercomunicación. Sugiero la solución limpia y efectiva de rendimiento sin contaminar el espacio 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
fuente
Para la pérdida de memoria, debe agregar un método adicional para anular el registro de los oyentes de eventos. De todos modos buena muestra trivial
Raffaeu
2

Aquí está la manera rápida y sucia.

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

    };

}

Aquí hay una función de ejemplo para agregar dentro de cualquiera de los controladores hermanos:

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

    alert('Sink that ship!');                       

});

y, por supuesto, aquí está tu HTML:

<button ngclick="sendorders()">Sink Enemy Ships</button>
Peter Drinnan
fuente
16
¿Por qué no te inyectas $rootScope?
Pieter Herroelen
1

Iniciando angular 1.5 y su enfoque de desarrollo basado en componentes. La forma recomendada para que los componentes interactúen es mediante el uso de la propiedad 'require' y mediante enlaces de propiedades (entrada / salida).

Un componente requeriría otro componente (por ejemplo, el componente raíz) y obtendría una referencia a su controlador:

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

Luego puede usar los métodos del componente raíz en su componente hijo:

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

Esta es la función del controlador del componente raíz:

function addWatchedBook(bookName){

  booksWatched.push(bookName);

}

Aquí hay una descripción arquitectónica completa: Comunicaciones de componentes

kevinius
fuente
0

Puede acceder a esta función de saludo desde cualquier lugar del módulo

Controlador uno

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

segundo controlador

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

Más información aquí.

Prashobh
fuente
77
Un poco tarde para la fiesta pero: no hagas esto. Poner una función en el ámbito raíz es similar a hacer que una función sea global, lo que puede causar todo tipo de problemas.
Dan Pantry
0

Crearé un servicio y usaré notificaciones.

  1. Crear un método en el Servicio de notificaciones
  2. Cree un método genérico para transmitir notificaciones en el Servicio de notificaciones.
  3. Desde el controlador de origen, llame al NoticeService.Method. También paso el objeto correspondiente para persistir si es necesario.
  4. Dentro del método, persisto los datos en el servicio de notificación y llamo al método de notificación genérico.
  5. En el controlador de destino escucho ($ scope.on) el evento de transmisión y accedo a los datos del Servicio de notificaciones.

Como en cualquier momento, el Servicio de notificaciones es único, debería poder proporcionar datos persistentes.

Espero que esto ayude

rahul
fuente
0

Puede usar el servicio incorporado AngularJS $rootScopee inyectar este servicio en sus dos controladores. Luego puede escuchar los eventos que se activan en el objeto $ rootScope.

$ rootScope proporciona dos despachadores de eventos llamados $emit and $broadcastque son responsables de despachar eventos (pueden ser eventos personalizados) y usan la $rootScope.$onfunción para agregar un detector de eventos.

Shivang Gupta
fuente
0

Debe usar el Servicio, ya que $rootscopees el acceso desde toda la Aplicación, y aumenta la carga, o puede usar los parámetros raíz si sus datos no son más.

abhaygarg12493
fuente
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
fuente
0

Puede hacerlo utilizando eventos angulares que son $ emit y $ broadcast. Según nuestro conocimiento, esta es la mejor manera, eficiente y efectiva.

Primero llamamos a una función desde un 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);

    });
});

También puede usar $ rootScope en lugar de $ scope. Use su controlador en consecuencia.

Peeyush Kumar
fuente