首页 > 代码库 > 高性能javascript(记录三)

高性能javascript(记录三)

  DOM(文档对象模型)是一个独立的语言,用于操作XML和HTML文档的程序接口(API)。在游览器中,主要用来与HTML文档打交道,同样也用在Web程序中获取XML文档,并使用DOM API用来访问文档中的数据。尽管DOM是个与语言无关的API,它在游览器中的接口却是用Javascript实现的。客户端脚本编程大多数的是在和底层文档打交道。

  DOM的访问和修改是有代价的。打个比方:DOM和js各自为一个岛屿,之间仅有一个收费桥梁连接。每次js访问DOM的时候就相当于途径一次这座桥,并交纳过桥费。访问的次数多了,费用也就高了。这是DOM的访问,而修改的代价就更高了,因为修改DOM会导致游览器重新计算页面的几何变化。最坏的情况就是:在循环中访问或修改元素,尤其是对HTML元素的集合循环操作。

//较慢
function innerHTMLLoop(){
    for(var count = 0;count < 15000;count++){
        document.getElmentById("id").innerHTML += str;
    }  
}
//这种方式问题在于每次循环迭代时,该元素都被访问两次:一次读取innerHTML属性值,另一次重写它。
//较快
function innerHTMLLoop2(){
    var content = ‘ ‘;
    for(var count = 0;count < 15000;count++){
        countent += str;    
    }
    document.getElementById("id").innerHTML += content;
}

  通用法则:减少访问DOM的次数,把运算尽量留在ECMAScript这一端处理。  

  问题:修改页面区域是用innerHTML属性还是document.createElement()的原生DOM方法好?

      答案:相差无几,除开最新版的WebKit内核之外的所有游览器中,innerHTML会更快一些。如果在一个对性能有着苛刻要求的操作中更新一大段HTML,推荐使用innerHTML,因为它在绝大部分游览器中都运行的更快。但大多数日常生活操作而言,并没有太大区别,故应该根据可读性、稳定性、团队习惯、代码风格来综合决定使用哪种方式。

  节点克隆:使用DOM方法更新页面内容的另一个途径是克隆已有的元素,而不是创建新元素--换句话说也就是用element.cloneNode()代替document.createElement()。在大多数游览器,节点克隆都更有效率,但也非明显。

  HTML集合:HTML集合是包含了DOM节点引用的类数组对象。

  以下方法的返回值就是集合:document.getElementsByName() 、document.getElementsByClassName()、document.getElementsByTagName();

  以下属性同样返回HTML集合:document.images、document.links、document.forms、document.forms[0].elements;

  以上的方法和属性返回值都是HTML集合对象,这是个类数组的列表(并非真正的数组,因为没有push()和slice()之类的方法),但提供了一个类似数组的length属性,并且还能以数字索引的方式访问列表中的元素。DOM标准中定义:HTML集以一种“假死实时态”实时存在,意味着当底层文档对象更新时,它也自动更新。事实上,HTML集合一直和文档保持连接,每次当你需要最新消息时,都会重复执行查询的过程,哪怕只是获取集合的元素个数也是,因此导致性能下降。

//一个意外的死循环
var allDivs = document.getElementsByTagName("div");
for(var i = 0;i < allDivs.length;i++){
    document.body.appendChild(document.createElement("div"))
}

  这就是因为html集会自动更新导致的一个死循环allDivs.length反应的是底层文档的当前状态,会随着迭代增加。

//读取一个集合的length比读取一个普通数组的length要慢得多,因为每次都要查询。

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

//比较下面两个函数:
//较慢
function loopCollection(){
    for(var count = 0;count < coll.length;count++){
        //代码处理
   }
}
//读取元素集合的length属性会引发集合进行更新,从而提高性能消耗。优化:把集合长度缓存到一个局部变量中,然后在循环的条件退出语句中使用该变量。性能跟loopCoiedArray()一样。
//较快
function loopCopiedArray(){
    for(var count = 0;count < arr.length;count++){
        //代码处理
    }
} 

  很多情况下如果只需要遍历一个相对较小的集合,缓存length就够了。因为虽然遍历数组比遍历集合快,但是也同时会带来额外的消耗,故因考虑是否值得使用数组拷贝。

                                                                                 访问集合元素时使用局部变量:最慢的版本每次都要读取全局document,优化后的版本缓存了一个集合的引用,最快的版本把当前的集合元素存储到一个变量。

//较慢
function collectionGlobal(){
    var coll = document.getElementsByTagName("div"),
          len = coll.length,
          name = ‘ ‘;
    for(var count = 0;count < len;count++){ 
        name = document.getElementsByTagName("div")[count].nodeName;
        name = document.getElementsByTagName("div")[count].nodeType;
        name = document.getElementsByTagName("div")[count].tagName;
    }
     return name;
}
//较快
function collectionLocal(){
    var coll = document.getElementsByTagName("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;
}
//最快
function collectionNodesLocal(){
    var coll = document.getElementsByTagName("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;
}

遍历DOM

  获取DOM元素:childNodes得到元素集合,nextSibling来获取每个相邻元素。

  以非递归方式遍历元素子节点:

function testNextSibling(){
    var el = document.getElementById("mydiv"),
          ch = el.firstChild,name = ‘ ‘;
    do {
        name = ch.nodeName;
    }while ( ch = ch.nextSibbling);
    return name;
};

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;
};

  nextSibling和childNode两种方法运行时间几乎相等,只有在IE里,nextSibling性能更高。

  元素节点:在某些情况下,只需访问元素节点,因此在循环中很可能需要检查返回节点的类型并过滤掉非元素节点。这些类型检查和过滤其实是不必要的DOM操作。大部分游览器提供API只返回元素节点。

              技术分享

  使用children替代childNodes会更快,因为集合项更少。

  选择器API:querySelectorAll()(使用CSS选择器定位节点)原生DOM方法比使用JS和DOM来遍历查找元素要快得多。

重绘和重排:

  游览器下载完页面中的所有组件:HTML标记、js、css、图片之后会解析并生成两个内部数据结构:DOM树(表示页面结构)和渲染树(表示DOM节点如何显示)。一旦完成,游览器就开始绘制页面元素。而当DOM的变化影响了元素的几何属性,游览器需要重新计算元素的几何属性,同样其他元素的几何属性和位置也会因此受到影响。这时就发生了重排和重绘。

  重排:游览器会使渲染树中受到影响的部分失效,并重新构造渲染树。

  发生时间:添加或删除可见DOM元素、元素位置改变、元素尺寸改变、内容改变、页面渲染器初始化、游览器窗口尺寸改变。

  重绘:完成重排后,游览器会重新绘制受影响的部分到屏幕中。

  最小化重排和重绘:为了减少发生次数,应该合并多次对DOM和样式的修改,然后一次处理。

  批量修改DOM:当需要对DOM元素进行一系列操作时,可以通过以下方式减少重排和重绘的次数:使元素脱离文档流、对其应用多重改变、把元素带回文档。

//更新指定节点数据的通用函数
function appendDataToElement(appendToElement,data){
    var a,li;
    for (var i = 0;max = data.length;i < max;i++){
        a = document.createElement("a");
        a.href = data.data[i].url;
        a.appendChild(document.createTextNode(data[i].name));
        li = document.createElement("li");
        li.appendChild(a);
        appendToElement.appendChild(li);
    }
};
//不考虑重排问题
var ul = document.getElementById("myul");
appendDataToElement(ul,data);
//优化,使DOM脱离文档,减少重排
//方法一     隐藏元素,应用修改,重新显示
var ul = document.getElementById("myul");
ul.style.display = "none";
appendDataToElement(ul,data);
ul.style.display = "block";
//方法二     使用文档片断(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档
var fragment = document.createDocumentFragment();
appendDataToElement(fragment,data);
document.getElementById("myul").appendChild(fragment);
//方法三     将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素
var old = document.getElementById("myul");
var clone = old.cloneNode(true);
appendDataToElement(clone,data);+
old.parentNode.replaceCHhild(clone,old);

注:推荐使用文档片断,因为它们所产生的DOM遍历和重排次数最少。

  还有两种情况下优化重排和重绘的方式:在获取布局信息时,缓存布局信息和处理页面动画时,让元素脱离动画流(注:若是大量使用css中:hover这个伪选择器会明显降低响应速度)。

  事件委托:

  当页面存在大量元素,而且每个都要一次或多次绑定事件处理器时,每绑定一个事件处理器都是有代价的,要么加重了页面负担(更多的代码),要么增加了运行期的执行时间。而一个简单而优雅的处理DOM事件的技术是事件委托。基于:事件逐层冒泡并能被父级元素捕获。

//例如
document.getElementById("menu").onclick = function(e){
   
    //游览器 target
    e = e || window.event;
    var target = e.target || e.srcElement;
    var pageid,hrefparts;
    //只关心hrefs,非链接点击则退出  
    if ( target.nodeName !== "A" ){
        return;
    }

    //从链接中找出页面ID
    hrefparts = target.href.split("/");
    pageid = hrefparts[ hrefparts.length - 1 ];
    pageid = pageid.replace(".html"," ");

    //更新页面
    ajaxRequest("xhr.php?page" + id,updatePageContents);

    //游览器组织默认行为并取消冒泡
    if (typeof e.preventDefault === "function"){
        e.preventDefault();
        e.stopPropagetion();
    }else{
        e.returnValue = false;
        e.cancelBubble = true;
    }
};

 

 

高性能javascript(记录三)