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:
When the directive is destroyed, are there any memory references to the element.on to keep it from being garbage collected?
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?
Event listeners
First off it's important to understand that there are two kinds of "event listeners":
Scope event listeners registered via $on:
$scope.$on('anEvent', function (event, data) {
...
});
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.
Related
We subscribe to broadcast events in our controller constructor like this:
var unsubscribe = $scope.$on("event-name", function() { // handle it });
And when controller gets destroyed we unsubscribe like this to prevent memory leak
$scope.$on("$destroy", function() {
unsubscribe();
unsubscribe = null;
});
But somewhere I read that calling this is not necessary as when controller is destroyed subscribed handlers are destroyed automatically (if they are declared on controller's scope - which in this case are).
Is unsubscribing them manually really necessary?
Technically you don't need to unsubscribe to $scope.$on events as the $scope is destroy along with the event listeners anyway. (and you don't need to set the variable to null either).
It's different from $rootScope.$on listeners as the $rootScope is not destroyed.
That being said, it is considered bad practice to listen to events using $scope.$on or create events inside the $scope.
You expose yourself to name conflicts
You're obfuscating your code by adding events that will be hard to
find when the code gets bigger
These events make sense for application-wise events (login, logout,
notification...etc.) but not for small isolated events
("received-product-list")
Refactor your code so that $scope events are replaced with promises.
I have a $rootScope.$on code on top of a controller. I noticed that everytime I load/call this controller, the $rootScope.$on listener increases meaning it will add and add and add a listener, indefinitely as you visit the controller.
I noticed it when I called it via $rootScope.$emit from another controller, the function inside the $rootScope.$on got executed several times even if it was only a single emit/broadcast.
$rootScope.$on('listen', function() {
$scope.displayString();
});
$scope.displayString = function() {
console.log('test'); // This display 7 times because I visit the controller 7 times
}
Is it possible to prevent it from creating another listener instance so that when there is already a listener, it won't create a new one.
You need to deregister the event listener when you controller's scope is destroyed.
The $on function returns a deregistration function that will remove the listener when the function is invoked.
So you can set it up like this:
var deregister = $rootScope.$on('listen', function() {
$scope.displayString();
});
$scope.$on('$destroy', deregister);
Note: this will only work if the controller's scope is actually destroyed (e.g. in a directive that is removed from the DOM or when you navigate to a different route). If that doesn't happen then you will need to work out a way of only registering the event listener once.
I've written a pretty simple directive that adds/removes a css class on an element when the element is clicked.
app.directive('dropdown', function() {
var open = false, element,
callback = function(){
open = !open;
if (open) {
element.addClass('open');
} else {
element.removeClass('open');
}
};
return {
scope: {},
link: function(scope, elem){
element = elem;
elem.bind('click', callback);
scope.$on('$destroy', function(){
elem.unbind('click', callback);
elem.remove();
});
}
};
});
I think that the $destroy method is probably unnecessary. Since I've used the built in jqlite the listener will be destroyed along with the element right? Also is there any benefit to calling elem.remove(). I've seen it in some examples but not sure if I see the need.
Any thoughts appreciated
C
You don't have to remove the element manually for sure. You also don't need to unbind anything from scope because it will be handled by angularjs itsef.
For jquery dom listeners:
In case you are referencing JQuery then angular will use it instead of his internal jqLite implementation. It means that the native jquery remove method will be used for the element removal. And the jquery documentation for remove says:
Similar to .empty(), the .remove() method takes elements out of the
DOM. Use .remove() when you want to remove the element itself, as well
as everything inside it. In addition to the elements themselves, all
bound events and jQuery data associated with the elements are removed.
So i think that you don't need to unbind your listeners.
But I'm not 100% sure about this:)
In your case, you should be fine since the event is bound to the element that gets removed and thus the handler gets destroyed along with the element itself. Now, if your directive binds an event to a parent outside of its own DOM element, then that would need to be removed manually on the $destroy.
However, closure can cause any object to stay alive so that's something you do need to worry about. You could introduce a new function still referencing variable objects in the functions whose scope you are trying to destroy and that prevents GC from doing what you likely want it to. Again, that won't affect your current example, but it's something to always consider.
As the title asks. Are they opposite?
Also how can I remove an event from the $rootScope based on its name?
A simple example would be also welcome.
$destroy is both an event that each scope would listen to and also a method on the scope to trigger that event manually.
It is primarily used to do any actions that you want while that scope is getting destroyed. Instances of scope getting destroyed are:
Switching views via ng-view, the previous controller scope gets destroyed.
A directive is instantiated and that element is removed from the DOM, this applies to all directives, within angular framework and custom.
While on the other hand $broadcast is just used to trigger events which are in current level or child level scope.
They are not the opposite.
For the second question on how you can remove the event from $rootScope.
Lets say you have refined an event listener:
$rootScope.$on("myEvent", function () {
//some code
});
Each event listener returns a deregister function. So you can just use something like:
var removeMyEvent = $rootScope.$on("myEvent", function () {
//some code
});
Whenever you want to remove the event listener you just call the deregister function.
removeMyEvent();
That should do the trick!
Is it recommended to always hang on to the $on method, as in
var dereg = $scope.$on(...)
And later
$scope.$on("$destroy", function() { dereg(); });
Or is this only needed for certain situations? Same question for $watch
AFAIK, you should only use $destroy event when your app may have memory leak issues or zombie events.
$destroy is useful when you pass some of the directive's variables to another directive or controller, and after the element gets removed, yo want to remove it from the reference.
For instance, you may want to use $destory when your directive creates event handlers on global DOM elements and the element with the directive gets removed.
let's say this is a linking function inside a directive:
function myEventListener(){console.log('scroll!');}
$window.bind('mousewheel',myEventListener);
scope.$on('$destroy',function(){$window.unbind(myEventListener)});
If you don't unbind your event listener, then after the element with the directive gets removed, you will still have scroll! messages when you scroll.