Como cancelar a inscrição em um evento de transmissão no angularJS. Como remover a função registrada via $ on

278

Registrei meu ouvinte em um evento $ broadcast usando $ on function

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

e quero cancelar o registro desse ouvinte com base em uma regra de negócios específica. Mas meu problema é que, uma vez registrado, não consigo cancelar o registro.

Existe algum método no AngularJS para cancelar o registro de um ouvinte específico? Um método como $ para cancelar o registro deste evento pode ser $ off. Então, com base na lógica de negócios, posso dizer

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

e essa função deixa de ser chamada quando alguém transmite o evento "onViewUpdated".

obrigado

EDIT : Quero cancelar o registro do ouvinte de outra função. Não é a função onde eu o registro.

Hitesh.Aneja
fonte
2
Para quem se perguntando, a função retornou está documentado aqui
Fagner Brack

Respostas:

477

Você precisa armazenar a função retornada e chamá-la para cancelar a inscrição no evento.

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

Isso é encontrado no código-fonte :) pelo menos na versão 1.0.4. Vou apenas postar o código completo, já que é curto

/**
  * @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;
    };
},

Além disso, consulte os documentos .

Liviu T.
fonte
Sim. Depois de depurar o código de configuração, descobri que existe uma matriz de ouvintes $$ que possui todos os eventos e criou minha função $ off. Graças
Hitesh.Aneja
Qual é o caso de uso real em que você não pode usar a maneira angular fornecida de cancelamento de registro? O cancelamento de registro feito em outro escopo não está vinculado ao escopo que criou o ouvinte?
30513 Liviu T.
1
Sim, na verdade eu apaguei minha resposta porque não quero confundir as pessoas. Esta é a maneira correta de fazer isso.
precisa
3
@Liviu: Isso se tornará uma dor de cabeça com o aumento da aplicação. Não é apenas neste evento que também existem muitos outros eventos e não necessariamente que eu sempre devo me registrar novamente na mesma função de escopo. Pode haver casos em que estou chamando uma função que está registrando esse ouvinte, mas cancelando o registro do ouvinte em outra chamada, mesmo nesses casos, não receberei a referência a menos que os armazene fora do meu escopo. Portanto, para minha implementação atual, minha implementação parece uma solução viável para mim. Mas, definitivamente, gostaria de saber as razões pelas quais o AngularJS fez isso dessa maneira.
Hitesh.Aneja
2
Eu acho que o Angular fez dessa maneira porque muitas vezes as funções anônimas inline são usadas como argumentos para a função $ on. Para chamar $ scope. $ Off (tipo, função), precisamos manter uma referência à função anônima. É só de pensar de uma maneira diferente à forma como se faria normalmente ouvintes de eventos Adicionar / Remover em uma linguagem como ActionScript ou o padrão observável em Java
dannrob
60

Olhando para a maioria das respostas, elas parecem excessivamente complicadas. Angular construiu mecanismos para cancelar o registro.

Use a função de cancelamento de registro retornada 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
fonte
Tão simples como estas, há muitas respostas, mas isso é mais conciso.
David Aguilar
8
Tecnicamente correto, embora um pouco enganador, porque $scope.$onnão precisa ser cancelado o registro manualmente $destroy. Um exemplo melhor seria usar a $rootScope.$on.
hgoebl
2
melhor resposta, mas deseja ver mais explicações sobre por que chamar esse ouvinte dentro de $ destroy mata o ouvinte.
Mohammad Rafigh
1
@MohammadRafigh Chamar o ouvinte dentro de $ destroy é exatamente onde eu escolhi colocá-lo. Se bem me lembro, esse era o código que eu tinha dentro de uma diretiva e fazia sentido que, quando o escopo das diretivas fosse destruído, os ouvintes não fossem registrados.
long2know
@ hgoebl Não sei o que você quer dizer. Se eu tenho, por exemplo, uma diretiva usada em vários locais e cada um está registrando um ouvinte para um evento, como o $ rootScope. $ Em me ajudar? A disposição do escopo da diretiva parece ser o melhor local para descartar seus ouvintes.
long2know
26

Este código funciona para mim:

$rootScope.$$listeners.nameOfYourEvent=[];
Rustem Mussabekov
fonte
1
Observar os ouvintes $ rootScope. $$ também é uma boa maneira de observar o ciclo de vida do ouvinte e experimentá-lo.
XML
Parece simples e ótimo. Eu acho que é apenas removido referência de função. não é?
Jay Shukla
26
Esta solução não é recomendada porque o membro $$ listeners é considerado privado. De fato, qualquer membro de um objeto angular com o prefixo '$$' é privado por convenção.
shovavnik
5
Eu não recomendaria esta opção, porque remove todos os ouvintes, não apenas o que você precisa remover. Pode causar problemas no futuro quando você adiciona outro ouvinte em outra parte do script.
Rainer Plumer
10

EDIT: A maneira correta de fazer isso é na resposta do @ LiviuT!

Você sempre pode estender o escopo do Angular para permitir a remoção de ouvintes da seguinte maneira:

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

E aqui está como isso funcionaria:

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

E aqui está um exemplo disso em ação

Ben Lesh
fonte
Trabalhou como um encanto! mas eu editei um pouco eu colocá-lo para a seção .run
aimiliano
Adoro esta solução. Cria uma solução muito mais limpa - muito mais fácil de ler. 1
Rick
7

Após depurar o código, criei minha própria função, como a resposta do "blesh". Então foi isso que eu fiz

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

então, anexando minha função ao $ rootscope, agora ela está disponível para todos os meus controladores.

e no meu código eu estou fazendo

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

obrigado

EDIT: A maneira AngularJS de fazer isso é na resposta do @ LiviuT! Mas se você deseja cancelar o registro do ouvinte em outro escopo e, ao mesmo tempo, deseja evitar a criação de variáveis ​​locais para manter as referências da função de cancelamento de registro. Esta é uma solução possível.

Hitesh.Aneja
fonte
1
Na verdade, estou excluindo minha resposta, porque a resposta da @ LiviuT está 100% correta.
Ben Lesh
@blesh A resposta da LiviuT é correta e, na verdade, é uma abordagem angualar fornecida para cancelar o registro, mas não se conecta bem aos cenários em que você precisa cancelar o registro do ouvinte em escopo diferente. Portanto, esta é uma alternativa fácil.
Hitesh.Aneja
1
Ele fornece a mesma conexão que qualquer outra solução faria. Você acabou de colocar a variável que contém a função de destruição em um fechamento externo ou mesmo em uma coleção global ... ou em qualquer lugar que desejar.
precisa
Eu não quero continuar criando variáveis ​​globais para manter referências das funções de cancelamento de registro e também não vejo problemas com o uso da minha própria função $ off.
Hitesh.Aneja
1

A resposta do @ LiviuT é incrível, mas parece deixar muita gente se perguntando como acessar novamente a função de desmontagem do manipulador de outro escopo ou função $, se você quiser destruí-lo de um local diferente daquele onde foi criado. A resposta de @ Рустем Мусабеков funciona muito bem, mas não é muito idiomática. (E se baseia no que é suposto ser um detalhe de implementação privada, que pode mudar a qualquer momento.) E a partir daí, fica mais complicado ...

Eu acho que a resposta fácil aqui é simplesmente levar uma referência à função de desmontagem ( offCallMeFnno exemplo dele) no próprio manipulador e chamá-la com base em alguma condição; talvez um argumento que você inclua no evento que você $ transmite ou $ emite. Assim, os manipuladores podem se derrubar, sempre que quiserem, onde quiserem, carregando as sementes de sua própria destruição. Igual a:

// 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.)

Dois pensamentos:

  1. Essa é uma ótima fórmula para um manipulador executado uma vez. Apenas abandone os condicionais e corra selfDestructassim que concluir sua missão suicida.
  2. Eu me pergunto se o escopo de origem será destruído e coletado corretamente, já que você está carregando referências a variáveis ​​fechadas. Você precisaria usar um milhão deles para que isso fosse um problema de memória, mas estou curioso. Se alguém tiver alguma ideia, compartilhe.
XML
fonte
1

Registre um gancho para cancelar a inscrição de seus ouvintes quando o componente for removido:

$scope.$on('$destroy', function () {
   delete $rootScope.$$listeners["youreventname"];
});  
Dima
fonte
Embora não seja a maneira geralmente aceita de fazer isso, há momentos em que essa é a solução necessária.
Tony Brasunas
1

Caso você precise ativar e desativar o ouvinte várias vezes, é possível criar uma função com o booleanparâmetro

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

'$ on' em si retorna a função para cancelar o registro

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

você pode chamar a função unregister () para cancelar o registro desse ouvinte

Rajiv
fonte
0

Uma maneira é simplesmente destruir o ouvinte assim que terminar.

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