fix($compile): Resolve leak with asynchronous compilation

Stop an asynchronous compilation when this is performed on an
already destroyed scope

Closes #9199
Closes #9079
Closes #8504
Closes #9197
This commit is contained in:
Lucas Galfaso
2014-09-22 13:08:25 +02:00
committed by Peter Bacon Darwin
parent cd2cfafcab
commit 6303c3dcf6
3 changed files with 67 additions and 1 deletions

View File

@@ -2011,6 +2011,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
boundTranscludeFn = linkQueue.shift(),
linkNode = $compileNode[0];
if (scope.$$destroyed) continue;
if (beforeTemplateLinkNode !== beforeTemplateCompileNode) {
var oldClasses = beforeTemplateLinkNode.className;
@@ -2037,6 +2039,7 @@ 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

@@ -223,6 +223,10 @@ function $RootScopeProvider(){
parent.$$childHead = parent.$$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);

View File

@@ -4537,6 +4537,65 @@ 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;
@@ -5117,7 +5176,7 @@ describe('$compile', function() {
expect(countScopes($rootScope)).toEqual(1);
}));
it('should destroy mark as destroyed all sub scopes of the scope being destroyed',
it('should mark as destroyed all sub scopes of the scope being destroyed',
inject(function($compile, $rootScope) {
element = $compile(