Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

refactor($compile): remove skipDestroyOnNextJQueryCleanData, fix data leak on some replaced nodes #12094

Closed
wants to merge 2 commits into from

Conversation

jbedard
Copy link
Contributor

@jbedard jbedard commented Jun 12, 2015

This removes the skipDestroyOnNextJQueryCleanData global while also fixing an edge case where some elements removed by replaceWith 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 a ng-if or ng-repeat before the transclude replaceWith call (or similar situations with replace:true). This is captured in the added tests (3 previously failed). Also added a test ensuring the firstElementToRemove does not trigger the element $destroy.

skipDestroyOnNextJQueryCleanData is removed by doing jqLite(firstElementToRemove).off('$destroy') before doing cleanData(fragment.qSA(*)).

Doing cleanData(fragment.qSA(*)) avoids jqLite(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. The replaceWith is just too small/infrequent when doing compile/link/digest.

@lgalfaso
Copy link
Contributor

with the patch, is the issue outlined at the comment about jQuery UI an issue?

@jbedard
Copy link
Contributor Author

jbedard commented Jun 12, 2015

I believe that's covered by this test added in b9389b2. As long as we always reference the current jqLite.cleanData it should be fine.

@@ -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() {
Copy link
Contributor Author

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?

Copy link
Member

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.

Copy link
Contributor Author

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...

Copy link
Member

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

Copy link
Contributor Author

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?

Copy link
Contributor Author

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).

@jbedard jbedard force-pushed the compile-replacewith-jqremove branch from 582e790 to bb808d1 Compare July 12, 2015 10:16
@jbedard
Copy link
Contributor Author

jbedard commented Jul 12, 2015

@mzgol, this is changing what you originally did for jQuery2. What do you think?

@mgol
Copy link
Member

mgol commented Jul 12, 2015

@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');
Copy link
Member

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.

Copy link
Contributor Author

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...

@mgol
Copy link
Member

mgol commented Jul 20, 2015

A few questions/remarks, overall I like the direction of this PR. 👍

@jbedard jbedard force-pushed the compile-replacewith-jqremove branch 2 times, most recently from c656d43 to d56438e Compare July 21, 2015 04:48
@jbedard jbedard force-pushed the compile-replacewith-jqremove branch from d56438e to 2d6a498 Compare August 9, 2015 01:58
@petebacondarwin petebacondarwin added this to the 1.5.x - migration-facilitation milestone Sep 7, 2015
@petebacondarwin
Copy link
Member

Moving to 1.5 - @mzgol & @jbedard can we try to combine this with #8695?

@mgol
Copy link
Member

mgol commented Sep 7, 2015

@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.

@jbedard jbedard force-pushed the compile-replacewith-jqremove branch from 2d6a498 to 4a38c1a Compare November 1, 2015 09:25
@lgalfaso lgalfaso closed this in 96288d0 Nov 1, 2015
gkalpak pushed a commit to gkalpak/angular.js that referenced this pull request Nov 23, 2015
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants