浏览器中的DOM(DOM in Browser World)
- 文档对象模型(DOM)是用于操作XML、HTML文档的程序接口(API)
- 浏览器通常独立实现DOM和ECMAScript (如IE中,DOM通过mshtml.dll库实现,而JavaScript通过JScript.dll库来实现),以便其他的技术能共享使用(例如VBScript也可以使用DOM)
天生就慢(Inherently Slow)
-
不同的浏览器厂商,通过不同的引擎来实现DOM和JavaScript
Safari浏览器使用Webkit的WebCore来实现DOM渲染,使用独立研发的JavaScript引擎(SquirrelFish 金鳞鱼)
[ˈskwɪrəl]
Google Chrome浏览器同样使用Webkit的WebCore来实现DOM渲染,独立研发的JavaScript引擎名为V8
Firefox浏览器使用独立的Gecko(壁虎)
[ˈgekəʊ]
引擎实现DOM渲染,使用独立研发的JavaScript引擎(JagerMonkey)['jɑ:gə]
- 由于浏览器把ECMAScript和DOM独立实现,所以通过JS代码调用DOM API时就会产生性能损耗。 作者把ECMAScript和DOM比喻成两座岛屿,岛屿间有一架收费的桥梁来连接。每当ECMAScript访问DOM时,就要经过这座桥并缴纳“过桥费”,而访问DOM次数越多,费用也就越高。这里的过桥费,就指的是性能损耗。
DOM访问与修改(DOM Access and Modification)
- 访问DOM元素是有代价的,而修改DOM元素代价更为高昂,因为会导致浏览器重新计算页面的几何变化。
示例:演示一个循环修改DOM元素的极端情况
function innerHTMLLoop(){
//循环一万五千次,累加a字符串,放入到id=here的DOM元素中
for(var count=0; count<15000; count++){
document.getElementById('here').innerHML += 'a';
}
}
修改:先循环获取a字符串,再放入DOM元素;
function innerHTMLLoop(){
var temp = ''; //缓存字符串
//循环一万五千次,累加a字符串,放入到id=here的DOM元素中
for(var count=0; count<15000; count++){
temp += 'a';
}
document.getElementById('here').innerHML += temp; //最后放入
}
##### innerHTML对比DOM方法(innerHML Versus DOM methods)
- HTML元素的innerHTML属性和DOM API document.createElement()方法,哪个更高效呢?
书里分别使用这两种方式,往`<table>`中插入1000行表格,对比测试性能。
测试发现,在旧版的浏览器中innerHTML效率稍微会高一些,且相对于DOM API的方式来看,代码可阅读性也更好。
究竟使用哪种方式,要根据代码可读性、稳定性、代码风格来综合考量。
##### 节点克隆(Cloning Nodes)
- 除此之外,还可以通过`element.cloneNode()`方法克隆已有的元素实现更新页面。在大多数浏览器中节点克隆都比较有效率,但不是特别明显。
##### HTML集合(HTML Collections)
- 以下的方法可以得到DOM节点的集合:
* document.getElementsByName()
* document.getElementsByClassName()
* document.getElementsByTagName()
- 事实上,如DOM标准中所定义的,HTML集合会以一种“假定实时态”(assumed to be live)实时存在。这意味着HTML集合一直与底层文档保持着连接,当底层文档对象更新时,它也会自动更新。
- 示例:一段由于因为HTML集合实时性,导致死循环的代码
//alldivs是页面中所有DIV的集合
var alldivs = ducument.getElementsByTagName('div');
//遍历集合,根据DIV集合的数量,给body追加同等数量的div元素
for(var i=0;i<alldivs.length;i++){
document.body.appendChild(document.createElement('div'));
}
而优化的方法也很简单:①把集合长度作为局部变量缓存起来;②将HTML集合放到一个数组中(因为遍历数组的速度比遍历HTM集合要快)
function collectionNodesLocal(){
var coll = ducument.getElementsByTagName('div'), //获取HML集合
len = coll.length, //缓存HTML集合的长度
el = null; //缓存集合中的对象
for(var count=0;count<len;count++){
el = coll[count];
console.info(el.nodeName);
}
}
- 开发者通过`getElementById()`获取特定元素比通过`getElementsByTagName()`获取元素列表效率更高,除此之外,还可以通过CSS选择器准确获取元素。尤其是大量组合查询的情况下,`querySelectorAll()`更有效率。
//获取id=menu的元素之中的所有a元素
var elements = document.querySelectAll('#menu a');
//获取clas为warning或者notice的div元素
var errs = document.querySelectorAll('div.warning,div.notice');
# 重绘与重排(Repaints and Reflows)
- 当浏览器下载完所有页面组件(HTML/JavaScript/CSS/图片)后,会解析并生成两个数据结构:DOM树、渲染树
- DOM树中每一个显示的节点在渲染树存在一个对应的节点,隐藏的DOM元素在渲染树中没有对应的节点
- 渲染树种的节点被称为fames(帧)或者boxes(盒子),这也符合CSS盒模型的定义
##### 重排何时发生(When Does a Reflow Happen)
- 一旦DOM树和渲染树构建完成后,浏览器就开始paint(显示/绘制)页面元素,而当DOM节点发生改变,影响元素的几何属性时(比如改变边框高度或者给段落添加文字),比如下列情况:
* 添加或删除可见的DOM元素
* 元素位置发生改变
* 元素(内外边距/边框厚度/宽高等)尺寸发生改变
* 元素内容改变
* 页面渲染器初始化
* 浏览器窗口尺寸发生改变
- 浏览器需要重新计算该元素的几何属性,而其他元素的几何属性和位置可能也会受到影响。当然不是所有的DOM变化都会影响几何属性比如改变元素的背景颜色并不会影响元素的宽高。
- 浏览器重新构造渲染树的过程称为“`重排(reflow)`”。完成重排后,浏览器会重新绘制受影响的元素到屏幕中,这个过程称为"`重绘(repaint)`"。
- 重排和重绘都需要付出高昂的性能代价,甚至可能会导致Web应用程序的UI卡顿,应当尽量避免重排和重绘的发生。
##### 渲染树变化的排队与刷新(Queuing and Flushing Render Tree Changes)
- 由于重排会产生计算消耗,大多数浏览器通过队列化批量执行来优化重排的过程。但如果执行类似获取布局信息等操作,会导致强制刷新队列,我们应当避免此类情况的发生。比如以下方法:
* offsetTop,offsetLeft,offsetWidth,offsetHeight
* scrollTop,scrollLeft,scrollWidth,scrollHeight
* clientTop,clientLeft,clientWidth,clientHeight
* getComputedStyle(),(currentStyle in IE)
- 如果实在需要查询布局信息,应当先一次性修改过后,再执行查询
bodystyle.color = 'red';
bodystyle.color = 'white';
bodystyle.color = 'green';
tmp = computed.backgroundColor;
tmp = computed.backgroundImage;
tmp = computed.backgroundAttachment;
##### 最小化重绘和重排(Minimizing Repaints and Reflows)
- 一个好的提高程序响应速度的策略应该减少重绘和重排的发生,比如合并多次对DOM的样式的修改,一次性处理。
案例:有三个样式属性被改变,每次改变都将影响元素的几何结构
var el = document.getElementById('myDiv');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';
优化:合并所有的改变,然后一次性处理(还可以通过修改CSS的class名称指定新样式)
var el = document.getElementById('myDiv');
el.style.cssText += 'border-left:1px;border-right:2px;padding:5px;';
- 当批量修改DOM时,可通过三种方式减少重绘和重排的次数:
* 使元素脱离文档流(隐藏元素,执行修改,重新显示)
* 使用文档片段(document fragment)的方式
* 拷贝原始元素到一个脱离文档流的节点中修改,修改完成后替换原始元素
- 案例:现有一个数组的数据,需要拆分更新到一个ul元素
//模拟数据
var data = [];
//获取ul元素
var ul = document.getElementById('newsList');
//传入要更新的数据数组,遍历并填充到ul元素中
appendDataToElement(ul,data);
- 方式①:使元素脱离文档(改变display属性,临时从文档流中移除要更新的元素)
//模拟数据
var data = [];
//获取ul元素
var ul = document.getElementById('newsList');
//传入要更新的数据数组,遍历并填充到ul元素中
ul.style.display = 'none'; //隐藏
appendDataToElement(ul,data);
ul.style.display = 'block'; //显示
- 方式②:使用文档片段 __(推荐使用此种方式)__
//创建代码片段
var fragment = document.createDocumentFragment();
//将要更新的数据先放入fragment元素中
appendDataToElement(fragment,data);
//更新fragment到ul
document.getElementById('newsList').appendChild(fragment);
- 方式③:拷贝原始元素更新
//获取原始节点
var old = document.getElementById('newsList');
//克隆节点
var clone = old.cloneNode(true);
//将要更新的数据先放入fragment元素中
appendDataToElement(clone,data);
//通过克隆节点进行覆盖更新
old.parentNode.replaceChild(clone,old);
# 事件委托(Event Delegation)
- 页面中大量的元素一次或多次绑定事件也可能会影响性能,要知道每绑定一个事件处理器都是有代价的。尤其是在页面onload时,对于富交互应用的页面来说都会造成拥堵。而页面跟踪事件处理器,也会占用更多的内存。
- 通过事件委托来处理DOM事件是比较优雅的处理方式。由于DOM事件逐层冒泡并能被父级元素捕获,所以只需要给外层元素绑定绑定一个处理器,作为事件代理,就可以处理其子元素上出发的所有事件了。
document.getElementById(‘menu’).onclick = function(e){
e = e || window.event;
var target = e.target || e.srcElement;
var pageid,hrefparts;
if(target.nodeName !== 'A'){
return;
}
//获取点击超链接的id值
hrefparts = target.href.split('/');
pageid = hrefparts[hrefparts.length - 1];
pageid = pageid.replace('.html','');
//更新页面 ajaxRequest('xhr.php?page='+id,updatePageContexts');
//浏览器阻止默认行为,并取消冒泡
if(type e.preventDefault === 'function'){
e.preventDefault();
e.stopPropagation();
}else{
e.returnValue = false;
e.cancelBubble = true;
}
}