Should angular $watch be removed when scope destroyed?

JavascriptAngularjsAngularjs DirectiveAngularjs Scope

Javascript Problem Overview


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

Javascript Solutions


Solution 1 - Javascript

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

Solution 2 - Javascript

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAndrewView Question on Stackoverflow
Solution 1 - JavascriptgkalpakView Answer on Stackoverflow
Solution 2 - JavascriptKieranView Answer on Stackoverflow