资讯专栏INFORMATION COLUMN

高性能JavaScript阅读简记(二)

RaoMeng / 874人阅读

摘要:访问集合元素时使用局部变量对于任何类型的访问,如果对同一个属性或者方法访问多次,最好使用一个局部变量对此成员进行缓存。

三、DOM Scripting DOM编程

我们都知道对DOM操作的代价昂贵,这往往成为网页应用中的性能瓶颈。在解决这个问题之前,我们需要先知道什么是DOM,为什么他会很慢。

DOM in the Browser World 浏览器中的DOM

DOM是一个独立于语言的,使用XMLHTML文档操作的应用程序接口(API)。浏览器中多与HTML文档打交道,DOM APIs也多用于访问文档中的数据。而在浏览器的实现中,往往要求DOM和JavaScript相互独立。例如在IE中,JavaScript的实现位于库文件jscript.dll中,而DOM的实现位于另一个库mshtml.dll中(内部代号Trident),这也是为什么IE内核是Trident,IE前缀为-ms-,当然,我们说的浏览器内核其实英文名叫 Rendering Engine/Layout Engine,准确翻译应该是渲染引擎/排版引擎/模板引擎(其实是一个东西);另外一个就是JavaScript引擎,ie6-8采用的是JScript引擎,ie9采用的是Chakra。而这是相分离的;Chrome中Webkit的渲染引擎和V8的JavaScript引擎,Firefox中Spider-MonkeyJavaScript引擎和Gecko的渲染引擎,都是相互分离的。
Inherently Slow 天生就慢
因为上文所说的,浏览器渲染引擎JavaScript引擎是相互独立的,那么两者之间以功能接口相互连接就会带来性能损耗。曾有人把DOMECMAScript(JavaScript)比喻成两个岛屿,之间以一座收费桥连接,每次ECMAScript需要访问DOM时,都需要过桥,交一次“过桥费”,操作DOM的次数越多,费用就越高,这里的费用我们可以看作性能消耗。因此请尽力减少操作DOM的次数。
1. DOM Access and Modification DOM访问和修改
访问DOM的代价昂贵,修改DOM的代价可能更贵,因为修改会导致浏览器重新计算页面的几何变化,更更更贵的是采用循环访问或者修改元素,特别是在HTML集合中进行循环。简单举例:

function innerHTMLLoop(){
    for ( var count = 0; count < 100000; count++){
        document.getElementById("p").innerHTML += "-";
    }
}

这时候,每执行一次for循环,就对DOM进行了一次读操作和写操作(访问和修改);此时我们可以采用另外一种方式:

function innerHTMLLoop(){
    var content = "";
    for ( var count = 0; count < 100000; count++){
        content += "-";
    }
    document.getElementById("p").innerHTML += content;
}

我们使用了一个局部变量存储更新后的内容,在循环结束时一次性写入,这时候只执行了一次读操作和写操作,性能提升显著。因此,尽量少的操作DOM,如果可以在JavaScript范围内完成的话。
2. innerHTML Versus DOM methods innerHTML与DOM方法
在老版本浏览器中,innerHTML更快但差别不大,更新的浏览器中,不相上下,最新的浏览器中,DOM方法更快,但依然差别不大。
3. Cloning Nodes 节点克隆
这样的方法和DOM方法操作速度不相上下。

HTML Collections HTML集合

HTMLCollection是用于存放DOM节点引用的类数组对象。得到的方法有:document.getElementByName/document.getElementByClassName/document.getElementByTagName/document.querySelectAll/document.images/document.links/document.forms等;也有类似于document.forms[0].elements(页面第一个表单的所有字段)等。
上面这些方法返回HTMLCollection对象,是一种类似数组的列表,没有数组的方法,但是有lenth属性。在DOM标准中定义为:“虚拟存在,意味着当底层文档更新时,他们将自动更新”。HTML集合实际上会去查询文档,更新信息时,每次都要重复执行这种查询操作,这正是低效率的来源。

Expensive collections 昂贵的集合
先看个例子:

var oDiv = document.getElementByTagName("div");
for (var i = 0; i < oDiv.length; i++){
    document.body.appendChild(document.createElement("div"))
}

好吧,这是个死循环,永远不会结束,但是这个过程中,每访问一次oDiv.length,就会重新计算一遍其长度,当然,前提是对所有的div重新进行一次遍历,因此,在这里,我们最好使用一个变量暂存oDIv.length

var oDivLen = document.getElementByTagName("div").length;
for (var i = 0; i < oDivLen; i++){
    document.body.appendChild(document.createElement("div"))
}

从性能角度来讲,这样做会快很多。同时,因为对HTML集合的访问比对数组访问要更耗费性能,因此在某些不得不多次访问HTML集合的情况下,可以先将集合存储为一个数组,然后对数组进行访问:

function toArray(htmlList){
    for (var i = 0, htmlArray = [], len = htmlList.length; i < len; i++){
        htmlArray[i] = htmlList[i];
    }
    return htmlArray;
}

当然,这也需要额外的开销,需要自己进行权衡是否有必要这样做。

Local variables when accessing collection elements 访问集合元素时使用局部变量
对于任何类型的DOM访问,如果对同一个DOM属性或者方法访问多次,最好使用一个局部变量对此DOM成员进行缓存。特别是在HTML集合中访问元素时,如果多次对集合中的某一元素访问,同样需要将这个元素先进行缓存。

Walking the DOM DOM漫谈

DOM API提供了多种访问文档结构特定部分的方法,去选择最有效的API。

Crawling the DOM 抓取DOM
如果你可以通过:document.getElementByID();获得某元素就不要去用document.getElementById().parentNode;这么麻烦去获取。如果你可已通过nextSibling去获取一个元素就不要通过childNodes去获取,因为后者是一个nodeList集合。

Element nodes 元素节点
DOM包含三个节点(也可以说是四个):元素节点、属性节点、文本节点(以及注释节点);通常情况下,我们获取到和使用的是元素节点,但是我们通过childNodes、firstChild、nextSibling等方法获取到的是所有节点的属性,js中有一些其他的API可以用来只返回元素节点或者元素节点的某些属性,我们可以用这些API取代那些返回整个全部节点或者节点属性的API,例如:

childNodes                        children
childNodes.length                childElementCount
firstChild                        firstElementChild
lastChild                        lastElementChild
nextSibling                        nextElementSibling
previousSibling                    previousElementSibling

在所有的浏览器中,后者比前者要快,只不过IE中后面部分方法并不支持,比如IE6/7/8,只支持children方法。

The Selectors API 选择器API
传统的选择器在性能方面问题不大,只不过应用场景相对单一,当我们用习惯了CSS选择器之后,我们会觉得DOM给我们提供的选择器让我们抓狂。在querySelector/querySelectorAll之前,如果我们想要查找到元素下符合条件的另一元素时,不得不使用类似下面的方法:document.getElementById("id1").getElementById("id2");,但如果你想获取一个类名为class1或类名为class2的div的时候,不得不这么处理:

function getDivClass1(className1,className2){
    var results = [];
    divs = document.getElementByTagName("div");
    for (var i = 0,len = divs.length; i < len; i++){
        _className = divs[i].className;
        if(_className === className1 || _className === className2){
            results.push(divs[i]);
        }
    }
    return results;
}

不仅仅是因为冗长的代码,多次对DOM进行遍历带来的性能问题也不可小窥;不过在有了querySelector/querySelectorAll之后,这一切变得简单,减少对DOM的遍历也带来了性能的提升。上面两个例子可以重写如下:

document.querySelector("#id1 #id2");
document.querySelectorAll("div.className1,div.className2");

因此,如果可以,尽量使用querySelector/querySelectorAll吧。

Repaints and Reflows 重绘和重排(也称回流)

这涉及到一个比较古老的议题,浏览器在拿到服务器响应时都干了什么。我查阅了相当一部分资料(网上很多地方说法是不准确的,包括一些问答、博客),去了解整个流程,这里简单的描述一下过程。更多细节可参考《浏览器的工作原理:新式网络浏览器幕后揭秘》,原版地址(http://taligarsiel.com/Projec...)。
上文提到过,浏览器的实现一般包括渲染引擎JavaScript引擎;二者是相互独立的。
我们先从渲染引擎的角度来看一下在拿到服务器的文档后的处理流程:

Parsing HTML to construct the DOM tree 解析HTML以构建DOM tree
解析HTML文档,将各个标记逐个转化为DOM tree 上的DOM节点;当然并不是一一对应;类似于head这样的标记是在DOM tree上是没有对应的节点的。在这个过程中,同时被解析的还包括外部CSS文件以及样式元素中的样式数据,这些数据信息被准备好进行下一步工作。

Render tree construction 构建render tree
DOM tree构建过程中,CSS文件同时被解析,DOM tree上每一个节点的对应的颜色尺寸等信息被保存在另一个被称作rules tree的对象中(具体实现方式webkitgecho是不一样的,可参考上文提到过的《浏览器的工作原理》)。DOM treerules tree两者一一对应,均构建完成之后,render tree也就构建完成了。

Layout of the render tree 布局render tree
依据render tree中的节点信息和对应的rules中的尺寸信息(包括display属性等),为每一个节点分配一个应该出现在屏幕上的确切坐标。

Painting the render tree 绘制render tree
就是将已布局好的节点,加上对应的颜色等信息,绘制在页面上。

当然,浏览器并不会等到全部的HTML文档信息都拿到之后才进行解析,也不会等到全部解析完毕之后才会进行构建render tree设置布局。渲染引擎可能在接收到一部分文档后就开始解析,在解析了一部分文档后就开始进行构建render treelayout render tree
JavaScript引擎的工作:
正常的流程中,渲染引擎在遇到

阅读需要支付1元查看
<