-
Notifications
You must be signed in to change notification settings - Fork 27.4k
refactor($compile): remove skipDestroyOnNextJQueryCleanData, fix data leak on some replaced nodes #12094
Conversation
with the patch, is the issue outlined at the comment about jQuery UI an issue? |
@@ -7815,4 +7817,125 @@ describe('$compile', function() { | |||
expect(element.hasClass('fire')).toBe(true); | |||
})); | |||
}); | |||
|
|||
describe('element replacement', function() { | |||
it('should broadcast $destroy only on removed elements, not replaced', function() { |
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.
Does anyone know if this is actually intentional? Why is this done? It seems like the replaced node was never .remove()
ed so this was always the case. But when multi-element directives were added (e46100f) they used .remove()
which does fire $destroy
.
Should $destroy
actually be fired on the replaced node as well?
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.
Did you try to remove the jqLite(firstElementToRemove).off('$destroy');
line and run unit tests? From what I remember the whole skipDestroyOnNextJQueryCleanData
was added because the scope is reused on the replaced elements so removing this logic made existing unit tests fail.
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.
Removing jqLite(firstElementToRemove).off('$destroy');
doesn't cause anything to fail. Even this test designed to fail in that case isn't failing! 😟
I thought of another case though which I think might show that not invoking the DOM $destory is (sometimes) wrong...
<div ng-form="myForm" lower-priority-replace-directive>
In this case the ng-form
directive runs first which adds itself to the parent form then does formElement.on('$destroy', function() { parentFormCtrl.$removeControl(controller); ... })
. Then the other lower priority directive replaces that element with something brand new, that replaced element is not removed from the ng-form because no $destroy was fired. The replaced ng-form controller left behind a) is leaked and b) might cause form validation issues?
However if that low priority directive was doing transclude instead of replace then maybe not invoking the DOM $destory is valid? Meaning replaceWith
would need an invokeDestroy
argument...
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.
Interesting; just removing the skipDestroyOnNextJQueryCleanData
logic from my commit definitely did fail unit tests.
This might be related, I totally forgot about this WIP PR: #8695
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.
Was that PR ready or was there more to look into still?
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'm tempted to just drop this test for now. The cases when $destroy
should/should-not fire are too unclear and I think the current implementation is wrong (or at least super weird) so I'm not sure if I want to enforce that with a test. I think all the other tests are good though (ensuring all elements get passed to cleanData
).
582e790
to
bb808d1
Compare
@mzgol, this is changing what you originally did for jQuery2. What do you think? |
@jbedard I'll take a look on Thursday, I'm on vacation until then. |
} | ||
// Remove $destroy event listeners from `firstElementToRemove` since it is being | ||
// replaced, not destroyed. | ||
jqLite(firstElementToRemove).off('$destroy'); |
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 wouldn't delete the whole above comment. The following:
// Remove data of the replaced element. We cannot just call .remove()
// on the element it since that would deallocate scope that is needed
// for the new node. Instead, remove the data "manually".
seems still relevant to the .off('$destroy')
code.
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'm not sure if that comment is 100% correct. .remove()
doesn't "deallocate [the] scope" it just fires a DOM $destroy event (not the scope $destroy event). It doesn't actually touch the scope...
A few questions/remarks, overall I like the direction of this PR. 👍 |
c656d43
to
d56438e
Compare
d56438e
to
2d6a498
Compare
@petebacondarwin Sure, I'll have a look. It seems all my comments have been addressed so I'll take the last look and it'll most likely be ready to land. |
2d6a498
to
4a38c1a
Compare
…q data of all replaced nodes Closes: angular#12094
This removes the
skipDestroyOnNextJQueryCleanData
global while also fixing an edge case where some elements removed byreplaceWith
did not have child elements cleaned and could leak jq data/handlers. It's a bit of an edge case but would be reproduced if jq data ever got onto the child element of ang-if
orng-repeat
before the transcludereplaceWith
call (or similar situations withreplace:true
). This is captured in the added tests (3 previously failed). Also added a test ensuring thefirstElementToRemove
does not trigger the element $destroy.skipDestroyOnNextJQueryCleanData
is removed by doingjqLite(firstElementToRemove).off('$destroy')
before doingcleanData(fragment.qSA(*))
.Doing
cleanData(fragment.qSA(*))
avoidsjqLite(elements[1...n]).remove()
for the multi element case while also fixing the bug mentioned above by also fetching child elements. This makes use of the fragment meaning #12041 that avoided creating it would no longer work.JQLite.cleanData
is added so this code doesn't need JQLite vs jQuery specific logic.For the single node case this adds the
.qSA(*)
and may add.off('$destroy')
but all the benchmarks I tried showed no effect. ThereplaceWith
is just too small/infrequent when doing compile/link/digest.