《高性能JavaScript》第三章 DOM编程

本文详细探讨了浏览器中的DOM编程,指出DOM与JavaScript交互的性能问题,并提出了一系列优化策略,包括减少DOM访问、innerHTML与DOM方法的权衡、节点克隆、避免昂贵的HTML集合、遍历DOM的高效方法、理解重绘与重排,以及事件委托等。通过这些方法,可以显著提升JavaScript在处理DOM时的性能。

3.1 浏览器中的DOM

文档对象模型(DOM)是一个独立于语言的,用于操作XML和HTML文档操作的程序接口(API)。在浏览器中,主要用来与HTML文档打交道,同样也用在Web程序中获取XML文档,并使用DOM API来访问文档中的数据。

天生就慢

简单理解,两个相互独立的功能只要通过接口彼此连接,就会产生消耗。将DOM和JavaScript各自想象成一个岛屿,它们之间用收费桥梁连接,JavaScript每次访问DOM都要途径这座桥,并交纳过桥费。访问DOM次数越多,费用就越高。

性能优化:尽可能减少过桥的次数,努力呆在JavaScript岛上。

3.2 访问与修改

  • 描述

访问DOM元素是有代价的,修改元素则更为昂贵,尤其对HTML集合循环操作。

  • 原因

它会导致浏览器重新计算页面的几何变化。

  • 反例
--------------------------------------------------------------------
注:如果你对python感兴趣,我这有个学习Python基地,里面有很多学习资料,感兴趣的+Q群:895817687
--------------------------------------------------------------------

    // 这个函数每次循环该元素都被访问两次:一次读取innerHTML属性值,另一次是重写它。
    function innerHTMLLoop() {
        for (var count = 0; count < 15000; count++) {
            document.getElementById('here').innerHTML += 'a'; 
        }
    }
  • 正例
    // 用局部变量存储修改中的内容,在循环结束后一次性写入。
    function innerHTMLLoop2() {
        var content = '';
        for (var count = 0; count < 15000; count++) {
            content += 'a';
        }
        document.getElementById('here').innerHTML += content;
    }

3.2.1 innerHTML对比DOM方法

修改页面区域的最佳方案?
innerHTML:非标准但支持良好;
DOM API:类似document.createElement()原生DOM方法。
答案:性能相差无几。除开WebKit内核(Chrome和Safari)之外的所有浏览器中,innerHTML会更快一些。

性能优化:如果对一个性能有着苛刻要求的操作中更新一大段HTML,推荐使用innerHTML,因为它在绝大部分浏览器中运行的更快。但对于大多数日常操作而言,没有太大的区别。

3.2.2 节点克隆

使用DOM方法更新页面内容的另一个途径是克隆已有元素,而不是创建新元素。即使用element.cloneNode()替代document.createElement()。

性能优化:大多数浏览器中,节点克隆都更有效率,但效果也不是特别明显。

3.2.3 HTML集合

HTML集合是包含了DOM节点引用的类数组对象。HTML集以一种“假定实时态”实时存在,这意味着当底层文档对象更新时,它也会自动更新。

注意:此集合是类似数组的列表,并非数组。但提供了length属性,以及以数字索引方式访问列表中的元素。

    // 以下方法返回的就是HTML集合。
    document.getElementsByName()
    document.getElementsByClassName()
    document.getElementsByTagName_r()
    document.images // 页面中所有的<img>元素
    document.links // 所有的<a>元素
    document.forms // 所有表单
    document.forms[0].elements // 页面中第一个表单的所有字段

低效根源:事实上,HTML集合一直与文档保持着连接,每次你需要最新信息时,都会重复执行查询的过程,哪怕是只获取集合中的元素个数(length)也是如此。

3.2.3.1 昂贵的集合

  • 描述

将集合复制到数组中,进行操作数组来代替操作集合。

  • 原因

读取集合中的length比读取普通数组中的length要慢很多。

  • 反例
    var coll = document.getElementsByTagName_r('div');
    // 慢
    function loopCollection() {
        for (var count = 0; count < coll.length; count++) {
    
        }
    }
  • 正例
    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 ar = toArray(coll);
    // 快
    function loopCopiedArray() {
        for (var count = 0; count < arr.length; count++) {
    
        }
    }

多数情况下只需要遍历一个相对较小的集合,那么缓存length就够了。

    // 此函数运行得与loopCopiedArray()一样快。
    function loopCacheLengthCollection() {
        var coll = document.getElementsByTagName_r('div'),
        len = coll.length;
        for (var count = 0; count < len; count++) {
    
        }
    }

注意:这会因为额外的步骤带来消耗,而且会多遍历一次集合,因此需要评估在特定条件下数组拷贝是否有帮助。

3.2.3.2 访问集合元素时使用局部变量

当遍历一个集合时,第一优化原则是把集合存储在局部变量中,并把length缓存在循环外部,然后食用局部变量替代这些需要多次读取的元素。

    // 较慢
    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;
    };
    // 较快
    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; 
    };
    // 最快
    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;
    };

3.2.4 遍历DOM

3.2.4.1 获取DOM元素

你可以使用childNodes得到元素集合,或者用nextSibling来获取每个相邻元素。

性能优化:不同浏览器中,这两种方法运行时间几乎相等。在性能要求极高时,在老版本IE中更推荐nextSibling方法来查找DOM节点。

3.2.4.2 元素节点

DOM元素诸如childNodes、firstChild、nextSibling并不区分元素节点和其他类型节点。多数情况下我们只需要访问元素节点,因此可能需要检查和过滤掉非元素节点。这些类型检查和过滤其实是比必要的操作。

在这里插入图片描述

性能优化:使用左边的属性代替右边,效率更快。

3.2.4.3 选择器API

使用CSS选择器也是一种定位节点的便利途径,因为开发者都熟悉CSS。最新浏览器提供了一个名为querySelectorAll()的原生DOM方法,这比使用JavaScript和DOM来遍历查找元素要快很多。

    // 使用querySelectorAll()
    var elements = document.querySelectorAll('#menu a');
    // 不使用querySelectorAll()
    var elements = document.getElementById('menu').getElementsByTagName_r('a');

如果要处理大量组合查询,使用querySelectorAll()会更有效率。

    var errs = document.querySelectorAll('div.warning, div.notice');

还可以使用另一个方法querySelector()来获取第一个匹配的节点。

注意:使用之前应该先检查一下浏览器是否支持,如果不支持,也许应该把库升级到最新版本了。

3.3 重绘与重排

浏览器下载完页面的所有组件:HTML标签、JavaScript、CSS、图片后,会解析并生成两个内部数据结构:
DOM树:表示页面结构。DOM树中的每一个需要显示的节点在渲染中至少存在一个对应的节点,隐藏的DOM元素在渲染树中没有对应的节点;
渲染树:表示DOM节点如何显示。渲染树中的节点被称为“帧”或“盒”。
一旦DOM树和渲染树构建完成,浏览器就开始绘制页面元素了。当DOM的变化影响了元素的几何属性(宽和高)时,浏览器就需要重新计算元素的集合属性,同样其他元素的几何属性和位置也会受到影响。

重排(reflow):浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树的过程;
重绘(repaint):完成重排后,浏览器会冲洗绘制受影响的部分到屏幕中的过程。

性能优化:重绘和重排操作的代价都是昂贵的,它们会导致Web应用的UI反应迟钝。所以应尽可能减少这类操作的发送。

3.3.1 重排何时发生

当页面布局和几何属性改变时就需要重排,以下情况会发生重排:

添加或删除可见的DOM元素;
元素位置改变;
元素尺寸改变;
内容改变;
页面渲染器初始化;
浏览器窗口尺寸改变。

3.3.2 渲染树变化的排队与刷新

由于每次重排都会产生计算消耗,大多数浏览器通过队列化修改并批量执行来优化重排过程。但以下获取布局信息的操作会强制刷新队列:

    offsetTop, offsetLeft, offsetWidth, offsetHeight
    scrollTop, scrollLeft, scrollWidth, scrollHeight
    clientTop, clientLeft, clientWidth, clientHeight
    getComputedStyle() (currentStyle in IE)(在 IE 中此函数称为 currentStyle)

性能优化:应避免使用以上的属性修改样式。

3.3.3 最小化重绘和重排

3.3.3.1 改变样式

  • 描述

重排和重绘的代价可能非常昂贵,因此减少此类操作会提升性能。

  • 反例
    var el = document.getElementById('mydiv');
    el.style.borderLeft = '1px';
    el.style.borderRight = '2px';
    el.style.padding = '5px';
  • 正例
    // 合并改变,一次处理。
    var el = document.getElementById('mydiv');
    el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px;';
    // 保留现有样式。
    el.style.cssText += '; border-left: 1px;';
    // 修改样式名称,尽管可能带来轻微的性能影响,因为改变类时需要检查级联样式。
    var el = document.getElementById('mydiv');
    el.className = 'active';

3.3.3.2 批量修改DOM

通过以下步骤可以减少一系列DOM操作带来重绘和重排的次数:

1:使元素脱离文档流(触发重排);
2:对其引用多重改变;
3:把元素带回文档中(触发重排)。
使DOM脱离文档的三种方法:

1:影藏元素,修改应用,重新显示;
2:使用文档片段在当前DOM之外构建一个子树,再把它拷贝回文档中;
3:将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素。

    // 更新节点数据通用函数
    function appendDataToElement(appendToElement, data) {
        var a, li;
        for (var i = 0, max = data.length; i < max; i++) {
            a = document.createElement('a');
            a.href = data[i].url;
            a.appendChild(document.createTextNode(data[i].name));
            li = document.createElement('li');
            li.appendChild(a);
            appendToElement.appendChild(li); 
        }
    };
    // 第一种方案
    var ul = document.getElementById('mylist');
    ul.style.display = 'none';
    appendDataToElement(ul, data);
    ul.style.display = 'block';
    // 第二种方案
    var fragment = document.createDocumentFragment();
    appendDataToElement(fragment, data);
    document.getElementById('mylist').appendChild(fragment);
    // 第三种方案
    var old = document.getElementById('mylist');
    var clone = old.cloneNode(true);
    appendDataToElement(clone, data);
    old.parentNode.replaceChild(clone, old);

性能优化:推荐尽可能使用第二种方案:文档片段。因为它们所产生的DOM和重排次数最少。

3.3.4 缓存布局信息

  • 描述

最好的做法是尽量减少布局信息的获取次数,获取后把它赋值给局部变量,然后再操作局部变量。

  • 原因

当你查询布局信息时,浏览器会返回最新值,会刷新队列并应用所有变更。

  • 反例
    // 低效的
    myElement.style.left = 1 + myElement.offsetLeft + 'px';
    myElement.style.top = 1 + myElement.offsetTop + 'px';
    if (myElement.offsetLeft >= 500) {
        stopAnimation();
    }
  • 正例
    current++
    myElement.style.left = current + 'px';
    myElement.style.top = current + 'px';
    if (current >= 500) { 
        stopAnimation();
    }

3.3.5 让元素脱离动画流

用展开/折叠的方式来显示和影藏部分页面是一种常见的交互模式。它通常包括展开区域的几何动画,并将页面其他部分推向下方。
使用以下步骤可以避免页面中大部分重排:

使用绝对位置定位页面上的动画元素,将其脱离文档流;
让元素动起来。当它扩大时覆盖部分页面。但是这只是页面的一个小区域重绘过程,不会产生重排和重绘页面的大部分内容;
当动画结束时回复定位,从而只会下移一次文档的其他元素。

3.3.6 IE和:hover

从IE 7开始,IE允许使用:hover这个CSS伪选择器。然而,当你有大量元素使用:hover,那么会降低响应速度。此问题在IE 8上更为明显。

性能优化:在元素很多的情况下,比如表格,应该避免使用:hover来高亮显示鼠标所在行。

3.4 事件委托

根据DOM标准,每个时间都要经历三个阶段:

捕获;
到达目标;
冒泡。
事件委托只需要冒泡即可。使用时事件代理,只需要给外层元素绑定一个处理器,就可以处理在其子元素上触发的所有时间。
在这里插入图片描述

当用户点击了“menu #1”链接,点击事件首先被元素收到。然后它沿着 DOM 树冒泡,被

  • 元素收到,然后是
    • ,以此类推,一直到达文档的顶层直至window。这使得你可以添加一个事件处理器,来接收所有子节点产生的事件消息。
      也许你想为图中的文档提供一个渐进增强的Ajax体验。
  •     document.getElementById('menu').onclick = function(e) {
            // 浏览器target
            e = e || window.event;
            var target = e.target || e.srcElement;
            var pageid, hrefparts;
            // 只关心hrefs,非链接点击则退出
            // exit the function on non-link clicks
            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.stopPropagation();
            } else {
                e.returnValue = false;
                e.cancelBubble = true;
            }
        };
    

    如你所见,事件委托技术并不复杂,你只需检查事件是否来自你所预期的元素。如果去掉跨浏览器兼容部分,代码会更简洁。
    跨浏览器兼容部分包括:

    1:访问事件对象,并判断事件源;
    2:取消文档树中的冒泡(可选);
    3:组织默认动作(可选,但本例需要,因为需要捕获并阻止打开链接)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值