Cómo cancelar la suscripción a un evento de difusión en angularJS. Cómo eliminar la función registrada a través de $ en

278

He registrado a mi oyente en un evento $ broadcast usando la función $ on

$scope.$on("onViewUpdated", this.callMe);

y quiero anular el registro de este oyente basado en una regla comercial particular. Pero mi problema es que una vez que está registrado no puedo anular el registro.

¿Hay algún método en AngularJS para anular el registro de un oyente en particular? Un método como $ en que anula el registro de este evento, puede ser $ off. Entonces, en base a la lógica de negocios, puedo decir

 $scope.$off("onViewUpdated", this.callMe);

y esta función deja de llamarse cuando alguien difunde el evento "onViewUpdated".

Gracias

EDITAR : Quiero cancelar el registro del oyente de otra función. No es la función donde lo registro.

Hitesh.Aneja
fuente
2
Para cualquiera que se pregunte, la función devuelta está documentada aquí
Fagner Brack,

Respuestas:

477

Debe almacenar la función devuelta y llamarla para darse de baja del evento.

var deregisterListener = $scope.$on("onViewUpdated", callMe);
deregisterListener (); // this will deregister that listener

Esto se encuentra en el código fuente :) al menos en 1.0.4. Voy a publicar el código completo ya que es corto

/**
  * @param {string} name Event name to listen on.
  * @param {function(event)} listener Function to call when the event is emitted.
  * @returns {function()} Returns a deregistration function for this listener.
  */
$on: function(name, listener) {
    var namedListeners = this.$$listeners[name];
    if (!namedListeners) {
      this.$$listeners[name] = namedListeners = [];
    }
    namedListeners.push(listener);

    return function() {
      namedListeners[indexOf(namedListeners, listener)] = null;
    };
},

Además, vea los documentos .

Liviu T.
fuente
Si. Después de depurar el código sorce descubrí que hay una matriz de oyentes $$ que tiene todos los eventos y creó mi función $ off. Gracias
Hitesh.Aneja
¿Cuál es el caso de uso real en el que no puede usar la forma angular proporcionada para cancelar el registro? ¿La cancelación del registro se realiza en otro ámbito que no está vinculado al ámbito que creó el oyente?
Liviu T.
1
Sí, he eliminado mi respuesta porque no quiero confundir a la gente. Esta es la forma correcta de hacer esto.
Ben Lesh
3
@Liviu: Eso se convertirá en un dolor de cabeza con la creciente aplicación. No es solo este evento, también hay muchos otros eventos y no necesariamente que siempre me registre en la misma función de alcance. Puede haber casos cuando estoy llamando a una función que está registrando este oyente pero cancelando el registro del oyente en otra llamada, incluso en esos casos no obtendré la referencia a menos que los almacene fuera de mi alcance. Entonces, para mi implementación actual, mi implementación me parece una solución viable. Pero definitivamente me gustaría saber las razones por las que AngularJS lo hizo de esta manera.
Hitesh.Aneja
2
Creo que Angular lo hizo de esta manera porque muchas veces las funciones anónimas en línea se usan como argumentos para la función $ on. Para llamar a $ scope. $ Off (tipo, función) deberíamos mantener una referencia a la función anónima. Simplemente está pensando de una manera diferente a cómo uno normalmente agregaría / eliminaría oyentes de eventos en un lenguaje como ActionScript o el patrón Observable en Java
dannrob
60

Mirando la mayoría de las respuestas, parecen demasiado complicadas. Angular ha incorporado mecanismos para cancelar el registro.

Utilice la función de desregistro devuelta por$on :

// Register and get a handle to the listener
var listener = $scope.$on('someMessage', function () {
    $log.log("Message received");
});

// Unregister
$scope.$on('$destroy', function () {
    $log.log("Unregistering listener");
    listener();
});
long2know
fuente
Tan simple como estos, hay muchas respuestas, pero esto es más conciso.
David Aguilar
8
Técnicamente correcto, aunque un poco engañoso, porque $scope.$onno es necesario que no se registre manualmente $destroy. Un mejor ejemplo sería usar a $rootScope.$on.
hgoebl
2
mejor respuesta, pero quiero ver más explicaciones sobre por qué llamar a ese oyente dentro de $ destroy mata al oyente.
Mohammad Rafigh
1
@MohammadRafigh Llamar al oyente dentro de $ destroy es justo donde elegí ponerlo. Si recuerdo correctamente, este era un código que tenía dentro de una directiva y tenía sentido que cuando se destruyera el alcance de las directivas, los oyentes no deberían estar registrados.
long2know
@hgoebl No sé a qué te refieres. Si tengo, por ejemplo, una directiva que se usa en varios lugares, y cada uno registra un oyente para un evento, ¿cómo me ayudaría usar $ rootScope. $ On? La disposición de alcance de la directiva parece ser el mejor lugar para disponer de sus oyentes.
long2know
26

Este código me funciona:

$rootScope.$$listeners.nameOfYourEvent=[];
Rustem Mussabekov
fuente
1
Mirar $ rootScope. $$ oyentes también es una buena manera de observar el ciclo de vida del oyente y experimentar con él.
XML
Se ve simple y genial. Creo que se acaba de eliminar la referencia de función. no es asi
Jay Shukla
26
Esta solución no se recomienda porque el miembro $$ oyentes se considera privado. De hecho, cualquier miembro de un objeto angular con el prefijo '$$' es privado por convención.
shovavnik
55
No recomendaría esta opción, porque elimina todos los oyentes, no solo el que necesita eliminar. Puede causar problemas en el futuro cuando agrega otro oyente en otra parte del script.
Rainer Plumer
10

EDITAR: ¡La forma correcta de hacer esto está en la respuesta de @ LiviuT!

Siempre puede ampliar el alcance de Angular para permitirle eliminar dichos oyentes de esta manera:

//A little hack to add an $off() method to $scopes.
(function () {
  var injector = angular.injector(['ng']),
      rootScope = injector.get('$rootScope');
      rootScope.constructor.prototype.$off = function(eventName, fn) {
        if(this.$$listeners) {
          var eventArr = this.$$listeners[eventName];
          if(eventArr) {
            for(var i = 0; i < eventArr.length; i++) {
              if(eventArr[i] === fn) {
                eventArr.splice(i, 1);
              }
            }
          }
        }
      }
}());

Y así es como funcionaría:

  function myEvent() {
    alert('test');
  }
  $scope.$on('test', myEvent);
  $scope.$broadcast('test');
  $scope.$off('test', myEvent);
  $scope.$broadcast('test');

Y aquí hay un golpeador en acción

Ben Lesh
fuente
¡Trabajado como un encanto! pero lo edité un poco, lo puse en la sección
.run
Amo esta solución. Es una solución mucho más limpia, mucho más fácil de leer. +1
Rick
7

Después de depurar el código, creé mi propia función al igual que la respuesta de "blesh". Entonces esto es lo que hice

MyModule = angular.module('FIT', [])
.run(function ($rootScope) {
        // Custom $off function to un-register the listener.
        $rootScope.$off = function (name, listener) {
            var namedListeners = this.$$listeners[name];
            if (namedListeners) {
                // Loop through the array of named listeners and remove them from the array.
                for (var i = 0; i < namedListeners.length; i++) {
                    if (namedListeners[i] === listener) {
                        return namedListeners.splice(i, 1);
                    }
                }
            }
        }
});

entonces al adjuntar mi función a $ rootscope ahora está disponible para todos mis controladores.

y en mi código estoy haciendo

$scope.$off("onViewUpdated", callMe);

Gracias

EDITAR: ¡La forma de AngularJS de hacer esto está en la respuesta de @ LiviuT! Pero si desea anular el registro del oyente en otro ámbito y, al mismo tiempo, desea mantenerse alejado de la creación de variables locales para mantener referencias de la función de anulación del registro. Esta es una posible solución.

Hitesh.Aneja
fuente
1
De hecho, estoy borrando mi respuesta, porque la respuesta de @ LiviuT es 100% correcta.
Ben Lesh
La respuesta de @blesh LiviuT es correcta y, en realidad, un enfoque proporcionado por angualar para cancelar el registro, pero no se conecta bien para los escenarios en los que tiene que cancelar el registro del oyente en un ámbito diferente. Entonces esta es una alternativa fácil.
Hitesh.Aneja
1
Proporciona la misma conexión que cualquier otra solución. Simplemente pondría la variable que contiene la función de destrucción en un cierre exterior o incluso en una colección global ... o en cualquier lugar que desee.
Ben Lesh
No quiero seguir creando variables globales para mantener referencias de las funciones de desregistro y tampoco veo ningún problema con el uso de mi propia función $ off.
Hitesh.Aneja
1

La respuesta de @ LiviuT es increíble, pero parece dejar a muchas personas preguntándose cómo volver a acceder a la función de desmantelamiento del controlador desde otro $ alcance o función, si desea destruirla desde un lugar distinto de donde se creó. La respuesta de @ Рустем Мусабеков funciona muy bien, pero no es muy idiomática. (Y se basa en lo que se supone que es un detalle de implementación privado, que podría cambiar en cualquier momento). Y a partir de ahí, se vuelve más complicado ...

Creo que la respuesta fácil aquí es simplemente llevar una referencia a la función de desmontaje ( offCallMeFnen su ejemplo) en el controlador mismo, y luego llamarla en función de alguna condición; quizás un argumento que incluyas en el evento que $ transmites o $ emites. De este modo, los manipuladores pueden derribarse, cuando quieran, donde quieran, llevando consigo las semillas de su propia destrucción. Al igual que:

// Creation of our handler:
var tearDownFunc = $rootScope.$on('demo-event', function(event, booleanParam) {
    var selfDestruct = tearDownFunc;
    if (booleanParam === false) {
        console.log('This is the routine handler here. I can do your normal handling-type stuff.')
    }
    if (booleanParam === true) {
        console.log("5... 4... 3... 2... 1...")
        selfDestruct();
    }
});

// These two functions are purely for demonstration
window.trigger = function(booleanArg) {
    $scope.$emit('demo-event', booleanArg);
}
window.check = function() {
    // shows us where Angular is stashing our handlers, while they exist
    console.log($rootScope.$$listeners['demo-event'])
};

// Interactive Demo:

>> trigger(false);
// "This is the routine handler here. I can do your normal handling-type stuff."

>> check();
// [function] (So, there's a handler registered at this point.)  

>> trigger(true);
// "5... 4... 3... 2... 1..."

>> check();
// [null] (No more handler.)

>> trigger(false);
// undefined (He's dead, Jim.)

Dos pensamientos:

  1. Esta es una gran fórmula para un controlador de ejecución única. Solo suelta los condicionales y corre selfDestructtan pronto como haya completado su misión suicida.
  2. Me pregunto si el alcance original alguna vez se destruirá correctamente y se recolectará basura, dado que lleva referencias a variables cerradas. Tendría que usar un millón de estos para que incluso sea un problema de memoria, pero tengo curiosidad. Si alguien tiene alguna idea, por favor comparta.
XML
fuente
1

Registre un enlace para cancelar la suscripción de sus oyentes cuando se elimine el componente:

$scope.$on('$destroy', function () {
   delete $rootScope.$$listeners["youreventname"];
});  
Dima
fuente
Si bien no es la forma generalmente aceptada de hacer esto, a veces es la solución necesaria.
Tony Brasunas
1

En caso de que necesite encender y apagar el oyente varias veces, puede crear una función con booleanparámetro

function switchListen(_switch) {
    if (_switch) {
      $scope.$on("onViewUpdated", this.callMe);
    } else {
      $rootScope.$$listeners.onViewUpdated = [];
    }
}
Rico
fuente
0

'$ on' en sí mismo devuelve la función para cancelar el registro

 var unregister=  $rootScope.$on('$stateChangeStart',
            function(event, toState, toParams, fromState, fromParams, options) { 
                alert('state changing'); 
            });

puede llamar a la función unregister () para cancelar el registro de ese oyente

Rajiv
fuente
0

Una forma es simplemente destruir al oyente una vez que haya terminado con él.

var removeListener = $scope.$on('navBarRight-ready', function () {
        $rootScope.$broadcast('workerProfile-display', $scope.worker)
        removeListener(); //destroy the listener
    })
usuario1502826
fuente