欢迎光临

木子屋

标题: 提升JavaScript运行速度之DOM篇

作者: dnawo

日期: 2009-03-05 11:11 AM

分类: 脚本语言

内容: 在Web开发中，JavaScript的一个很重要的作用就是对DOM进行操作，可你知道么？对DOM的操作是非常昂贵的，因为这会导致浏览器执行回流操作，而执行了过多的回流操作，你就会发现自己的网站变得越来越慢了，我们应该尽可能的减少DOM操作。本文是这个系列的最后一篇，给出了一些指导性原则，比如在什么时候应该对DOM可以进行什么样的操作等。
【原文】Nicholas C. Zakas - Speed up your JavaScript, Part 4
【译文】明达 - 如何提升JavaScript的运行速度（DOM篇）
以下是对原文的翻译：
在过去的几周中，我为大家介绍了几种可以加快JavaScript脚本运行速度的技术。第一节 介绍了如何优化循环。第二节 的重点放在优化函数内部代码上，还介绍了队列（queuing）和记忆化（memoization）两种技术，来减轻函数的工作负担。第三节 就如何将递归转换为迭代循环或者记忆化方式的话题，展开了讨论。第四节是这个系列的最后一篇，也就是本文，将重点阐述过多的DOM操作所带来的影响。
我们都知道，DOM操作的效率是很低的，而且不是一般的慢，而且这也是引发性能问题的常见问题之一。为什么会慢呢？因为对DOM的修改为影响网页的用户界面，重绘页面是一项昂贵的操作。太多的DOM操作会导致一系列的重绘操作，为了确保执行结果的准确性，所有的修改操作是按顺序同步执行的。我们称这个过程叫做回流（reflow），同时这也是最昂贵的浏览器操作之一。回流操作主要会发生在几种情况下：
* 当对DOM节点执行新增或者删除操作时。
* 动态设置一个样式时（比如element.style.width="10px"）。
* 当获取一个必须经过计算的尺寸值时，比如访问offsetWidth、clientHeight或者其他需要经过计算的CSS值（在兼容DOM的浏览器中，可以通过getComputedStyle函数获取；在IE中，可以通过currentStyle属性获取）。
解决问题的关键，就是限制通过DOM操作所引发回流的次数。大部分浏览器都不会在JavaScript的执行过程中更新DOM。相应的，这些浏览器将对对DOM的操作放进一个队列，并在JavaScript脚本执行完毕以后按顺序一次执行完毕。也就是说，在JavaScript执行的过程中，用户不能和浏览器进行互动，直到一个回流操作被执行。（ 失控脚本对话框 会触发回流操作，因为他执行了一个中止JavaScript执行的操作，此时会对用户界面进行更新）
如果要减少由于DOM修改带来的回流操作，有两个基本的方法。第一个就是在对当前DOM进行操作之前，尽可能多的做一些准备工作。一个经典的例子就是向document对象中添加很多DOM节点：
复制内容到剪贴板 程序代码for (var i=0; i < items.length; i++){
var item = document.createElement("li");
item.appendChild(document.createTextNode("Option " + i);
list.appendChild(item);
}
这段代码的效率是很低的，因为他在每次循环中都会修改当前DOM结构。为了提高性能，我们需要将这个次数降到最低，对于这个案例来说，最好的办法是建立一个文档碎片（document fragment），作为那些已创建元素元素的临时容器，最后一次将容器的内容直接添加到父节点中：
复制内容到剪贴板 程序代码var fragment = document.createDocumentFragment();
for (var i=0; i < items.length; i++){
var item = document.createElement("li");
item.appendChild(document.createTextNode("Option " + i);
fragment.appendChild(item);
}
list.appendChild(fragment);
经过调整的代码，只会修改一次当前DOM的结构，就在最后一行，而在这之前，我们用文档碎片来保存那些中间结果。因为文档碎片没有任何可见内容，所以这类修改不会触发回流操作。实际上，文档碎片也不能被添加到DOM中，我们需要将它作为参数传给appendChild函数，而实际上添加的不是文档碎片本身，而是它下面的所有子元素。
避免不必要回流操作的另外一种方法，就是在对DOM操作之前，把要操作的元素，先从当前DOM结构中删除。对于删除一个元素，基本有两种方法：
1.通过removeChild()或者replaceChild()实现真正意义上的删除。
2.设置该元素的display样式为“none”。
而一旦修改操作完成，上面这个过程就需要反转过来，将删除的元素重新添加到当前的DOM结构中，我们还是拿上面的例子来做说明：
复制内容到剪贴板 程序代码list.style.display = "none";
for (var i=0; i < items.length; i++){
var item = document.createElement("li");
item.appendChild(document.createTextNode("Option " + i);
list.appendChild(item);
}
list.style.display = "";
将list的display样式设置为“none”后，就将这个元素从当前的DOM结构中删除了，因为这个节点不再可视。在将display属性设置回之前的默认值之前，向其下添加子元素是不会触发回流操作的。
另外一个经常引起回流操作的情况是通过style属性对元素的外观进行修改。比如下面这个例子：
复制内容到剪贴板 程序代码element.style.backgroundColor = "blue";
element.style.color = "red";
element.style.fontSize = "12em";
这段代码修改了三个样式，同时也就触发了三次回流操作。每次修改元素的style属性，都肯定会触发回流操作。如果你要同时修改一个元素的很多样式，最好的办法是将这些样式放到一个class下，然后直接修改元素的class，这可比单独修改元素的样式要强得多。比如下面这个例子：
复制内容到剪贴板 程序代码.newStyle {
background-color: blue;
color: red;
font-size: 12em;
}
这样我们在JavaScript代码中，只需下面这行代码就可以修改样式：
复制内容到剪贴板 程序代码/*element.className = "newStyle";*/
修改元素的class属性，会一次将所有的样式应用在目标元素上，而且只会触发一次回流操作。这样做不止更加有效，而且还更容易维护。
既然DOM几乎在所有情况下都很慢，就很有必要将获取的DOM数据缓存起来。这种方法，不仅对获取那些会触发回流操作的属性（比如offsetWidth等）尤为重要，就算对于一般情况，也同样适用。下面介绍一个效率低的夸张的例子：
复制内容到剪贴板 程序代码document.getElementById("myDiv").style.left = document.getElementById("myDiv").offsetLeft +
document.getElementById("myDiv").offsetWidth + "px";
这里对getElementById()调用了三次，是一个很大的问题，访问DOM是很昂贵的，而这三个调用恰恰访问的是同一个元素，也许我们像下面这样写，会更好一些：
复制内容到剪贴板 程序代码var myDiv = document.getElementById("myDiv");
myDiv.style.left = myDiv.offsetLeft + myDiv.offsetWidth + "px";
我们去掉了一些冗余操作，现在对DOM操作的次数已经被减小了。对于那些使用次数超过一次的DOM值，我们都应该缓冲起来，这样可以避免无谓的性能消耗。
也许，拖慢属性访问速度的罪魁祸首就是HTMLCollection对象。这些对象是object类型的，只要DOM需要返回一组节点时就会使用这个对象，也就是说childNodes属性和getElementsByTagName()的返回值都属于这种情况。我们可能经常会将HTMLCollection当作数组来使用，但实际上他是一个根据DOM结构自动变化的实体对象。每次你访问一个HTMLCollection对象的属性，他都会对DOM内所有的节点进行一次完整匹配，这意味着下面的代码将导致一个死循环：
复制内容到剪贴板 程序代码var divs = document.getElementsByTagName("div");
for (var i=0; i < divs.length; i++){ //infinite loop
document.body.appendChild(document.createElement("div"));
}
这段代码为什么会变成死循环呢？因为在每次循环中，将会向document中新增一个div元素，同时也会更新divs这个集合，也就是说循环的索引永远都不会超过divs.length的值，因为divs.length的值是伴随着循环而递增的。每次访问divs.length，就会更新一次集合对象，这可比访问一个普通数组的length属性要付出更大的代价。当对HTMLCollection对象进行操作时，应该将访问的次数尽可能的降至最低，最简单的，你可以将length属性缓存在一个本地变量中，这样就能大幅度的提高循环的效率。
复制内容到剪贴板 程序代码var divs = document.getElementsByTagName("div");
for (var i=0, len=divs.length; i < len; i++){ //not an infinite loop
document.body.appendChild(document.createElement("div"));
}
修改后的代码已经不是死循环了，因为在每次循环时，len的值都是保持固定不变的。将属性值缓存起来除了更加有效率，还可以保证document不会执行多于一次的查询。
本文是“Speed up your JavaScript”这个系列的最后一篇文章，我希望你现在已经知道如何避免那个脚本失控的对话框，以及如何让你的脚本运行的更快。我所提到的技巧很多别人已经提过了，我只是将它们组织到一起，这样大家可以更容易的找到这些信息。

+ 查看当前日志评论 (0)

登录

────────────

木子屋

PJBlog3 v3.2.9.518 Inside.

Processed In 0.957 ms

返回

标题: 提升JavaScript运行速度之DOM篇

你没有权限发表评论

────────────

木子屋

PJBlog3 v3.2.9.518 Inside.

Processed In 0.957 ms

返回

暂无评论

返回

────────────

木子屋

PJBlog3 v3.2.9.518 Inside.

Processed In 0.988 ms

返回