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

Commit 3410f65

Browse files
mgolIgorMinar
authored andcommitted
perf(jqLite): implement and use the empty method in place of html(‘’)
jQuery's elem.html('') is way slower than elem.empty(). As clearing element contents happens quite often in certain scenarios, switching to using .empty() provides a significant performance boost when using Angular with jQuery. Closes #4457
1 parent f3de5b6 commit 3410f65

File tree

17 files changed

+64
-30
lines changed

17 files changed

+64
-30
lines changed

docs/component-spec/annotationsSpec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ describe('Docs Annotations', function() {
55
var body;
66
beforeEach(function() {
77
body = angular.element(document.body);
8-
body.html('');
8+
body.empty();
99
});
1010

1111
var normalizeHtml = function(html) {

docs/components/angular-bootstrap/bootstrap-prettify.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function escape(text) {
2828
function setHtmlIe8SafeWay(element, html) {
2929
var newElement = angular.element('<pre>' + html + '</pre>');
3030

31-
element.html('');
31+
element.empty();
3232
element.append(newElement.contents());
3333
return element;
3434
}

docs/content/guide/dev_guide.unit-testing.ngdoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ var pc = new PasswordCtrl();
222222
input.val('abc');
223223
pc.grade();
224224
expect(span.text()).toEqual('weak');
225-
$('body').html('');
225+
$('body').empty();
226226
</pre>
227227

228228
In angular the controllers are strictly separated from the DOM manipulation logic and this results in

src/Angular.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -974,7 +974,7 @@ function startingTag(element) {
974974
try {
975975
// turns out IE does not let you set .html() on elements which
976976
// are not allowed to have children. So we just ignore it.
977-
element.html('');
977+
element.empty();
978978
} catch(e) {}
979979
// As Per DOM Standards
980980
var TEXT_NODE = 3;

src/jqLite.js

+18-4
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
* - [`contents()`](http://api.jquery.com/contents/)
4747
* - [`css()`](http://api.jquery.com/css/)
4848
* - [`data()`](http://api.jquery.com/data/)
49+
* - [`empty()`](http://api.jquery.com/empty/)
4950
* - [`eq()`](http://api.jquery.com/eq/)
5051
* - [`find()`](http://api.jquery.com/find/) - Limited to lookups by tag name
5152
* - [`hasClass()`](http://api.jquery.com/hasClass/)
@@ -358,6 +359,15 @@ function jqLiteInheritedData(element, name, value) {
358359
}
359360
}
360361

362+
function jqLiteEmpty(element) {
363+
for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) {
364+
jqLiteDealoc(childNodes[i]);
365+
}
366+
while (element.firstChild) {
367+
element.removeChild(element.firstChild);
368+
}
369+
}
370+
361371
//////////////////////////////////////////
362372
// Functions which are declared directly.
363373
//////////////////////////////////////////
@@ -552,7 +562,9 @@ forEach({
552562
jqLiteDealoc(childNodes[i]);
553563
}
554564
element.innerHTML = value;
555-
}
565+
},
566+
567+
empty: jqLiteEmpty
556568
}, function(fn, name){
557569
/**
558570
* Properties: writes return selection, reads return first value
@@ -562,11 +574,13 @@ forEach({
562574

563575
// jqLiteHasClass has only two arguments, but is a getter-only fn, so we need to special-case it
564576
// in a way that survives minification.
565-
if (((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2) === undefined) {
577+
// jqLiteEmpty takes no arguments but is a setter.
578+
if (fn !== jqLiteEmpty &&
579+
(((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2) === undefined)) {
566580
if (isObject(arg1)) {
567581

568582
// we are a write, but the object properties are the key/values
569-
for(i=0; i < this.length; i++) {
583+
for (i = 0; i < this.length; i++) {
570584
if (fn === jqLiteData) {
571585
// data() takes the whole object in jQuery
572586
fn(this[i], arg1);
@@ -591,7 +605,7 @@ forEach({
591605
}
592606
} else {
593607
// we are a write, so apply to all children
594-
for(i=0; i < this.length; i++) {
608+
for (i = 0; i < this.length; i++) {
595609
fn(this[i], arg1, arg2);
596610
}
597611
// return self for chaining

src/ng/compile.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1219,7 +1219,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
12191219
});
12201220
} else {
12211221
$template = jqLite(jqLiteClone(compileNode)).contents();
1222-
$compileNode.html(''); // clear contents
1222+
$compileNode.empty(); // clear contents
12231223
childTranscludeFn = compile($template, transcludeFn);
12241224
}
12251225
}
@@ -1651,7 +1651,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
16511651
? origAsyncDirective.templateUrl($compileNode, tAttrs)
16521652
: origAsyncDirective.templateUrl;
16531653

1654-
$compileNode.html('');
1654+
$compileNode.empty();
16551655

16561656
$http.get($sce.getTrustedResourceUrl(templateUrl), {cache: $templateCache}).
16571657
success(function(content) {

src/ng/directive/ngTransclude.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ var ngTranscludeDirective = ngDirective({
6969

7070
link: function($scope, $element, $attrs, controller) {
7171
controller.$transclude(function(clone) {
72-
$element.html('');
72+
$element.empty();
7373
$element.append(clone);
7474
});
7575
}

src/ng/directive/select.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -333,13 +333,13 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
333333
// becomes the compilation root
334334
nullOption.removeClass('ng-scope');
335335

336-
// we need to remove it before calling selectElement.html('') because otherwise IE will
336+
// we need to remove it before calling selectElement.empty() because otherwise IE will
337337
// remove the label from the element. wtf?
338338
nullOption.remove();
339339
}
340340

341341
// clear contents, we'll add what's needed based on the model
342-
selectElement.html('');
342+
selectElement.empty();
343343

344344
selectElement.on('change', function() {
345345
scope.$apply(function() {

src/ngAnimate/animate.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1237,7 +1237,7 @@ angular.module('ngAnimate', ['ng'])
12371237
//make the element super hidden and override any CSS style values
12381238
clone.attr('style','position:absolute; top:-9999px; left:-9999px');
12391239
clone.removeAttr('id');
1240-
clone.html('');
1240+
clone.empty();
12411241

12421242
forEach(oldClasses.split(' '), function(klass) {
12431243
clone.removeClass(klass);

test/helpers/testabilityPatch.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ beforeEach(function() {
2929
bindJQuery();
3030
}
3131

32-
33-
angular.element(document.body).html('').removeData();
32+
angular.element(document.body).empty().removeData();
3433
});
3534

3635
afterEach(function() {

test/jqLiteSpec.js

+23-2
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ describe('jqLite', function() {
322322
});
323323

324324

325-
it('should emit $destroy event if an element is removed via html()', inject(function(log) {
325+
it('should emit $destroy event if an element is removed via html(\'\')', inject(function(log) {
326326
var element = jqLite('<div><span>x</span></div>');
327327
element.find('span').on('$destroy', log.fn('destroyed'));
328328

@@ -333,6 +333,17 @@ describe('jqLite', function() {
333333
}));
334334

335335

336+
it('should emit $destroy event if an element is removed via empty()', inject(function(log) {
337+
var element = jqLite('<div><span>x</span></div>');
338+
element.find('span').on('$destroy', log.fn('destroyed'));
339+
340+
element.empty();
341+
342+
expect(element.html()).toBe('');
343+
expect(log).toEqual('destroyed');
344+
}));
345+
346+
336347
it('should retrieve all data if called without params', function() {
337348
var element = jqLite(a);
338349
expect(element.data()).toEqual({});
@@ -786,7 +797,7 @@ describe('jqLite', function() {
786797
});
787798

788799

789-
it('should read/write value', function() {
800+
it('should read/write a value', function() {
790801
var element = jqLite('<div>abc</div>');
791802
expect(element.length).toEqual(1);
792803
expect(element[0].innerHTML).toEqual('abc');
@@ -797,6 +808,16 @@ describe('jqLite', function() {
797808
});
798809

799810

811+
describe('empty', function() {
812+
it('should write a value', function() {
813+
var element = jqLite('<div>abc</div>');
814+
expect(element.length).toEqual(1);
815+
expect(element.empty() == element).toBeTruthy();
816+
expect(element.html()).toEqual('');
817+
});
818+
});
819+
820+
800821
describe('on', function() {
801822
it('should bind to window on hashchange', function() {
802823
if (jqLite.fn) return; // don't run in jQuery

test/ng/compileSpec.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -170,26 +170,26 @@ describe('$compile', function() {
170170
// First with only elements at the top level
171171
element = jqLite('<div><div></div></div>');
172172
$compile(element.contents())($rootScope);
173-
element.html('');
173+
element.empty();
174174
expect(calcCacheSize()).toEqual(0);
175175

176176
// Next with non-empty text nodes at the top level
177177
// (in this case the compiler will wrap them in a <span>)
178178
element = jqLite('<div>xxx</div>');
179179
$compile(element.contents())($rootScope);
180-
element.html('');
180+
element.empty();
181181
expect(calcCacheSize()).toEqual(0);
182182

183183
// Next with comment nodes at the top level
184184
element = jqLite('<div><!-- comment --></div>');
185185
$compile(element.contents())($rootScope);
186-
element.html('');
186+
element.empty();
187187
expect(calcCacheSize()).toEqual(0);
188188

189189
// Finally with empty text nodes at the top level
190190
element = jqLite('<div> \n<div></div> </div>');
191191
$compile(element.contents())($rootScope);
192-
element.html('');
192+
element.empty();
193193
expect(calcCacheSize()).toEqual(0);
194194
});
195195

test/ng/directive/formSpec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ describe('form', function() {
216216
// yes, I know, scope methods should not do direct DOM manipulation, but I wanted to keep
217217
// this test small. Imagine that the destroy action will cause a model change (e.g.
218218
// $location change) that will cause some directive to destroy the dom (e.g. ngView+$route)
219-
doc.html('');
219+
doc.empty();
220220
destroyed = true;
221221
}
222222

test/ng/directive/ngIncludeSpec.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ describe('ngInclude', function() {
4747
$rootScope.url = 'myUrl';
4848
$rootScope.$digest();
4949
expect(body.text()).toEqual('misko');
50-
body.html('');
50+
body.empty();
5151
}));
5252

5353

@@ -60,7 +60,7 @@ describe('ngInclude', function() {
6060
$rootScope.url = 'myUrl';
6161
$rootScope.$digest();
6262
expect(element.text()).toEqual('Alibaba');
63-
jqLite(document.body).html('');
63+
jqLite(document.body).empty();
6464
}));
6565

6666

@@ -74,7 +74,7 @@ describe('ngInclude', function() {
7474
expect(function() { $rootScope.$digest(); }).toThrowMinErr(
7575
'$sce', 'insecurl',
7676
/Blocked loading resource from url not allowed by \$sceDelegate policy. URL: http:\/\/example.com\/myUrl.*/);
77-
jqLite(document.body).html('');
77+
jqLite(document.body).empty();
7878
}));
7979

8080

@@ -88,7 +88,7 @@ describe('ngInclude', function() {
8888
expect(function() { $rootScope.$digest(); }).toThrowMinErr(
8989
'$sce', 'insecurl',
9090
/Blocked loading resource from url not allowed by \$sceDelegate policy. URL: http:\/\/example.com\/myUrl.*/);
91-
jqLite(document.body).html('');
91+
jqLite(document.body).empty();
9292
}));
9393

9494

test/ngAnimate/animateSpec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ describe("ngAnimate", function() {
301301
inject(function($animate, $compile, $rootScope, $timeout, $sniffer) {
302302

303303
$rootScope.$digest();
304-
element.html('');
304+
element.empty();
305305

306306
var child1 = $compile('<div>1</div>')($rootScope);
307307
var child2 = $compile('<div>2</div>')($rootScope);

test/ngScenario/dslSpec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ describe("angular.scenario.dsl", function() {
5353
// Just use the real one since it delegates to this.addFuture
5454
$root.addFutureAction = angular.scenario.
5555
SpecRunner.prototype.addFutureAction;
56-
jqLite($window.document).html('');
56+
jqLite($window.document).empty();
5757
}));
5858

5959
afterEach(function(){

test/ngTouch/directive/ngClickSpec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ describe('ngClick (touch)', function() {
152152
}));
153153

154154
afterEach(inject(function($document) {
155-
$document.find('body').html('');
155+
$document.find('body').empty();
156156
}));
157157

158158

0 commit comments

Comments
 (0)