先来点概念

文档对象模型(DOM)是一个独立于语言的,使用 XML 和 HTML 文档操作的应用程序接口(API)。

在浏览器中,主要与 HTML 文档打交道,在网页应用中检索 XML 文档也很常见。DOM APIs 主要用于访问这些文档中的数据。

尽管 DOM 是与语言无关的 API,在浏览器中的接口却是以 JavaScript 实现的。客户端大多数脚本程序与文档打交道,DOM 就成为 JavaScript 代码日常行为中重要的组成部分。

浏览器通常要求 DOM 实现和 JavaScript 实现保持相互独立。例如,在 Internet Explorer中,被称为 JScript 的 JavaScript实现位于库文件 jscript.dll中,而 DOM 实现位于另一个库 mshtml.dll(内部代号 Trident)。

这种分离技术允许其他技术和语言,如 VBScript,受益于 Trident 所提供的 DOM 功能和渲染功能。Safari 使用 WebKit的 WebCore处理 DOM 和渲染,具有一个分离的 JavaScriptCore 引擎(最新版本中的绰号是 SquirrelFish)。

Google Chrome 也使用 WebKit的 WebCore库渲染页面,但实现了自己的 JavaScript 引擎 V8。

在 Firefox 中,JavaScript 实现采用 Spider-Monkey(最新版中称作 TraceMonkey),与其 Gecko 渲染 引擎相分离。

天生就慢

这对性能意味着什么呢?简单说来,两个独立的部分以功能接口连接就会带来性能损耗。

一个很形象的比喻是把 DOM 看成一个岛屿,把 JavaScript(ECMAScript)看成另一个岛屿,两者之间以一座收费桥连接(参见 John Hrvatin,微,MIX09,http://videos.visitmix.com/MIX09/T53F)。

每次 ECMAScript 需要访问 DOM 时,你需要过桥,交一次“过桥费”。

你操作 DOM 次数越多,费用就越高。一般的建议是尽量减少过桥次数,努力停留在 ECMAScript 岛上。

本章将对此问题给出详细解答,告诉你应该关注什么地方,以提高用户交互速度。

DOM访问和修改
简单来说,正如前面所讨论的那样,访问一个 DOM 元素的代价就是交一次“过桥费”。修改元素的费用可能更贵,因为它经常导致浏览器重新计算页面的几何变化。

当然,访问或修改元素最坏的情况是使用循环执行此操作,特别是在 HTML 集合中使用循环。

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

此函数在循环中更新页面内容。这段代码的问题是,在每次循环单元中都对 DOM 元素访问两次:一次读取 innerHTML 属性能容,另一次写入它。

一个更有效率的版本将使用局部变量存储更新后的内容,在循环结束时一次性写入

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

在所有浏览器中,新版本运行速度都要快得多。

访问 DOM 越多,代码的执行速度就越慢。因此,一般经验法则是:轻轻地触摸 DOM,并尽量保持在 ECMAScript 范围内。

重绘和重排版 

当浏览器下载完所有页面 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)

布局信息由这些属性和方法返回最新的数据,所以浏览器不得不运行渲染队列中待改变的项目并重新排版以返回正确的值。

在改变风格的过程中,最好不要使用前面列出的那些属性。

任何一个访问都将刷新渲染队列,即使你正在获取那些最近未发生改变的或者与最新的改变无关的布局信息。

考虑下面这个例子,它改变同一个风格属性三次(这也许不是你在真正的代码中所见到的,不过它孤立地展示出一个重要话题):

var computed,
    tmp = "",
    bodystyle = document.body.style;

if (document.body.currentStyle) { // IE, Opera 

    computed = document.body.currentStyle; 

} else { // W3C 

    computed = document.defaultView.getComputedStyle(document.body, '');
} 

bodystyle.color = 'red';
tmp = computed.backgroundColor; 

bodystyle.color = 'white';
tmp = computed.backgroundImage; 

bodystyle.color = 'green';
tmp = computed.backgroundAttachment;

在这个例子中,body元素的前景色被改变了三次,每次改变之后,都导入 computed 的风格。

导入的属性 backgroundColor, backgroundImage, 和 backgroundAttachment 与颜色改变无关。

然而,浏览器需要刷新渲染队列并重排版,因为 computed的风格被查询而引发。

最小化重绘和重排版

重排版和重绘代价昂贵,所以,提高程序响应速度一个好策略是减少此类操作发生的机会。

为减少发生次数,你应该将多个 DOM 和风格改变合并到一个批次中一次性执行。

考虑这个例子:

var el = document.getElementById('mydiv');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px'; 

这里改变了三个风格属性,每次改变都影响到元素的几何属性。

在这个糟糕的例子中,它导致浏览器重排版了三次。

大多数现代浏览器优化了这种情况只进行一次重排版,但是在老式浏览器中,效率将十分低下。

如果其他代码在这段代码运行时查询布局信息,将导致三次重布局发生。

而且,此代码访问 DOM 四次,可以被优化。

var el = document.getElementById('mydiv');
el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px;';

批量修改 DOM 

当你需要对 DOM 元素进行多次修改时,你可以通过以下步骤减少重绘和重排版的次数:

  • 从文档流中摘除该元素
  • 对其应用多重改变
  • 将元素带回文档中

此过程引发两次重排版——第一步引发一次,第三步引发一次。

如果你忽略了这两个步骤,那么第二步中每次改变都将引发一次重排版。

有三种基本方法可以将 DOM 从文档中摘除:

  • 隐藏元素,进行修改,然后再显示它
  • 使用一个文档片断在已存 DOM 之外创建一个子树,然后将它拷贝到文档中
  • 将原始元素拷贝到一个脱离文档的节点中,修改副本,然后覆盖原始元素

为演示脱离文档操作,考虑这样一个链接列表,它必须被更多的信息所更新:

<ul id="mylist">
    <li><a href="http://phpied.com">Stoyan</a></li>
    <li><a href="http://julienlecomte.com">Julien</a></li>
</ul>

假设附加数据已经存储在一个对象中了,需要插入到这个列表中。这些数据定义如下:

 var data = [
     {
         "name": "Nicholas",
         "url": "http://nczonline.net"
     },
     {
         "name": "Ross",
         "url": "http://techfoolery.com"
     }
]; 

下面是一个通用的函数,用于将新数据更新到指定节点中:

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');
appendDataToElement(ul, data); 

使用这个方法,data 队列上的每个新条目追加到 DOM 树都会导致重排版。如前面所讨论过的,减少重排版的一个方法是通过改变 display属性,临时从文档上移除<ul>元素然后再恢复它。

var ul = document.getElementById('mylist');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block'; 

另一种减少重排版次数的方法是:在文档之外创建并更新一个文档片断,然后将它附加在原始列表上。

文档片断是一个轻量级的 document 对象,它被设计专用于更新、移动节点之类的任务。

文档片断一个便利的语法特性是当你向节点附加一个片断时,实际添加的是文档片断的子节点群,而不是片断自己。

下面的例子减少一行代码,只引发一次重排版,只触发“更新 DOM”一次。

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 操作和重排版。

唯一潜在的缺点是,当前文档片断还没有得到充分利用,开发者可能不熟悉此技术。

注:信息来源:javascript高性能编程

web前端 DOM 详解的更多相关文章

  1. web前端攻击详解

    前端攻击成因 在web网页的脚本中,有些部分的显示内容会依据外界输入值而发生变化,而如果这些声称html的程序中存在问题,就会滋生名为跨站脚本的安全隐患 XSS跨站脚本攻击: 英文全称cross-si ...

  2. web前端——CSS详解

    简介 CSS(Casading Style Sheet)是一组HTML元素外观的设置规则,用于控制web页面的表现形式,一般被翻译为"级联样式表"或"层叠样式表" ...

  3. 虚拟Dom详解 - (二)

    第一篇文章中主要讲解了虚拟DOM基本实现,简单的回顾一下,虚拟DOM是使用json数据描述的一段虚拟Node节点树,通过render函数生成其真实DOM节点.并添加到其对应的元素容器中.在创建真实DO ...

  4. JavaScript(2)---DOM详解

    JavaScript(2)---DOM详解 一.DOM概念 什么是DOM DOM全称为文本对象模型(Document Object Model),它定义了所有HTML元素的对象和属性,以及访问他们的方 ...

  5. Java web.xml 配置详解

    在项目中总会遇到一些关于加载的优先级问题,近期也同样遇到过类似的,所以自己查找资料总结了下,下面有些是转载其他人的,毕竟人家写的不错,自己也就不重复造轮子了,只是略加点了自己的修饰. 首先可以肯定的是 ...

  6. [深入学习Web安全](5)详解MySQL注射

    [深入学习Web安全](5)详解MySQL注射 0x00 目录 0x00 目录 0x01 MySQL注射的简单介绍 0x02 对于information_schema库的研究 0x03 注射第一步—— ...

  7. java web.xml配置详解

    1.启动一个WEB项目的时候,WEB容器会去读取它的配置文件web.xml,读取<listener>和<context-param>两个结点. 2.紧急着,容创建一个Servl ...

  8. web.xml文件详解

      web.xml文件详解 Table of Contents 1 listener. filter.servlet 加载顺序 2 web.xml文件详解 3 相应元素配置 1 listener. f ...

  9. java web.xml配置详解(转)

    源出处:java web.xml配置详解 1.常规配置:每一个站的WEB-INF下都有一个web.xml的设定文件,它提供了我们站台的配置设定. web.xml定义: .站台的名称和说明 .针对环境参 ...

随机推荐

  1. IP网际协议 - IP首部,IP路由选择,子网掩码

    IP首部 4个字节的32 bit值以下面的次序传输:首先是0-7 bit,其次8-15 bit,然后1 6-23 bit,最后是24~31 bit.这种传输次序称作big endian字节序.由于T ...

  2. HBase 索引创建

    本文参考了文"mysql索引背后的数据结构及算法原理",之所以还要摘录,主要是为了形成hbase索引研究的开篇,弄明白什么索引的本质,如有版权问题,请及时通知. 索引的本质 索引是 ...

  3. 如果以一个树状的形式返回一个UIView的所有子视图

    该方法也是从一个视频中看到,总觉得会有很大作用,故记录在这里. 它返回一个xml的字符串,用火狐浏览器或者其他可以格式化xml的工具打开,即可查看其层级关系. /** * 返回传入view的所有层级结 ...

  4. unix下各种查看“变量”的命令比较

    子程序只会继承父程序的环境变量,而不继承其自定义变量. env 查看所有环境变量 set 查看所有变量,包括环境变量和自定义变量 set 还可以给程序位置参数赋值: set 1 2 3 将1赋值给$1 ...

  5. 和菜鸟一起学linux之linux性能分析工具oprofile移植

    一.内核编译选项 make menuconfig General setup---> [*] Profiling support <*> OProfile system profil ...

  6. Which SQL statement is the trump card to the senior software developer

    Which SQL statement is the trump card to the senior software developer                    MA Genfeng ...

  7. CDH安装系统环境准备——虚拟机网络配置

    虚拟机网络配置教程如下: 1.修改网络配置文件[root@master ~]# vi /etc/sysconfig/network-scripts/ifcfg-eth0配置IP地址.网关.掩码.DNS ...

  8. 获取list,有内容就赋值,根据ID显现NAME,没有显现list

    function onTOWN() { var town=mini.get("TOWN_ID"); var town_id =town.getValue(); $.ajax({ u ...

  9. 03_Linux FTP

    linux搭建ftp server,在windows向上传 http://www.2cto.com/os/201204/126898.html yum install vsftp.rpm    安装v ...

  10. WebLogic域配置策略

    WebLogic域配置策略--手动和模板选项,第一部分 域含有BEA WebLogic Server实例的配置信息.它包含有关服务器.集群和机器的配置信息.域还含有关于资源,例如Java数据库连接(J ...