revert: fix($compile): Resolve leak with asynchronous compilation

This reverts commit 5c9c197305.

This "fix" is not yet ready for 1.2.x as it has lots of issues when JQuery
is loaded.
This commit is contained in:
Peter Bacon Darwin
2014-09-29 22:16:11 +01:00
parent 5036ac905b
commit 1f182853d4
3 changed files with 6 additions and 336 deletions

View File

@@ -976,11 +976,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) {
var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, containingScope) {
var boundTranscludeFn = function(transcludedScope, cloneFn, controllers) {
var scopeCreated = false;
if (!transcludedScope) {
transcludedScope = scope.$new(false, containingScope);
transcludedScope = scope.$new();
transcludedScope.$$transcluded = true;
scopeCreated = true;
}
@@ -1592,7 +1592,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
transcludeControllers = elementControllers;
}
return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, scopeToChild);
return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers);
}
}
}
@@ -1754,8 +1754,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
boundTranscludeFn = linkQueue.shift(),
linkNode = $compileNode[0];
if (scope.$$destroyed) continue;
if (beforeTemplateLinkNode !== beforeTemplateCompileNode) {
var oldClasses = beforeTemplateLinkNode.className;
@@ -1786,7 +1784,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) {
var childBoundTranscludeFn = boundTranscludeFn;
if (scope.$$destroyed) return;
if (linkQueue) {
linkQueue.push(scope);
linkQueue.push(node);

View File

@@ -182,17 +182,12 @@ function $RootScopeProvider(){
* When creating widgets, it is useful for the widget to not accidentally read parent
* state.
*
* @param {Scope} [parent=this] The {@link ng.$rootScope.Scope `Scope`} that will contain this
* the newly created scope. Defaults to `this` scope if not provided.
* This is used to ensure that $destroy events are handled correctly.
*
* @returns {Object} The newly created child scope.
*
*/
$new: function(isolate, parent) {
var child;
parent = parent || this;
$new: function(isolate) {
var ChildScope,
child;
if (isolate) {
child = new Scope();
@@ -225,19 +220,6 @@ function $RootScopeProvider(){
} else {
this.$$childHead = this.$$childTail = child;
}
// When the new scope is not isolated or we inherit from `this`, and
// the parent scope is destroyed, the property `$$destroyed` is inherited
// prototypically. In all other cases, this property needs to be set
// when the parent scope is destroyed.
// The listener needs to be added after the parent is set
if (isolate || parent != this) child.$on('$destroy', destroyChild);
function destroyChild() {
child.$$destroyed = true;
}
return child;
},

View File

@@ -3740,119 +3740,6 @@ describe('$compile', function() {
});
});
it('should not leak when continuing the compilation of elements on a scope that was destroyed', function() {
if (jQuery) {
// jQuery 2.x doesn't expose the cache storage.
return;
}
var linkFn = jasmine.createSpy('linkFn');
module(function($controllerProvider, $compileProvider) {
$controllerProvider.register('Leak', function ($scope, $timeout) {
$scope.code = 'red';
$timeout(function () {
$scope.code = 'blue';
});
});
$compileProvider.directive('isolateRed', function() {
return {
restrict: 'A',
scope: {},
template: '<div red></div>'
};
});
$compileProvider.directive('red', function() {
return {
restrict: 'A',
templateUrl: 'red.html',
scope: {},
link: linkFn
};
});
});
inject(function($compile, $rootScope, $httpBackend, $timeout, $templateCache) {
$httpBackend.whenGET('red.html').respond('<p>red.html</p>');
var template = $compile(
'<div ng-controller="Leak">' +
'<div ng-switch="code">' +
'<div ng-switch-when="red">' +
'<div isolate-red></div>' +
'</div>' +
'</div>' +
'</div>');
element = template($rootScope);
$rootScope.$digest();
$timeout.flush();
$httpBackend.flush();
expect(linkFn).not.toHaveBeenCalled();
expect(jqLiteCacheSize()).toEqual(2);
$templateCache.removeAll();
var destroyedScope = $rootScope.$new();
destroyedScope.$destroy();
var clone = template(destroyedScope);
$rootScope.$digest();
$timeout.flush();
expect(linkFn).not.toHaveBeenCalled();
});
});
if (jQuery) {
describe('cleaning up after a replaced element', function () {
var $compile, xs;
beforeEach(inject(function (_$compile_) {
$compile = _$compile_;
xs = [0, 1];
}));
function testCleanup() {
var privateData, firstRepeatedElem;
element = $compile('<div><div ng-repeat="x in xs" ng-click="noop()">{{x}}</div></div>')($rootScope);
$rootScope.$apply('xs = [' + xs + ']');
firstRepeatedElem = element.children('.ng-scope').eq(0);
expect(firstRepeatedElem.data('$scope')).toBeDefined();
privateData = jQuery._data(firstRepeatedElem[0]);
expect(privateData.events).toBeDefined();
expect(privateData.events.click).toBeDefined();
expect(privateData.events.click[0]).toBeDefined();
$rootScope.$apply('xs = null');
expect(firstRepeatedElem.data('$scope')).not.toBeDefined();
privateData = jQuery._data(firstRepeatedElem[0]);
expect(privateData && privateData.events).not.toBeDefined();
}
it('should work without external libraries (except jQuery)', testCleanup);
it('should work with another library patching jQuery.cleanData after Angular', function () {
var cleanedCount = 0;
var currentCleanData = jQuery.cleanData;
jQuery.cleanData = function (elems) {
cleanedCount += elems.length;
// Don't return the output and expicitly pass only the first parameter
// so that we're sure we're not relying on either of them. jQuery UI patch
// behaves in this way.
currentCleanData(elems);
};
testCleanup();
// The initial ng-repeat div is dumped after parsing hence we expect cleanData
// count to be one larger than size of the iterated array.
expect(cleanedCount).toBe(xs.length + 1);
// Restore the previous jQuery.cleanData.
jQuery.cleanData = currentCleanData;
});
});
}
it('should add a $$transcluded property onto the transcluded scope', function() {
module(function() {
@@ -4210,179 +4097,6 @@ 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);
$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);
}));
it('should mark as destroyed all sub scopes of the scope being destroyed',
inject(function($compile, $rootScope) {
element = $compile(
'<div toggle>' +
'<div ng:repeat="msg in [\'msg-1\']">{{ msg }}</div>' +
'</div>'
)($rootScope);
$rootScope.$apply('t = true');
var childScopes = getChildScopes($rootScope);
$rootScope.$apply('t = false');
for (var i = 0; i < childScopes.length; ++i) {
expect(childScopes[i].$$destroyed).toBe(true);
}
}));
});
describe('nested transcludes', function() {
beforeEach(module(function($compileProvider) {
@@ -4451,29 +4165,6 @@ describe('$compile', function() {
$rootScope.$digest();
expect(element.text()).toEqual('transcluded content');
}));
it('should not leak memory with nested transclusion', function() {
inject(function($compile, $rootScope) {
var size;
expect(jqLiteCacheSize()).toEqual(0);
element = jqLite('<div><ul><li ng-repeat="n in nums">{{n}} => <i ng-if="0 === n%2">Even</i><i ng-if="1 === n%2">Odd</i></li></ul></div>');
$compile(element)($rootScope.$new());
$rootScope.nums = [0,1,2];
$rootScope.$apply();
size = jqLiteCacheSize();
$rootScope.nums = [3,4,5];
$rootScope.$apply();
expect(jqLiteCacheSize()).toEqual(size);
element.remove();
expect(jqLiteCacheSize()).toEqual(0);
});
});
});