This repository has been archived by the owner on Apr 12, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 27.5k
TRANSCLUDE MEMORY LEAK: fix($compile): connect transclude scopes to their containing scope to prevent memory leaks #9281
Closed
Closed
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
d58ae61
feat(Scope): allow the parent of a new scope to be specified
petebacondarwin 43a3b4e
fix($compile): connect transclude scopes to their containing scope to…
petebacondarwin ac39095
test($compile): add comments to clarify what scopes we expect
petebacondarwin 594c00f
test($compile): add extra multi-element transclude test
petebacondarwin 53bc475
style($compile): remove unused variable declaration
petebacondarwin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4331,17 +4331,21 @@ describe('$compile', function() { | |
return { | ||
transclude: 'content', | ||
replace: true, | ||
scope: true, | ||
template: '<ul><li>W:{{$parent.$id}}-{{$id}};</li><li ng-transclude></li></ul>' | ||
scope: {}, | ||
link: function(scope) { | ||
scope.x='iso'; | ||
}, | ||
template: '<ul><li>W:{{x}}-{{$parent.$id}}-{{$id}};</li><li ng-transclude></li></ul>' | ||
}; | ||
}); | ||
}); | ||
inject(function(log, $rootScope, $compile) { | ||
element = $compile('<div><div trans>T:{{$parent.$id}}-{{$id}}<span>;</span></div></div>') | ||
element = $compile('<div><div trans>T:{{x}}-{{$parent.$id}}-{{$id}}<span>;</span></div></div>') | ||
($rootScope); | ||
$rootScope.x = 'root'; | ||
$rootScope.$apply(); | ||
expect(element.text()).toEqual('W:1-2;T:1-3;'); | ||
expect(jqLite(element.find('span')[0]).text()).toEqual('T:1-3'); | ||
expect(element.text()).toEqual('W:iso-1-2;T:root-2-3;'); | ||
expect(jqLite(element.find('span')[0]).text()).toEqual('T:root-2-3'); | ||
expect(jqLite(element.find('span')[1]).text()).toEqual(';'); | ||
}); | ||
}); | ||
|
@@ -4551,47 +4555,6 @@ describe('$compile', function() { | |
} | ||
|
||
|
||
it('should remove transclusion scope, when the DOM is destroyed', function() { | ||
module(function() { | ||
directive('box', valueFn({ | ||
transclude: true, | ||
scope: { name: '=', show: '=' }, | ||
template: '<div><h1>Hello: {{name}}!</h1><div ng-transclude></div></div>', | ||
link: function(scope, element) { | ||
scope.$watch( | ||
'show', | ||
function(show) { | ||
if (!show) { | ||
element.find('div').find('div').remove(); | ||
} | ||
} | ||
); | ||
} | ||
})); | ||
}); | ||
inject(function($compile, $rootScope) { | ||
$rootScope.username = 'Misko'; | ||
$rootScope.select = true; | ||
element = $compile( | ||
'<div><div box name="username" show="select">user: {{username}}</div></div>') | ||
($rootScope); | ||
$rootScope.$apply(); | ||
expect(element.text()).toEqual('Hello: Misko!user: Misko'); | ||
|
||
var widgetScope = $rootScope.$$childHead; | ||
var transcludeScope = widgetScope.$$nextSibling; | ||
expect(widgetScope.name).toEqual('Misko'); | ||
expect(widgetScope.$parent).toEqual($rootScope); | ||
expect(transcludeScope.$parent).toEqual($rootScope); | ||
|
||
$rootScope.select = false; | ||
$rootScope.$apply(); | ||
expect(element.text()).toEqual('Hello: Misko!'); | ||
expect(widgetScope.$$nextSibling).toEqual(null); | ||
}); | ||
}); | ||
|
||
|
||
it('should add a $$transcluded property onto the transcluded scope', function() { | ||
module(function() { | ||
directive('trans', function() { | ||
|
@@ -4964,6 +4927,162 @@ describe('$compile', function() { | |
}); | ||
|
||
|
||
// see issue https://github.com/angular/angular.js/issues/9095 | ||
describe('removing a transcluded element', function() { | ||
|
||
function countScopes($rootScope) { | ||
return [$rootScope].concat( | ||
getChildScopes($rootScope) | ||
).length; | ||
|
||
function getChildScopes(scope) { | ||
var children = []; | ||
if (!scope.$$childHead) { return children; } | ||
var childScope = scope.$$childHead; | ||
do { | ||
children.push(childScope); | ||
children = children.concat(getChildScopes(childScope)); | ||
} while ((childScope = childScope.$$nextSibling)); | ||
return children; | ||
} | ||
} | ||
|
||
beforeEach(module(function() { | ||
directive('toggle', function() { | ||
return { | ||
transclude: true, | ||
template: '<div ng:if="t"><div ng:transclude></div></div>' | ||
}; | ||
}); | ||
})); | ||
|
||
|
||
it('should not leak the transclude scope when the transcluded content is an element transclusion directive', | ||
inject(function($compile, $rootScope) { | ||
|
||
element = $compile( | ||
'<div toggle>' + | ||
'<div ng:repeat="msg in [\'msg-1\']">{{ msg }}</div>' + | ||
'</div>' | ||
)($rootScope); | ||
|
||
$rootScope.$apply('t = true'); | ||
expect(element.text()).toContain('msg-1'); | ||
// Expected scopes: $rootScope, ngIf, transclusion, ngRepeat | ||
expect(countScopes($rootScope)).toEqual(4); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it might be good to clarify that the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (same for other similar assertions) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea! |
||
|
||
$rootScope.$apply('t = false'); | ||
expect(element.text()).not.toContain('msg-1'); | ||
// Expected scopes: $rootScope | ||
expect(countScopes($rootScope)).toEqual(1); | ||
|
||
$rootScope.$apply('t = true'); | ||
expect(element.text()).toContain('msg-1'); | ||
// Expected scopes: $rootScope, ngIf, transclusion, ngRepeat | ||
expect(countScopes($rootScope)).toEqual(4); | ||
|
||
$rootScope.$apply('t = false'); | ||
expect(element.text()).not.toContain('msg-1'); | ||
// Expected scopes: $rootScope | ||
expect(countScopes($rootScope)).toEqual(1); | ||
})); | ||
|
||
|
||
it('should not leak the transclude scope when the transcluded content is an multi-element transclusion directive', | ||
inject(function($compile, $rootScope) { | ||
|
||
element = $compile( | ||
'<div toggle>' + | ||
'<div ng:repeat-start="msg in [\'msg-1\']">{{ msg }}</div>' + | ||
'<div ng:repeat-end>{{ msg }}</div>' + | ||
'</div>' | ||
)($rootScope); | ||
|
||
$rootScope.$apply('t = true'); | ||
expect(element.text()).toContain('msg-1msg-1'); | ||
// Expected scopes: $rootScope, ngIf, transclusion, ngRepeat | ||
expect(countScopes($rootScope)).toEqual(4); | ||
|
||
$rootScope.$apply('t = false'); | ||
expect(element.text()).not.toContain('msg-1msg-1'); | ||
// Expected scopes: $rootScope | ||
expect(countScopes($rootScope)).toEqual(1); | ||
|
||
$rootScope.$apply('t = true'); | ||
expect(element.text()).toContain('msg-1msg-1'); | ||
// Expected scopes: $rootScope, ngIf, transclusion, ngRepeat | ||
expect(countScopes($rootScope)).toEqual(4); | ||
|
||
$rootScope.$apply('t = false'); | ||
expect(element.text()).not.toContain('msg-1msg-1'); | ||
// Expected scopes: $rootScope | ||
expect(countScopes($rootScope)).toEqual(1); | ||
})); | ||
|
||
|
||
it('should not leak the transclude scope if the transcluded contains only comments', | ||
inject(function($compile, $rootScope) { | ||
|
||
element = $compile( | ||
'<div toggle>' + | ||
'<!-- some comment -->' + | ||
'</div>' | ||
)($rootScope); | ||
|
||
$rootScope.$apply('t = true'); | ||
expect(element.html()).toContain('some comment'); | ||
// Expected scopes: $rootScope, ngIf, transclusion | ||
expect(countScopes($rootScope)).toEqual(3); | ||
|
||
$rootScope.$apply('t = false'); | ||
expect(element.html()).not.toContain('some comment'); | ||
// Expected scopes: $rootScope | ||
expect(countScopes($rootScope)).toEqual(1); | ||
|
||
$rootScope.$apply('t = true'); | ||
expect(element.html()).toContain('some comment'); | ||
// Expected scopes: $rootScope, ngIf, transclusion | ||
expect(countScopes($rootScope)).toEqual(3); | ||
|
||
$rootScope.$apply('t = false'); | ||
expect(element.html()).not.toContain('some comment'); | ||
// Expected scopes: $rootScope | ||
expect(countScopes($rootScope)).toEqual(1); | ||
})); | ||
|
||
it('should not leak the transclude scope if the transcluded contains only text nodes', | ||
inject(function($compile, $rootScope) { | ||
|
||
element = $compile( | ||
'<div toggle>' + | ||
'some text' + | ||
'</div>' | ||
)($rootScope); | ||
|
||
$rootScope.$apply('t = true'); | ||
expect(element.html()).toContain('some text'); | ||
// Expected scopes: $rootScope, ngIf, transclusion | ||
expect(countScopes($rootScope)).toEqual(3); | ||
|
||
$rootScope.$apply('t = false'); | ||
expect(element.html()).not.toContain('some text'); | ||
// Expected scopes: $rootScope | ||
expect(countScopes($rootScope)).toEqual(1); | ||
|
||
$rootScope.$apply('t = true'); | ||
expect(element.html()).toContain('some text'); | ||
// Expected scopes: $rootScope, ngIf, transclusion | ||
expect(countScopes($rootScope)).toEqual(3); | ||
|
||
$rootScope.$apply('t = false'); | ||
expect(element.html()).not.toContain('some text'); | ||
// Expected scopes: $rootScope | ||
expect(countScopes($rootScope)).toEqual(1); | ||
})); | ||
|
||
}); | ||
|
||
|
||
describe('nested transcludes', function() { | ||
|
||
beforeEach(module(function($compileProvider) { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 see that the breaking change is documented in the second commit --- people are going to be mad about this landing in a release candidate, but I think you're right, the solution here is better
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 believe that for the vast majority of users they won't even notice.
Those that it does affect were writing dubious code anyway, since they should not have been manipulating the DOM without worrying about the scope.
And even in those cases, above, the apps will not "break" they will just leak memory, and as soon as the containing scope is destroyed the memory will be recovered. If they get a memory leak, they will file an issue and we can provide them with the directions to fix it.
Also, while this is a breaking change, it is a memory leak bug fix and not a new feature. So I think that is reasonable even so close to release.
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 am in agreement with everything you say, just noting that it's going to upset people =) I definitely think it's worth it to improve well-behaved apps