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

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

3.1 浏览器中的DOM

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

天生就慢

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

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

3.2 访问与修改

  • 描述
    访问DOM元素是有代价的,修改元素则更为昂贵,尤其对HTML集合循环操作。
  • 原因
    它会导致浏览器重新计算页面的几何变化。
  • 反例
// 这个函数每次循环该元素都被访问两次:一次读取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()
document.images // 页面中所有的<img>元素
document.links // 所有的<a>元素
document.forms // 所有表单
document.forms[0].elements // 页面中第一个表单的所有字段

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

3.2.3.1 昂贵的集合

  • 描述
    将集合复制到数组中,进行操作数组来代替操作集合。
  • 原因
    读取集合中的length比读取普通数组中的length要慢很多。
  • 反例
var coll = document.getElementsByTagName('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('div');
var ar = toArray(coll);
// 快
function loopCopiedArray() {
    for (var count = 0; count < arr.length; count++) {

    }
}

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

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

    }
}

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

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

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

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

3.2.4 遍历DOM

3.2.4.1 获取DOM元素

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

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

3.2.4.2 元素节点

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

能区分元素节点和其他节点的DOM属性

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

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 重排何时发生

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

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

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脱离文档的三种方法:
  4. 影藏元素,修改应用,重新显示;
  5. 使用文档片段在当前DOM之外构建一个子树,再把它拷贝回文档中;
  6. 将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素。
// 更新节点数据通用函数
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 让元素脱离动画流

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

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

3.3.6 IE和:hover

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

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

3.4 事件委托

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

  1. 捕获;
  2. 到达目标;
  3. 冒泡。
    事件委托只需要冒泡即可。使用时事件代理,只需要给外层元素绑定一个处理器,就可以处理在其子元素上触发的所有时间。
一个DOM树的例子

当用户点击了“menu #1”链接,点击事件首先被<a>元素收到。然后它沿着 DOM 树冒泡,被<li>元素收到,然后是<ul>,以此类推,一直到达文档的顶层直至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. 组织默认动作(可选,但本例需要,因为需要捕获并阻止打开链接)。

欢迎大佬纠错指导,欢迎同行交流学习。

GitHub:https://github.com/Code4GL
简书:https://www.jianshu.com/u/7f5541a6b6d2
知乎:https://www.zhihu.com/people/code4gl/activities
公众号:code_everything
QQ:771841496
邮箱:guanli1991@163.com

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

推荐阅读更多精彩内容