高性能javascript(第三章 DOM编程)

Posted on 2014-09-09 15:26  liguwe  阅读(184)  评论(0编辑  收藏  举报

1、浏览器之DOM

浏览器通常要求 DOM 实现和 JavaScript 实现保持相互独立

这对性能意味着什么呢?简单说来,两个独立的部分以功能接口连接就会带来性能损耗。一个很形象的 比喻是把 DOM 看成一个岛屿,把 JavaScript(ECMAScript)看成另一个岛屿,两者之间以一座收费桥连接

每次 ECMAScript 需要访问DOM 时,你需要过桥,交一次“过桥费”。你操作 DOM 次数越多,费用就越高。一般的建议是尽量减少过桥次数,努力停留在 ECMAScript 岛上。本章将对此问题给出详细解答,告诉你应该关注什么地方, 以提高用户交互速度。

 

2、DOM访问和修改

  访问一个 DOM 元素的代价就是交一次“过桥费”。修改元素的费用可能更贵,因为它经常导致浏览器重新计算页面的几何变化

function innerHTMLLoop() {
for (var count = 0; count < 15000; count++) {
document.getElementById('here').innerHTML += 'a';

  上面这段代码对dom访问了两次一次读取它,另外一次写入它

  改进:

  

function innerHTMLLoop2() {
    var content = '';
    for (var count = 0; count < 15000; count++) {
        content += 'a';
    }
    document.getElementById('here').innerHTML += content;
}

  一般经验法则是:轻轻地触摸 DOM,并尽量保持在 ECMAScript 范围内。

 

3、innerHTML 与 DOM 方法比较

  使用innerHTML创建1000*5的表:

  使用 DOM 方法创建同样的表:

  其实这两种方式在新版的浏览器上差别不是很大

 

4、使用节点克隆,一般会快2%~10%;

5、HTML集合:

  死循序:

    var alldivs = document.getElementsByTagName_r('div');
    for (var i = 0; i < alldivs.length; i++) {
        document.body.appendChild(document.createElement('div'))
    }

  看下面三种不同的访问方式:

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

    var coll = document.getElementsByTagName_r('div');
    var arr = toArray(coll);
//slower
    function loopCollection() {
        for (var count = 0; count < coll.length; count++) {
        }
    }

// faster
    function loopCopiedArray() {
        for (var count = 0; count < arr.length; count++) {
        }
    }
//缓存  跟loopCopiedArray差不多一样快
    function loopCacheLengthCollection() {
        var coll = document.getElementsByTagName_r('div'), len = coll.length;
        for (var count = 0; count < len; count++) {
        }
    }

6、访问集合元素时使用局部变量(缓存):

  三种缓存方式的比较如下代码:

// slow
function collectionGlobal() {
    var coll = document.getElementsByTagName_r('div'), len = coll.length,
        name = '';
    for (var count = 0; count < len; count++) {
        name = document.getElementsByTagName_r('div')[count].nodeName;
        name = document.getElementsByTagName_r('div')[count].nodeType;
        name = document.getElementsByTagName_r('div')[count].tagName;
    }
    return name;
};
// faster
function collectionLocal() {
    var coll = document.getElementsByTagName_r('div'), len = coll.length,
        name = '';
    for (var count = 0; count < len; count++) {
        name = coll[count].nodeName;
        name = coll[count].nodeType;
        name = coll[count].tagName;
    }
    return name;
};
// fastest
function collectionNodesLocal() {
    var coll = document.getElementsByTagName_r('div'), len = coll.length,
        name = '',
        el = null;
    for (var count = 0; count < len; count++) {
        el = coll[count];
        name = el.nodeName;
        name = el.nodeType;
        name = el.tagName;
    }
    return name;
};
7、遍历dom tree:使用childNode集合或使用nextSibling两种方式的比较:
  在不同的浏览器上,这两种方式的运行时间基本相同:
//nextSibling方式
function testNextSibling() {
    var el = document.getElementById('mydiv'), ch = el.firstChild,
        name = '';
    do {
        name = ch.nodeName;
    } while (ch = ch.nextSibling);
    return name;
};
// nodeName方式
function testChildNodes() {
    var el = document.getElementById('mydiv'), ch = el.childNodes,
        len = ch.length,
        name = '';
    for (var count = 0; count < len; count++) {
        name = ch[count].nodeName;
    }
    return name;
};

 

8、只表示元素节点的DOM属性(HTML标签)和表示所有节点的属性如下表:一般情况下只表示元素节点的dom要快些

 

9、选择器API

var elements = document.querySelectorAll('#menu a');

  elements 的值将包含一个引用列表,指向那些具有 id="menu"属性的元素。函数 querySelectorAll()接收 一个 CSS 选择器字符串参数并返回一个 NodeList——由符合条件的节点构成的类数组对象。此函数不返回 HTML 集合,所以返回的节点不呈现文档的“存在性结构”。

  这就避免了本章前面提到的 HTML 集合所固有 的性能问题(以及潜在的逻辑问题)。

 

var elements = document.getElementById('menu').getElementsByTagName_r('a');

  这种情况下 elements 将是一个 HTML 集合,所以你还需要将它拷贝到一个数组中,如果你想得到与 querySelectorAll()同样的返回值类型的话。

 

对于下面两段代码,使用选择器API比对手快2~6倍

//使用选择器API
var errs = document.querySelectorAll('div.warning, div.notice');
//使用一般方法
var errs = [],
    divs = document.getElementsByTagName_r('div'),
    classname = '';
for (var i = 0, len = divs.length; i < len; i++) {
    classname = divs[i].className;
    if (classname === 'notice' || classname === 'warning') {
        errs.push(divs[i]);
    }
}

 


 

10、重绘和重排版

当浏览器下载完所有页面 HTML 标记,JavaScript,CSS,图片之后,它解析文件并创建两个内部数据结构:

   一棵DOM树:表示页面结构

   一棵渲染树:表示 DOM 节点如何显示

渲染树中为每个需要显示的 DOM 树节点存放至少一个节点(隐藏 DOM 元素在渲染树中没有对应节点)。渲染树上的节点称为“框”或者“盒”,符合 CSS 模型的定义,将页面元素看作一个具有填充、边距、 边框和位置的盒。

一旦 DOM 树和渲染树构造完毕,浏览器就可以显示(绘制)页面上的元素了。

 

当 DOM 改变影响到元素的几何属性(宽和高)——例如改变了边框宽度或在段落中添加文字,将发生一系列后续动作——浏览器需要重新计算元素的几何属性,而且其他元素的几何属性和位置也会因此改变受到影响。

浏览器使渲染树上受到影响的部分失效,然后重构渲染树。这个过程被称作重排版。重排版完成时,浏览器在一个重绘进程中重新绘制屏幕上受影响的部分。

不是所有的 DOM 改变都会影响几何属性。例如,改变一个元素的背景颜色不会影响它的宽度或高度。 在这种情况下,只需要重绘(不需要重排版),因为元素的布局没有改变。


 

重绘和重排版是负担很重的操作,可能导致网页应用的用户界面失去相应。所以,十分有必要尽可能减 少这类事情的发生。

当布局和几何改变时需要重排版。在下述情况中会发生重排版:
添加或删除可见的 DOM 元素

元素位置改变

元素尺寸改变(因为边距,填充,边框宽度,宽度,高度等属性改变)
内容改变,例如,文本改变或图片被另一个不同尺寸的所替代
最初的页面渲染

浏览器窗口改变尺寸

某些改变可导致重排版整个页面:例如,当一个滚动条出现时。

 
查询并刷新渲染树改变

因为计算量与每次重排版有关,大多数浏览器通过队列化修改和批量显示优化重排版过程。然而,你可能(经常不由自主地)强迫队列刷新并要求所有计划改变的部分立刻应用。获取布局信息的操作将导致刷新队列动作,这意味着使用了下面这些方法:
• offsetTop, offsetLeft, offsetWidth, offsetHeight
• scrollTop, scrollLeft, scrollWidth, scrollHeight
• clientTop, clientLeft, clientWidth, clientHeight
• getComputedStyle() (currentStyle in IE)(在 IE 中此函数称为 currentStyle)
布局信息由这些属性和方法返回最新的数据,所以浏览器不得不运行渲染队列中待改变的项目并重新排版以返回正确的值。
在改变风格的过程中,最好不要使用前面列出的那些属性。任何一个访问都将刷新渲染队列,即使你正在获取那些最近未发生改变的或者与最新的改变无关的布局信息。
考虑下面这个例子,它改变同一个风格属性三次(这也许不是你在真正的代码中所见到的,不过它孤立地展示出一个重要话题):