205

https://docs.angularjs.org/guide/directive

By listening to this event, you can remove event listeners that might cause memory leaks. Listeners registered to scopes and elements are automatically cleaned up when they are destroyed, but if you registered a listener on a service, or registered a listener on a DOM node that isn't being deleted, you'll have to clean it up yourself or you risk introducing a memory leak.

Best Practice: Directives should clean up after themselves. You can use element.on('$destroy', ...) or scope.$on('$destroy', ...) to run a clean-up function when the directive is removed.

Question:

I have a element.on "click", (event) -> inside my directive:

  1. When the directive is destroyed, are there any memory references to the element.on to keep it from being garbage collected?
  2. Angular documentation states that I should use a handler to remove event listeners on the $destroy emitted event. I was under the impression that destroy() removed event listeners, is this not the case?
Community
  • 1
  • 1
dman
  • 10,406
  • 18
  • 102
  • 201

1 Answers1

446

Event listeners

First off it's important to understand that there are two kinds of "event listeners":

  1. Scope event listeners registered via $on:

    $scope.$on('anEvent', function (event, data) {
      ...
    });
    
  2. Event handlers attached to elements via for example on or bind:

    element.on('click', function (event) {
      ...
    });
    

$scope.$destroy()

When $scope.$destroy() is executed it will remove all listeners registered via $on on that $scope.

It will not remove DOM elements or any attached event handlers of the second kind.

This means that calling $scope.$destroy() manually from example within a directive's link function will not remove a handler attached via for example element.on, nor the DOM element itself.


element.remove()

Note that remove is a jqLite method (or a jQuery method if jQuery is loaded before AngularjS) and is not available on a standard DOM Element Object.

When element.remove() is executed that element and all of its children will be removed from the DOM together will all event handlers attached via for example element.on.

It will not destroy the $scope associated with the element.

To make it more confusing there is also a jQuery event called $destroy. Sometimes when working with third-party jQuery libraries that remove elements, or if you remove them manually, you might need to perform clean up when that happens:

element.on('$destroy', function () {
  scope.$destroy();
});

What to do when a directive is "destroyed"

This depends on how the directive is "destroyed".

A normal case is that a directive is destroyed because ng-view changes the current view. When this happens the ng-view directive will destroy the associated $scope, sever all the references to its parent scope and call remove() on the element.

This means that if that view contains a directive with this in its link function when it's destroyed by ng-view:

scope.$on('anEvent', function () {
 ...
});

element.on('click', function () {
 ...
});

Both event listeners will be removed automatically.

However, it's important to note that the code inside these listeners can still cause memory leaks, for example if you have achieved the common JS memory leak pattern circular references.

Even in this normal case of a directive getting destroyed due to a view changing there are things you might need to manually clean up.

For example if you have registered a listener on $rootScope:

var unregisterFn = $rootScope.$on('anEvent', function () {});

scope.$on('$destroy', unregisterFn);

This is needed since $rootScope is never destroyed during the lifetime of the application.

The same goes if you are using another pub/sub implementation that doesn't automatically perform the necessary cleanup when the $scope is destroyed, or if your directive passes callbacks to services.

Another situation would be to cancel $interval/$timeout:

var promise = $interval(function () {}, 1000);

scope.$on('$destroy', function () {
  $interval.cancel(promise);
});

If your directive attaches event handlers to elements for example outside the current view, you need to manually clean those up as well:

var windowClick = function () {
   ...
};

angular.element(window).on('click', windowClick);

scope.$on('$destroy', function () {
  angular.element(window).off('click', windowClick);
});

These were some examples of what to do when directives are "destroyed" by Angular, for example by ng-view or ng-if.

If you have custom directives that manage the lifecycle of DOM elements etc. it will of course get more complex.

tasseKATT
  • 38,470
  • 8
  • 84
  • 65
  • 4
    '$rootScope is never destroyed during the lifetime of the application.' : obvious once you think of it. That's what I was missing. – user276648 Feb 05 '15 at 05:40
  • @tasseKATT A small question here, If in same controller we have multiple $rootScope.$on for different events, then shall we call $scope.$on("$destroy", ListenerName1); for each $rootScope.$on differently ?? – Yashika Garg May 08 '15 at 07:08
  • 2
    @YashikaGarg It'd probably be easiest to just have a helper function that calls all the listeners. Like $scope.$on('$destroy'), function() {ListenerName1(); ListenerName2();...}); Is there any additional complexity for $on event handlers on non-isolate scopes? Or isolate scopes with two way bindings? – David Rice Jun 05 '15 at 13:49
  • Why register event listeners on $rootscope? I register event listeners on $scope and then other controllers perform $rootscope.broadcast('eventname') and my event listeners run. Are these event listeners on $scope that are listening to application events still going to be auto cleaned? – Skystrider Oct 02 '15 at 14:43
  • @Skychan Sorry I missed your comment. This is a guess, but people might use `$rootScope` because of this: http://stackoverflow.com/questions/11252780/whats-the-correct-way-to-communicate-between-controllers-in-angularjs/19498009#19498009 Note that as the answer states at the top, this has been changed. Yes, event listeners on the normal `$scope` will be auto cleaned when that scope is destroyed. – tasseKATT Dec 22 '15 at 20:30
  • @tasseKAT Great info. Know this is slightly off topic but could you shed some light on the situation where the scope is inherited? i create a simple [fiddle](https://jsfiddle.net/oxx6g76p/) to demo the use case i mean. if you create one child directive and remove it all works. If you have more than one child and remove one of them it breaks the others. If you call `$destroy()` in the `kill()` method it seems to remove the parent also. – ste2425 Jan 05 '16 at 15:33
  • @ste2425 In your fiddle, the `child` directive will not create a new scope, so there is no inheritance. You can add `console.log($scope.$id)` in the link function to see that it's the same scope each time a child is created. This means that every child will have the same `kill` function, and `e` in that function will be the last created child element. So if you create ten children, all kill buttons will remove the last element. If you add `scope: true` to the child directive you will get another behavior. – tasseKATT Jan 05 '16 at 15:54
  • Do you also need to deregister the `'$destroy'` listener itself, like this? `$element.on('$destroy', onDestroy); function onDestroy () { /* ... */; $element.off('$destroy', onDestroy); }` – Matthias Jul 05 '16 at 18:52
  • @tasseKATT: appreciate for your answer but to be honest it is still not clear about to questions asked by `damn`, which are 1>When the directive is destroyed, are there any memory references to the element.on to keep it from being garbage collected? 2>Angular documentation states that I should use a handler to remove event listeners on the $destroy emitted event. I was under the impression that destroy() removed event listeners, is this not the case? – Sohail Faruqui Jan 15 '17 at 15:46
  • for example I have a directive which will register event handler on my element `(element.on('touchstart'), function() {//some code})` and this directive is used in most of my pages (states) , say I can navigate to another sates which uses same directive to display data or even I reinitialize the same state on some actions which will ultimately reinitialize the directive or can navigate to an state where this directive not being user and come back to the previous state which this directive being used. should I perform cleanup and how? – Sohail Faruqui Jan 15 '17 at 15:53