本文深入探讨JavaScript中DOM操作的性能瓶颈,指出DOM与ECMAScript交互的高昂代价。通过对比循环内直接修改DOM与字符串拼接后一次性更新的性能差异,强调减少DOM访问次数的重要性。同时分析HTMLCollection实时更新的特性,建议缓存length属性以提升遍历效率,并对querySelector与数组拷贝策略进行实测对比,提供实用的性能优化建议。
DOM与JavaScript的交互成本
DOM是用于操作XML和HTML文档的应用程序接口,通过脚本进行DOM操作的代价非常昂贵。一个形象的比喻是,将DOM和JavaScript(此处指ECMAScript)分别视为两个岛屿,它们之间由一座收费桥梁连接。每次ECMAScript访问DOM,都需要通过这座桥并支付“过桥费”。访问DOM的频率越高,产生的费用就越大。因此,最佳实践是尽可能减少过桥的次数,尽量让逻辑停留在ECMAScript一侧。虽然无法完全避免使用DOM接口,但我们可以探讨如何提高程序运行效率。
DOM访问与修改的性能陷阱
1、DOM访问与修改 访问DOM元素本身存在成本,而修改元素的代价更为高昂,因为这会触发浏览器重新计算页面的几何布局(即重排和重绘)。
最糟糕的情况是在循环中频繁访问或修改元素。参考以下两段代码对比:
var times = 15000;
// code1
console.time(1);
for(var i = 0; i < times; i++) {
document.getElementById('myDiv1').innerHTML += 'a';
}
console.timeEnd(1);
// code2
console.time(2);
var str = '';
for(var i = 0; i < times; i++) {
str += 'a';
}
document.getElementById('myDiv2').innerHTML = str;
console.timeEnd(2);
测试结果显示,第一次运行的耗时竟是第二次的千倍以上(测试环境为Chrome版本 44.0.2403.130 m)。
1: 2846.700ms
2: 1.046ms
第一段代码的问题在于,每次循环迭代都会访问元素两次:一次读取innerHTML的值,另一次重新写入。这意味着每次循环都在“过桥”(重排和重绘的具体机制将在后续章节讲解)。结果充分证明,DOM访问次数越多,代码执行速度越慢。因此,应尽可能减少DOM访问次数,尽量在ECMAScript端完成处理。
HTML集合遍历与长度缓存优化
2、HTML集合 & 遍历DOM 操作DOM的另一个性能消耗点在于遍历DOM。通常我们会获取一个HTML集合,例如使用getElementsByTagName()或document.links等方法,这并不陌生。收集结果是一个类似数组的集合,它处于“实时状态”,这意味着当底层文档对象发生变化时,该集合会自动更新。例如:
<body>
<ul id='fruit'>
<li> apple </li>
<li> orange </li>
<li> banana </li>
</ul>
</body>
<script type="text/javascript">
var lis = document.getElementsByTagName('li');
var peach = document.createElement('li');
peach.innerHTML = 'peach';
document.getElementById('fruit').appendChild(peach);
console.log(lis.length); // 4
</script>
这正是导致低效的原因。与优化数组操作类似,只需缓存length变量即可解决问题(读取集合的length比读取普通数组的length要慢得多,因为每次都需要查询):
console.time(0);
var lis0 = document.getElementsByTagName('li');
var str0 = '';
for(var i = 0; i < lis0.length; i++) {
str0 += lis0[i].innerHTML;
}
console.timeEnd(0);
console.time(1);
var lis1 = document.getElementsByTagName('li');
var str1 = '';
for(var i = 0, len = lis1.length; i < len; i++) {
str1 += lis1[i].innerHTML;
}
console.timeEnd(1);
我们来看看性能提升的效果如何?
0: 0.974ms
1: 0.664ms
当集合长度较大时(本例中为1000),性能提升效果显著。
《高性能JavaScript》提出了另一种优化策略,指出“由于遍历数组比遍历集合快,如果先将集合元素拷贝到数组中,访问其属性会更快”。然而经过测试,并未发现明显的性能优势,因此建议避免这种多此一举的操作。测试代码如下(如有不同见解欢迎交流):
console.time(1);
var lis1 = document.getElementsByTagName('li');
var str1 = '';
for(var i = 0, len = lis1.length; i < len; i++) {
str1 += lis1[i].innerHTML;
}
console.timeEnd(1);
console.time(2);
var lis2 = document.getElementsByTagName('li');
var a = [];
for(var i = 0, len = lis2.length; i < len; i++)
a[i] = lis2[i];
var str2 = '';
for(var i = 0, len = a.length; i < len; i++) {
str2 += a[i].innerHTML;
}
console.timeEnd(2);
querySelector系列方法的应用与局限
本节最后介绍两个原生DOM方法:querySelector()和querySelectorAll()。前者返回一个静态节点列表(注意,其返回值不像HTML集合那样动态变化),后者返回匹配的第一个元素。实际上,并非所有情况下它们的性能都优于传统的HTML集合遍历。
console.time(1);
var lis1 = document.getElementsByTagName('li');
console.timeEnd(1);
console.time(2);
var lis2 = document.querySelectorAll('li');
console.timeEnd(2);
// 1: 0.038ms
// 2: 3.957ms
但由于它们支持类似CSS的选择器语法,在进行组合查询时,效率会更高且使用更便捷。例如进行如下组合查询:
var elements = document.querySelectorAll('#menu a');
var elements = document.querySelectorAll('div.warning, div.notice');
以上就是关于高性能JavaScript DOM编程的全部内容,希望读者能够理解并从中受益。