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

Commit 791804b

Browse files
fix(compile): should not leak memory when there are top level empty text nodes
The change to prevent <span> elements being wrapped around empty text nodes caused these empty text nodes to have scopes and controllers attached, through jqLite.data() calls, which led to memory leaks and errors in IE8. Now we exclude all but document nodes and elements from having jqLite.data() set both in the compiler and in ng-view. Fixes: #1968 and #1876
1 parent 7eafbb9 commit 791804b

File tree

4 files changed

+75
-4
lines changed

4 files changed

+75
-4
lines changed

src/ng/compile.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,14 @@ function $CompileProvider($provide) {
330330
var $linkNode = cloneConnectFn
331331
? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!!
332332
: $compileNodes;
333-
$linkNode.data('$scope', scope);
333+
334+
// Attach scope only to non-text nodes.
335+
for(var i = 0, ii = $linkNode.length; i<ii; i++) {
336+
var node = $linkNode[i];
337+
if (node.nodeType == 1 /* element */ || node.nodeType == 9 /* document */) {
338+
$linkNode.eq(i).data('$scope', scope);
339+
}
340+
}
334341
safeAddClass($linkNode, 'ng-scope');
335342
if (cloneConnectFn) cloneConnectFn($linkNode, scope);
336343
if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode);

src/ng/directive/ngView.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$c
147147
if (current.controller) {
148148
locals.$scope = lastScope;
149149
controller = $controller(current.controller, locals);
150-
element.contents().data('$ngControllerController', controller);
150+
element.children().data('$ngControllerController', controller);
151151
}
152152

153153
link(lastScope);

test/ng/compileSpec.js

+41-1
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,17 @@ describe('$compile', function() {
116116

117117
describe('compile phase', function() {
118118

119+
it('should attach scope to the document node when it is compiled explicitly', inject(function($document){
120+
$compile($document)($rootScope);
121+
expect($document.scope()).toBe($rootScope);
122+
}));
123+
119124
it('should wrap root text nodes in spans', inject(function($compile, $rootScope) {
120125
element = jqLite('<div>A&lt;a&gt;B&lt;/a&gt;C</div>');
121126
var text = element.contents();
122127
expect(text[0].nodeName).toEqual('#text');
123128
text = $compile(text)($rootScope);
124-
expect(lowercase(text[0].nodeName)).toEqual('span');
129+
expect(text[0].nodeName).toEqual('SPAN');
125130
expect(element.find('span').text()).toEqual('A<a>B</a>C');
126131
}));
127132

@@ -137,6 +142,41 @@ describe('$compile', function() {
137142
expect(spans.text().indexOf('C')).toEqual(0);
138143
});
139144

145+
it('should not leak memory when there are top level empty text nodes', function() {
146+
var calcCacheSize = function() {
147+
var size = 0;
148+
forEach(jqLite.cache, function(item, key) { size++; });
149+
return size;
150+
};
151+
152+
// We compile the contents of element (i.e. not element itself)
153+
// Then delete these contents and check the cache has been reset to zero
154+
155+
// First with only elements at the top level
156+
element = jqLite('<div><div></div></div>');
157+
$compile(element.contents())($rootScope);
158+
element.html('');
159+
expect(calcCacheSize()).toEqual(0);
160+
161+
// Next with non-empty text nodes at the top level
162+
// (in this case the compiler will wrap them in a <span>)
163+
element = jqLite('<div>xxx</div>');
164+
$compile(element.contents())($rootScope);
165+
element.html('');
166+
expect(calcCacheSize()).toEqual(0);
167+
168+
// Next with comment nodes at the top level
169+
element = jqLite('<div><!-- comment --></div>');
170+
$compile(element.contents())($rootScope);
171+
element.html('');
172+
expect(calcCacheSize()).toEqual(0);
173+
174+
// Finally with empty text nodes at the top level
175+
element = jqLite('<div> \n<div></div> </div>');
176+
$compile(element.contents())($rootScope);
177+
element.html('');
178+
expect(calcCacheSize()).toEqual(0);
179+
});
140180

141181
describe('multiple directives per element', function() {
142182
it('should allow multiple directives per element', inject(function($compile, $rootScope, log){

test/ng/directive/ngViewSpec.js

+25-1
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ describe('ngView', function() {
429429
$rootScope.$digest();
430430
expect($rootScope.load).toHaveBeenCalledOnce();
431431
});
432-
})
432+
});
433433

434434

435435
it('should set $scope and $controllerController on the view', function() {
@@ -459,4 +459,28 @@ describe('ngView', function() {
459459
expect(div.controller()).toBe($route.current.scope.ctrl);
460460
});
461461
});
462+
463+
it('should not set $scope or $controllerController on top level text elements in the view', function() {
464+
function MyCtrl($scope) {}
465+
466+
module(function($routeProvider) {
467+
$routeProvider.when('/foo', {templateUrl: 'tpl.html', controller: MyCtrl});
468+
});
469+
470+
inject(function($templateCache, $location, $rootScope, $route) {
471+
$templateCache.put('tpl.html', '<div></div> ');
472+
$location.url('/foo');
473+
$rootScope.$digest();
474+
475+
forEach(element.contents(), function(node) {
476+
if ( node.nodeType == 3 ) {
477+
expect(jqLite(node).scope()).not.toBe($route.current.scope);
478+
expect(jqLite(node).controller()).not.toBeDefined();
479+
} else {
480+
expect(jqLite(node).scope()).toBe($route.current.scope);
481+
expect(jqLite(node).controller()).toBeDefined();
482+
}
483+
});
484+
});
485+
});
462486
});

0 commit comments

Comments
 (0)