本文深入解析 JavaScript 代码性能优化的核心策略,涵盖 DOM 交互最小化、文档碎片与 innerHTML 的使用、事件委托机制、作用域链查找优化、循环迭代技巧以及算法复杂度分析。通过具体代码对比,帮助开发者掌握提升页面渲染速度与执行效率的关键技术。
引言
在上一篇关于编写可维护性代码的文章之后,本文旨在总结代码性能优化的关键要点。此前因需补全过往未总结的内容而稍有耽搁,但坚持每日更新以巩固记忆的理念不变。对于初学者而言,定期总结所学知识至关重要,这不仅能加深印象,更能将可维护性与性能优化的意识内化为习惯,从而显著提升开发能力。接下来,我们将正式探讨如何提升 JavaScript 代码的性能。
优化 DOM 交互
1. 优化 DOM 交互
DOM(文档对象模型)与页面渲染紧密相关,浏览器通过解析和渲染 DOM 元素来构建页面。由于 DOM 操作涉及大量数据交互,往往触发页面的重新渲染或部分重绘,因此消耗大量时间。即使细微的操作也可能因处理信息庞大而耗时,故优化 DOM 相关操作是加速浏览器渲染的关键。关于 DOM 操作影响性能的深层原理,可参考关于浏览器渲染机制的相关文章。
针对 DOM 操作的优化,主要采取以下几种策略:
1.1 最小化现场更新
所谓“现场更新”,是指对页面中已显示的部分进行即时 DOM 修改。每一次修改,无论是插入单个字符还是整个片段,都会产生性能损耗,因为浏览器需要重新计算尺寸以完成更新。现场更新的次数越多,代码执行耗时越长;反之,减少更新次数可显著提升速度。具体示例如下:
var list = document.getElementById('mylist'),
item,
i;
for(i = 0; i < 10; i++){
item = document.creatElement('li');
list.appendChild(item);
item.appendChild(document.creatTextNode('item' + i));
}
上述代码为列表 mylist 添加了 10 个项目。每添加一个项目需执行两次现场更新:插入元素节点和插入文本节点。因此,总共需完成 20 次现场更新,每次更新均带来性能损失,导致代码运行相对缓慢。
解决方法是利用文档碎片(DocumentFragment)间接修改 DOM 元素:
var list = document.getElementById('mylist'),
fragment = document.creatDocumentFragment(),
item,
i;
for(i = 0; i < 10; i++){
item = document.creatElement('li');
fragment .appendChild(item);
item.appendChild(document.creatTextNode('item' + i));
}
list.appendChild(fragment);
采用上述方式仅需进行一次现场更新。需注意,当向 appendChild() 传入文档碎片时,仅将碎片中的子节点添加到目标元素,碎片本身不会被添加。
通过对比,可以清晰看出直接在循环中进行 DOM 节点的增删查改对浏览器造成的负担。
1.2 使用 innerHTML
除了使用 createElement() 和 appendChild() 结合的方式创建 DOM 元素外,还可以通过为 innerHTML 赋值来创建。对于少量的 DOM 更改,两种方法效率相近;但对于大量 DOM 节点的更改,后者速度远快于前者。原因在于:
当为 innerHTML 赋值时,浏览器后台会创建一个 HTML 解析器,并使用内部的 DOM 调用构建 DOM 结构,而非基于 JavaScript 的 DOM 调用。由于内部方法是编译好的而非解释执行,因此执行速度更快。
使用 innerHTML 改写上述示例:
var list = document.getElementById('mylist'),
html = '', //声明一个空字符串
i;
for(i = 0; i < 10; i++){
html += '<li>item' + i + '</li>';
}
list.innerHTML = html; // 这里记得innerHTML后面的HTML四个字母都要大写!
这种方式同样仅进行一次现场更新,且性能优于前一种方式,尽管在字符串拼接上存在轻微的性能损耗。
1.3 使用事件代理/事件委托
事件处理程序赋予 Web 应用交互能力,但许多开发人员会向页面添加大量处理程序。这直接影响了页面的整体运行性能,原因如下:
首先,每个事件处理程序对应至少一个函数,而 JavaScript 中每个函数都是对象,占用内存。内存中对象越多,性能越差。
其次,预先指定所有事件处理程序增加了 DOM 访问次数,延迟了页面交互就绪时间,导致页面响应用户操作变慢。
因此,减少事件处理程序数量是提升页面流畅度的关键,使用事件委托势在必行。
事件委托基于事件冒泡原理,只需指定一个事件处理程序即可管理某一类操作的所有事件。例如,click 事件会冒泡至 document 层级,因此无需为每个元素单独添加事件,只需在较高层级元素上添加处理程序,并通过 event 对象的属性或方法判断当前点击元素,进而做出响应。初学者可查阅事件冒泡相关知识以深入理解。
作用域优化
2. 作用域很重要
提到作用域,便涉及作用域链(scope chain)。搜索变量时,执行环境需沿作用域链向上查找。作用域链上的变量越多,遍历所需时间越长,且层级越高耗时越多。减少查找时间可直接提高代码执行效率。
以下是减少查找时间的几种方法:
2.1 避免全局查找
这是性能优化的重点。如前所述,向上查找耗时更多,因此查找全局变量和函数比局部变量更慢。示例代码如下:
function updateUI(){
var imgs = document.getElementByTagName('img');
for(var i = 0 ,lng = imgs.length;i < lng;i ++){
imgss[i].title = document.title + 'image' + i;
}
var msg = docuement.getElementById('msg');
msg.innerHTML = 'update complete.';
}
这段代码看似正常,但仔细观察会发现三处引用了全局变量 document。若页面包含大量图片,for 循环中的 document 将被执行上百次,每次均需遍历作用域链查找,导致时间浪费。
解决方法是在函数内部创建局部变量保存对 document 的引用,从而在函数内部任何地方引用 document 时无需查找全局变量。这改进了代码性能,示例如下:
function updateUI(){
var doc = document; // 将document保存在局部变量doc中
var imgs = doc.getElementByTagName('img');
for(var i = 0 ,lng = imgs.length;i < lng;i ++){
imgss[i].title = doc.title + 'image' + i;
}
var msg = doc.getElementById('msg');
msg.innerHTML = 'update complete.';
}
因此,在开发中,若函数内频繁使用全局变量,应将其保存在局部变量中。
2.2 避免使用 with 语句
with 语句会延长作用域链,导致变量查找耗时增加。虽然日常开发中较少使用,但其解决方法与前述示例相同,即将全局变量保存在局部变量中。
循环优化
3. 优化循环
循环在编程中极为常见,JavaScript 中亦随处可见。循环体反复执行同一段代码,执行时间累加,因此优化循环体代码可大幅减少执行时间。优化方法主要有四种:
3.1 减值迭代
编写迭代器(循环条件)时,通常采用从 0 开始增加至特定值的方式(var i = 0; i < 10; i++)。然而,在许多情况下,使用减值迭代器效率更高。经测试,若循环体不复杂,两者性能差异不大。
//增值迭代 --效率较低
for(var i = 0;i < items.length;i++){
doSomething(items[i]);
}
//减值迭代 --效率较高
for(var i = items.length - 1;i >= 0;i--){
doSomething(items[i]);
}
3.2 简化终止条件
由于每次循环都会计算终止条件,必须确保其执行尽可能快。主要策略是避免查找其他 DOM 元素及其属性。
//看终止条件,每次循环都需要查询items及其length属性
for(var i = 0;i < items.length;i++){
doSomething(items[i]);
}
//将items.length的值保存在局部变量lng中。
for(var i = 0,lng = items.length;i < lng;i++){
doSomething(items[i]);
}
3.3 简化循环体
原因同上,即在循环体内避免大量密集操作。
这与前述“1.1 最小化现场更新”的优化方式一致,可回顾前文。
算法复杂度与优化
4. 基本的算法优化
在计算机中,算法复杂度用 O 表示。以下是 JavaScript 中几种常见的算法类型:
O(1):常数时间,无论数据量多少,执行时间恒定,如简单值和存储在变量中的值。 O(log n):对数时间,执行时间与数量相关,但不必获取每个值,如二分法查找。 O(n):线性时间,执行时间与数量直接相关,如遍历。 O(n*n):平方时间,执行时间与数量相关,每个值至少获取 N 次,如插入排序。 掌握上述知识后,可对 JavaScript 进行算法层面的优化。示例如下:
var value = 5;
var sum = value + 10;
alert(sum);
这段代码进行了 4 次常量值查找:数字 5、变量 value、数字 10、变量 sum,其算法复杂度为 O(1)。又如:
var value = [10,5];
var sum = value[0] + value[1];
alert(sum);
在 JavaScript 中访问数组元素也是 O(1) 操作,与简单变量查找效率相同。再看:
var value = {one:10,two:10};
var sum = value.one + value.two;
alert(sum);
此处旨在说明,访问对象上的属性比访问数组和变量的效率低,因为这是一个 O(n) 操作。需要在对象的原型链中查找该属性,耗时较多。
至此,前文所述将常用全局属性保存在局部变量中的做法,正是基于此原理:访问全局属性是 O(n) 操作,而访问变量是 O(1) 操作。
最小化语句数
5. 最小化语句数
前文所述的优化多与精简语句有关,代码的质量和数量是性能评判的重要标准。前文涉及代码质量优化,此处重点讲解代码数量的优化。
5.1 精简变量声明
//用了5条语句声明5个变量
var count = 5;
var color = 'red';
var values = [1,2,3];
var now = new Date();
//用了1条语句声明5个变量,注意每个变量用逗号隔开
var count = 5,
color = 'red',
values = [1,2,3],
now = new Date();
5.2 使用数组和对象字面量
// 创建两个对象 ----不好的方式
//one 四条语句
var values = new Array();
values[0] = 123;
values[1] = 456;
values[2] = 789;
//two 四条语句
var person = new Object();
person.name = 'jozo';
person.age = 21;
person.sayName = function(){
alert(this.name);
};
// 创建两个对象 ----推荐的方式
//one 1条语句
var values = [123,456,789]
//two 1条语句
var person = {
name : 'jozo',
age : 21,
sayName : function(){
alert(this.name);
};
6. 其他
篇幅有限,如有不当之处请指正。其他优化技巧将在后续文章中继续探讨!