高性能JavaScript之DOM操作
浏览器通常要求 DOM 实现和 JavaScript 实现保持相互独立。这对性能意味着什么呢?简单说来,两个独立的部分以功能接口连接就会带来性能损耗。
一个很形象的比喻是把 DOM 看成一个岛屿,把JavaScript看成另一个岛屿,两者之间以一座收费桥连接。每次 ECMAScript 需要访 问 DOM 时,你需要过桥,交一次“过桥费”。你操作 DOM 次数越多,费用就越高
DOM访问和修改
访问一个 DOM 元素的代价就是交一次“过桥费”。修改元素的费用 可能更贵,因为它经常导致浏览器重新计算页面的几何变化(重排)。
例子:
function innerHTMLLoop() {
for (var count = 0; count < 15000; count++) {
document.getElementById('here').innerHTML += 'a';
}
}
此函数在循环中更新页面内容。这段代码的问题是,在每次循环都对 DOM 元素访问两次:一次读取innerHTML 属性,另一次写入它。也访问次数:2*n
优化:
function innerHTMLLoop2() {
var content = '';
for (var count = 0; count < 15000; count++) {
content += 'a';
}
document.getElementById('here').innerHTML += content;
}
优化版本将字符串先进行拼接,这种情况对DOM元素的访问只有两次:一次读取innerHTML 属性能容,另一次写入它。很显然。访问 DOM 越多,代码的执行速度就越慢。访问次数:2
重绘和重排
当浏览器下载完所有页面 HTML 标记,JavaScript,CSS,图片之后,它解析文件并创建两个内部数据结构:
一棵 DOM 树:表示页面结构
一棵渲染树:表示 DOM 节点如何显示
渲染树中为每个需要显示的 DOM 树节点存放至少一个节点(隐藏 DOM 元素在渲染树中没有对应节 点)。渲染树上的节点称为“框”或者“盒”,符合 CSS 模型的定义,将页面元素看作一个具有填充、边距、 边框和位置的盒。一旦 DOM 树和渲染树构造完毕,浏览器就可以显示(绘制)页面上的元素了。
重排
当 DOM 改变影响到元素的几何属性(宽和高)——例如改变了边框宽度或在段落中添加文字,将发生 一系列后续动作——浏览器需要重新计算元素的几何属性,而且其他元素的几何属性和位置也会因此改变 受到影响。浏览器使渲染树上受到影响的部分失效,然后重构渲染树。这个过程被称作重排。重排版完成时,浏览器在一个重绘进程中重新绘制屏幕上受影响的部分。(重绘)
发生重排情况:
- 添加或删除可见的 DOM 元素。
- 元素位置改变
- 元素尺寸改变(因为边距,填充,边框宽度,宽度,高度等属性改变)
- 内容改变,例如,文本改变或图片被另一个不同尺寸的所替代
- 最初的页面渲染
- 浏览器窗口改变尺寸
根据改变的性质,渲染树上或大或小的一部分需要重新计算。某些改变可导致重排版整个页面。
重绘
不是所有的 DOM 改变都会影响几何属性。例如,改变一个元素的背景颜色不会影响它的宽度或高度。 在这种情况下,只需要重绘(不需要重排版),因为元素的布局没有改变。
重绘和重排是负担很重的操作,可能导致网页应用的用户界面失去相应。所以需要尽量减少重绘和重排。优化策略是将多个 DOM 和风格改变合并到一个批次中一次性执行。
最小化重绘和重排版
法一:改变style
例子:
var el = document.getElementById('mydiv');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';
这里改变了三个style属性,每次改变都影响到元素的几何属性。它导致浏览器重 排版了三次。
优化
法一:将所有改变合并在一起执行,只修改 DOM 一次。可通过使用 cssText 属性实现。
var el = document.getElementById('mydiv');
el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px;'
法二:修改 CSS 的类名称,而不是修改内联风格代码。适用于那些style不依赖于运行逻辑,不需要计算的情况。
优点:改变 CSS 类名称更清晰,更易于维护;有助于保持脚本免 除显示代码。
var el = document.getElementById('mydiv');
el.className = 'active';
批量修改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 树都会导致重排。
有下面减少重排的三个方法:
1、隐藏元素,进行修改,然后再显示它。
var ul = document.getElementById('mylist');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';
2、使用一个文档片断在已存 DOM 之外创建一个子树,然后将它拷贝到文档中。
文档片断是一个轻量级的 document 对象,它被设计专用于更新、移动节点之类的任务。文档片断一个便 利的语法特性是当你向节点附加一个片断时,实际添加的是文档片断的子节点群,而不是片断自己。
var fragment = document.createDocumentFragment();
for (let i = 0;i<10;i++){
let node = document.createElement("p");
node.innerHTML = i;
fragment.appendChild(node);
}
document.body.appendChild(fragment);
3、将原始元素拷贝到一个脱离文档的节点中,修改副本,然后覆盖原始元素。
var old = document.getElementById('mylist');
var clone = old.cloneNode(true);
appendDataToElement(clone, data);
old.parentNode.replaceChild(clone, old);
动画元素的优化
例子:
显示和隐藏部分页面构成展开/折叠动画是一种常见的交互模式。它通常包括区域扩大的几何动画,将 页面其他部分推向下方。对于这种场景,可以用下面步骤避免对大部分页面进行重排:
1、用绝对坐标对它进行定位,使它位于页面布局流之外。当它的尺寸改变时,就不会推移页面中其他元素的位置,而只是覆盖其他元素。
2、展开动作只在“动画元素”上进行时,这时其他元素的坐标并没有改变,换句话说,其他元素并没有因为“动画元素”的扩大而随之下移,而是任由动画元素覆盖。
3、“动画元素”的动画结束时,将其他元素的位置下移到动画元素下方,界面“跳”了一下。
事件代理
当页面中存在大量元素,而且每个元素有一个或多个事件句柄与之挂接(例如 onclick)时,可能会影响性能。
事件代理原理:事件逐层冒泡总能被父元素捕获。你只需要在一个包装元素上挂接一个事件,用于处理子元素发生的所有事件
根据 DOM 标准,每个事件有三个阶段:
捕获、到达目标、冒泡
实现事件代理只需要冒泡即可。只需要监听事件,看看他们是不是从你感兴趣的元素中发出的。
总结
DOM 访问和操作是现代网页应用中很重要的一部分。但每次你通过桥梁从 ECMAScript 岛到达 DOM 岛 时,都会被收取“过桥费”。
对于DOM角度的性能优化策略需要注意:
1、最小化 DOM 访问,在 JavaScript 端做尽可能多的事情。
2、在反复访问的地方使用局部变量存放 DOM 引用。
3、注意重绘和重排版;批量修改style,离线操作 DOM 树,缓存并减少对布局信息的访问。
4、动画中使用绝对坐标。
5、使用事件代理技术最小化事件操作数量