Skip to content

Instantly share code, notes, and snippets.

@joekrump
Last active January 28, 2020 00:29
Show Gist options
  • Save joekrump/bfd13535f06f8681e58a31eb525ddf9a to your computer and use it in GitHub Desktop.
Save joekrump/bfd13535f06f8681e58a31eb525ddf9a to your computer and use it in GitHub Desktop.
AngularJS and 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: Components should clean up after themselves. You can use the $onDestroy() AngularJS component lifecycle hook to run a clean-up function when the component is removed. An older way of doing this would have been to add an event handler for element.on('$destroy', …) or scope.$on('$destroy', …) for an AngularJS Directive.

Question:

I have a

element.on("click", (event) => {...});

inside my component controller code:

When the component is destroyed, are there any memory references to the

element.on(..., (event) => {...});

to keep it from being garbage collected? AngularJS documentation states that the $onDestroy hook should be used for releasing external resources, watches and event handler. See "$onDestroy" section here.

I was under the impression that destroy() removed event listeners, is this not the case?

Answers:

Event listeners

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

Scope event listeners registered via $on. Ex.

$scope.$on('th.filters.clear', function (event, data) {
  ...
});

Event handlers attached to elements via for example on or bind. Ex.

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

$onDestroy()

When a component controller's $onDestroy hook 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.

Also, calling $scope.$destroy() manually, for 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 component is "destroyed"

This depends on how the component is "destroyed".

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

It’s important to note that the code inside these listeners can still cause memory leaks.

Even in the normal case of a component 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 component passes callbacks to services.

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

let promise = this.$interval(function () {}, 1000);

$onDestroy() {
  this.$interval.cancel(promise);
}

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

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

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

$onDestroy() {
  angular.element(window).off('click', windowClick);
}

These were some examples of what to do when components are “destroyed” by AngularJS, for example by ng-view or ng-if.

Initial article source relating to AngularJS Directive

Additional Reading

  1. AngularJS 1.5 lifecylce hooks
@girasquid
Copy link

You know much more Angular than I do. Is it possible to hack how you register listeners to also keep track of them and either clean up or raise exceptions nicely for you if you're missing a cleanup? Wondering about ways you can make the system self-educate here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment