高性能JavaScript之DOM操作优化

高性能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、使用事件代理技术最小化事件操作数量

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值