56

Currently working on a project where we found huge memory leaks when not clearing broadcast subscriptions off destroyed scopes. The following code has fixed this:

var onFooEventBroadcast = $rootScope.$on('fooEvent', doSomething);

scope.$on('$destroy', function() {
    //remove the broadcast subscription when scope is destroyed
    onFooEventBroadcast();
});

Should this practice also be used for watches? Code example below:

var onFooChanged = scope.$watch('foo', doSomething);

scope.$on('$destroy', function() {
    //stop watching when scope is destroyed
    onFooChanged();
});
Andrew
  • 5,525
  • 5
  • 36
  • 40
  • 3
    why don't you use `$on` on the current `$scope`? Subscriptions of the current `$scope` will be removed when the scope gets destroyed. – Sabacc Aug 04 '14 at 08:02
  • 1
    By the way you can simply pass the oFooEventBroadcast de-registration function as follows $scope.$on('$destroy', onFooEventBroadcast) . That you don't need to call it yourself. – Willa Apr 07 '16 at 12:49

2 Answers2

90

No, you don't need to remove $$watchers, since they will effectively get removed once the scope is destroyed.

From Angular's source code (v1.2.21), Scope's $destroy method:

$destroy: function() {
    ...
    if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
    if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
    if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
    if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling;
    ...
    this.$$watchers = this.$$asyncQueue = this.$$postDigestQueue = [];
    ...

So, the $$watchers array is emptied (and the scope is removed from the scope hierarchy).

Removing the watcher from the array is all the unregister function does anyway:

$watch: function(watchExp, listener, objectEquality) {
    ...
    return function deregisterWatch() {
        arrayRemove(array, watcher);
        lastDirtyWatch = null;
    };
}

So, there is no point in unregistering the $$watchers "manually".


You should still unregister event listeners though (as you correctly mention in your post) !

NOTE: You only need to unregister listeners registered on other scopes. There is no need to unregister listeners registered on the scope that is being destroyed.
E.g.:

// You MUST unregister these
$rootScope.$on(...);
$scope.$parent.$on(...);

// You DON'T HAVE to unregister this
$scope.$on(...)

(Thx to @John for pointing it out)

Also, make sure you unregister any event listeners from elements that outlive the scope being destroyed. E.g. if you have a directive register a listener on the parent node or on <body>, then you must unregister them too.
Again, you don't have to remove a listener registered on the element being destroyed.


Kind of unrelated to the original question, but now there is also a $destroyed event dispatched on the element being destroyed, so you can hook into that as well (if it's appropriate for your usecase):

link: function postLink(scope, elem) {
  doStuff();
  elem.on('$destroy', cleanUp);
}
Community
  • 1
  • 1
gkalpak
  • 47,844
  • 8
  • 105
  • 118
  • Another case would be when you a directive with isolated scope and you need to manually watch something on the parent scope: ```$scope.$on('$destroy', $scope.$parent.$watch(..));``` – VanTanev Jan 31 '15 at 18:55
  • 2
    Actually, seems that there is **no need** to deregister event listeners **if they are registered on the scope that is being destroyed**. I mean: `$scope.$on('fooEvent', doSomething); //No need to deregister` `$rootScope.$on('fooEvent', doSomething); //$rootScope is never destroyed, you need to deregister this one Seen on: http://stackoverflow.com/questions/26983696/angularjs-does-destroy-remove-event-listeners http://www.bennadel.com/blog/2723-unbinding-scope-on-event-handlers-in-angularjs.htm – John Bernardsson May 27 '15 at 10:49
  • @John: True, but in OP's code (see the question) they are registering listeners on the `$rootScope` and **must** be removed "manually". – gkalpak May 27 '15 at 10:51
  • @ExpertSystem Yup, just saying that maybe would be better to modify a bit that statement ;) I was ready to unbind every event on my scope when I read it, lol – John Bernardsson May 27 '15 at 10:53
  • 1
    @John: I just saw your updated comment. Good point. I will update the answer. Thx ! – gkalpak May 27 '15 at 10:54
  • Does this also hold true, if we are watching a variable from rootScope in the current scope, does that watch also get deregistered on destroy ? – gaurav5430 Jun 30 '16 at 05:09
  • 1
    @gaurav5430, yes. It doesn't matter what is being watched, but who watches it (i.e. what scope is the watcher on). So, if you are calling `$scope.$watch(...)`, you don't need to deregister. If you are using `$rootScope.$watch(...)`, you do. – gkalpak Jun 30 '16 at 08:09
3

I would like to add too @gkalpak's answer as it lead me in the right direction..

The application I was working on created a memory leak by replacing directives whom had watches. The directives were replaced using jQuery and then complied.

To fix i added the following link function

link: function (scope, elem, attrs) {
    elem.on('$destroy', function () {
        scope.$destroy();
    });
}

it uses the element destroy event to in turn destroy the scope.

Community
  • 1
  • 1
Kieran
  • 17,572
  • 7
  • 45
  • 53