乐趣区

高性能JavaScript之DOM篇

 本篇文章主要分享一下操作 DOM 时的一些细节,来提高页面性能。首先我们来思考以下几个问题。1. 如何获取页面中所有 class 为 div1 和 div2 的 div 元素。2. 你了解 HTMLCollection 和 NodeList 吗?有什么区别?3. 如何获取一个元素的 css 属性值?4. 怎么减少重排和重绘?

问题一:如何获取页面中所有 class 为 div1 和 div2 的 div 元素。
 这个问题如果我以前写的话,我会这么写。
var errs = [],
    divs = document.getElementsByTagName('div'),
    className = '';
for(var i = 0 , len = divs.length; i < len; i ++){className = divs[i].className;
    if (className === 'div1' || className === 'div2') {errs.push(divs[i]);
    }
}
 看起来中规中矩,好像没什么大问题。但是现在我会这么写
var errs2 = document.querySelectorAll('div.div1, div.div2');
 用 querySelectorAll, 表面上看缩短了很多代码量,其实它的作用还不止这个。接下来我们引出第二个问题。

问题二:你了解 HTMLCollection 和 NodeList 吗?有什么区别?
 我们先来看两段代码。想一下这两段代码有什么区别。
// 片段一
var all = document.getElementsByTagName('div');
for(var i = 0; i < all.length ; i ++) {document.body.appendChild(document.createElement('div'));
}
// 片段二
var all2 = document.querySelectorAll('div');
for(var i = 0; i < all2.length ; i ++) {document.body.appendChild(document.createElement('div'));
}

你可以分别复制一下这两段代码在你的浏览器中运行,看看有什么区别没有。前提是你的页面中至少有一个 div。否则不会进入到循环中去。
运行完之后你会发现,片段一运行完之后页面崩溃了。片段二正常如期运行。为什么呢?因为用 getElementsByTagName 方法获取到的是 HTMLCollection, 而 HTMLCollection 是实时获取的 ,也就是说,每次循环时请求 all.length,都会重新去触发一次 getElementsByTagName 方法。导致 all.length 每次循环之后都会加 1。最终造成了死循环。而用 querySelectorAll 方法获取到的结果是 NodeList,NodeList 不是实时获取的 。也正是因为这个原因,我们推荐使用 querySelector 去代替 getElementBy 方法。实时获取会造成页面不断重排,这极大的降低了性能。另外:除了 getElementByXXX 方法外,document.images/document.links 等方法返回的也是 HTMLCollection。


问题三:如何获取一个元素的 css 属性值?

我们知道,直接用 div.style.color 这样的方式去获取 css 样式只能获取到行内写的属性,写在 style 标签中的样式是获取不到的。标准浏览器中有一个 window.getComputedStyle 方法,可以获取实时的样式(在 IE 中是 currentStyle)。但是这个方法也会引起页面的重排,因为只有把页面重排之后他才会获取到实时的样式。


问题四:如何减少重排和重绘次数。

在改变 DOM 的一些样式时,重排和重绘是无法避免的,但是我们要尽量让这个次数降低,下面介绍集中方法。
1. 当需要给一个 DOM 元素加各种动画或者改变一系列样式时,先把这个元素设置成绝对定位。因为脱离文档流后,再对这个元素进行操作就不会影响到他周围的元素,也就不会发生重排。等到一系列复杂的操作结束之后,再把这个元素的定位恢复到之前的样子。这样只进行了两次重排,能明显的提高性能。
2. 当我们需要改变很多样式时,不要用 style.color,style.width,style.height 这样一个一个去改,因为每次修改都会引起一次重排。我们应该使用给这个元素加一个类名,把所有的样式都写到这个类名中。或者是改 style.cssText。这样可以有效的减少重排的次数。
3. 用 fragment 代替 dom。思考这样一个问题,如何给页面中插入 10000 个 div 元素。每次创建一个然后 append 到 body 中去吗?这显然不是一个好的办法。我们应该先把所有创建的元素都添加到一个 fragment 中。然后再把 fragment 添加到 body 中去。这样只会引起一次重排,而不是 1 万次。
4. 介绍一些会引起重排的属性。当访问元素的以下属性时也会引起重排,因为这些属性是实时算的。

  • offsetTop,offsetLeft,offsetWidth,offsetHeight
  • scrollTop,scrollLeft,scrollWidth,scrollHeight
  • clientTop,clientLeft,clientWidth,clientHeight

5. 尽可能少的访问 DOM。看下面两段代码。

// 片段一
console.time('test1')
for(let i = 0; i < 1000; i++) {document.getElementById('div1').innerHTML+= i;
}
console.timeEnd('test1'); // 打印结果 80ms 左右

// 片段二
console.time('test3')
let count = ''
for(let i = 0; i < 1000; i++) {count += i;}
document.getElementById('div1').innerHTML+= count;
console.timeEnd('test3'); // 打印结果 0.3ms 左右 

一个是不停的访问 DOM,并修改内容,一个是先把内容缓存起来,只访问一次。打印出来时间相差好几百倍。


最后:说几个可以提高效率和性能的小细节。

用正确的 DOM 属性。

// 因为我们大部分时候需要操作的是元素节点,像什么文本节点,注释节点一般都会过滤掉。如果是这样的话,提倡用前面的属性代替后面的属性。// children childNodes
// childElementCount childNodes.length
// firstElementChild firstChild
// lastElementChild lastChild
// nextElementSibling nextSibling
// previousElementSibling previousSibling

需要给大量元素分发事件时,采取事件委托。因为浏览器不仅不需要分发很多事件,而且需要跟踪每个事件处理器,这也会占用更多内存。


总结:

本篇主要分享和 DOM 相关的提高性能的技巧,后续会更新其他方面关于提高页面性能的技巧。想第一时间获得通知就来关注我吧。觉得学到了新知识?点个赞再走呗!

退出移动版