diff --git a/src/ng/compile.js b/src/ng/compile.js index e8f582a385d8..2e252a8a62df 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -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); diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index b00fe80511b2..0ec85364c915 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -222,7 +222,19 @@ function $RootScopeProvider(){ } else { 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); + return child; + + function destroyChild() { + child.$$destroyed = true; + } }, /** diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index ddde992ab454..cc0203e104b8 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -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: '
' + }; + }); + $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('red.html
'); + var template = $compile( + '