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

refactor($scope): prevent multiple calls to listener on $destroy #9079

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/ng/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
Expand Down
12 changes: 12 additions & 0 deletions src/ng/rootScope.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
},

/**
Expand Down
96 changes: 86 additions & 10 deletions test/ng/compileSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -4971,17 +5030,17 @@ describe('$compile', function() {
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;
}
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() {
Expand Down Expand Up @@ -5117,6 +5176,23 @@ describe('$compile', function() {
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);
}
}));
});


Expand Down
7 changes: 7 additions & 0 deletions test/ng/rootScopeSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,13 @@ describe('Scope', function() {
expect(log).toBe('123');
}));

it('should broadcast the $destroy only once', inject(function($rootScope, log) {
var isolateScope = first.$new(true);
isolateScope.$on('$destroy', log.fn('event'));
first.$destroy();
isolateScope.$destroy();
expect(log).toEqual('event');
}));

it('should decrement ancestor $$listenerCount entries', inject(function($rootScope) {
var EVENT = 'fooEvent',
Expand Down