在Web开发中,JavaScript的一个很重要的做用就是对DOM进行操做,可你知道么?对DOM的操做是很是昂贵的,由于这会致使浏览器执行 回流操做,而执行了过多的回流操做,你就会发现本身的网站变得愈来愈慢了,咱们应该尽量的减小DOM操做。本文是这个系列的最后一篇,给出了一些指导性 原则,好比在何时应该对DOM能够进行什么样的操做等。javascript
【原文】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节点:
html
/*
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),做为那些已建立元素元素的临时容器,最后一次将容器的内容直接添加到父节点中:
java
/*
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结构中,咱们仍是拿上面的例子来作说明:
web
/*
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等)尤其重要,就算对于通常状况,也一样适用。下面介绍一个效率低的夸张的例子:
app
/*
document.getElementById("myDiv").style.left = document.getElementById("myDiv").offsetLeft +
document.getElementById("myDiv").offsetWidth + "px";
*/
这里对getElementById()调用了三次,是一个很大的问题,访问DOM是很昂贵的,而这三个调用偏偏访问的是同一个元素,也许咱们像下面这样写,会更好一些:
ide
/*
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不会执行多于一次的查询。