-
Notifications
You must be signed in to change notification settings - Fork 27.5k
fix(jqLite): don't attach event handler to comments (memory leak) #7942
Conversation
This is what jQuery does by default: https://github.com/jquery/jquery/blob/c18c6229c84cd2f0c9fe9f6fc3749e2c93608cc7/src/data/accepts.js#L16 We don't need to set data on text/comment nodes internally and if we don't allow setting data on these nodes, we don't need to worry about cleaning it up. BREAKING CHANGE: previously it was possible to set jqLite data on Text/Comment nodes, but now that is allowed only on Element and Document nodes just like in jQuery. We don't expect that app code actually depends on this accidental feature.
@petebacondarwin I don't think this is a complete solution. jqLite still leaks, just not with events anymore (see my test snippet in a196c8b comments). But the other thing is that https://github.com/angular/angular.js/blob/master/src/ng/compile.js#L1005 is still adding a $destroy event to comment nodes which will now do nothing, isn't that still a problem? When running the tests there's about 15 cases where that line adds a $destroy event only to comment nodes. I think it is always nested-transclusion cases and it always adds the $destory event to 2 comment nodes. Yet the tests still pass and I haven't been able to write a failing one (that's what I was trying to do with the |
@jbedard - I agree that there is still potential for a memory leak (as you show in your snippet) but that that particular memory leak does not occur in this case. With this PR in place the line that you are talking about no longer attaches anything to the comment node - jqLite and jQuery will do nothing if you try to attach a handler to a comment. |
Just to be clear - with this fix in place we are no longer adding an event handler (even one that does nothing) the |
The reason that you are not able to make a failing test is that we attach a valid handler to the non-comment nodes that constitute the clone, and that when these elements are removed, the scope is correctly destroyed. |
Where does the handler get attached to the non-comment nodes in the case where the |
As far as I can see at this point they are never only comment nodes. Can you find a situation where they are? |
If you just add a snippet in Something such as... (I haven't run this exact code, but I did something similar this morning, and I'm typing this straight into this comment so no guarantee it runs :)
|
Right. I see. Let's take a look at that... |
It appears that the only time when you get this situation of the clone only consisting of comments is when you have more than one "element" transclusion directive on the same element. So I have these two tests that cover this:
They don't seem to leak onto the cache once we stop attaching event handlers to comments. |
I see, so those are all cases with more then one element transclusion on the same element, where I was assuming it was nested transclusions. We know it won't leak listeners onto comment nodes now, but I was wondering if there is an issue now that there are no element $destroy listeners on those transcluded elements. So if someone manually removes the elements the scope won't be destroyed? That's what I was trying to reproduce in the 'should remove transclusion scope, when the DOM is destroyed' test... |
As far as I can see, all top level elements in a clone will have a $destroy handler attached. Any top level comments in the clone are now ignored. But comments don't have scope attached, while elements do. So you can remove comments with no issues and if you remove elements then the scope will be destroyed. One should be aware though that if there are multiple elements in a clone then removing any one of them will destroy the scope, so they should all be removed at once. I'll put together another test that has something along the lines of:
|
@@ -750,6 +750,12 @@ forEach({ | |||
on: function onFn(element, type, fn, unsupported){ | |||
if (isDefined(unsupported)) throw jqLiteMinErr('onargs', 'jqLite#on() does not support the `selector` or `eventData` parameters'); | |||
|
|||
|
|||
// Do not add event handlers to non-elements because they will not be cleaned up. | |||
if ( element.nodeType && element.nodeType !== 1 && element.nodeType !== 9 ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we now have this check in at least two places. I think we should have a helper test fn instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's the jQuery helper test fn: https://github.com/jquery/jquery/blob/master/src/data/accepts.js
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we allow adding data to arbitrary objects so we probably don't need the jquery version. I'll refactor into a simple helper
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I take that back, the window
object can handle events but has no nodeType
property.
If an element contains two "element" transcludes then the initial clone consists of only comment nodes. The concern was that this meant that the transclude scopes would not be cleaned up. But it turns out that in the case that there are only comments then the scope is never attached to anything so we don't need to worry about cleaning it up. Later if a concrete element is created as part of the transclude then these elements will have destroy handlers.
We were attaching handlers to comment nodes when setting up bound transclusion functions. But we don't clean up comments and text nodes when deallocating so there was a memory leak. Closes angular#7913
expect(calcCacheSize()).toEqual(1); | ||
|
||
$rootScope.$apply('xs = [0,1]'); | ||
expect(calcCacheSize()).toEqual(2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
btw wouldn't the global after each that checks the state of the cache catch the leaks?
These tests are much more readable, so keep things as they are. I'm just curious.
otherwise this looks good. thanks for all the tests |
We were attaching handlers to comment nodes when setting up bound transclusion functions. But we don't clean up comments and text nodes when deallocating so there was a memory leak. Closes angular#7913 Closes angular#7942
No description provided.