《高性能JavaScript》读书笔记③:DOM编程

浏览器中的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;
}
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容

  • 一、JS前言 (1)认识JS 也许你已经了解HTML标记(也称为结构),知道了CSS样式(也称为表示),会使用HT...
    凛0_0阅读 2,752评论 0 8
  • 翻译自:高性能Javascript 第三章Dom操作是昂贵的,它通常是web应用的性能瓶颈。这篇文章讨论Dom操作...
    Addy_Zhou阅读 3,024评论 0 5
  • 之前通过深入学习DOM的相关知识,看了慕课网DOM探索之基础详解篇这个视频(在最近看第三遍的时候,准备记录一点东西...
    微醺岁月阅读 4,444评论 2 62
  • 最近我的通讯录里出现了下面的情况,有不同的人给我发信息,我估计是群发的,他们有可能是同一个师傅教的 我敢保证,他并...
    韦哥说道阅读 299评论 0 0
  • 喜欢狗,因为它们是人类忠实的朋友。尤其看了电影《忠犬八公》之后,当时哭得稀里哗啦。结果我们家现在养了一只喵星人,这...
    JamineX阅读 1,363评论 0 0