JavaScript之优化DOM

优化DOM得从重绘和重排讲起,long long ago...css

 

一、重绘和重排

1.1 重绘和重排是什么

重绘是指一些样式的修改,元素的位置和大小都没有改变;面试

重排是指元素的位置或尺寸发生了变化,浏览器须要从新计算渲染树,而新的渲染树创建后,浏览器会从新绘制受影响的元素。数组

1.2 浏览器渲染页面

去参加面试总会被问到一个问题,那就是“向浏览器输入一行url会发生什么?”,这个问题的答案除了要回答网络方面的知识还牵扯到浏览器渲染页面问题。当咱们的浏览器接收到从服务器响应的页面以后便开始逐行渲染,遇到css的时候会异步的去计算属性值,再继续向下解析dom解析完毕以后造成一颗DOM树,将异步计算好的样式(样式盒子)与DOM树相结合便成为了一个Render树,再由浏览器绘制在页面上。DOM树与Render树的区别在于:样式为display:none;的节点会在DOM树中而不在渲染树中。浏览器绘制了以后便开始解析js文件,根据js来肯定是否重绘和重排。浏览器

1.3 引发重绘和重排的缘由

产生重绘的因素:缓存

  • 改变visibility、outline、背景色等样式属性,并无改变元素大小、位置等。浏览器会根据元素的新属性从新绘制。

产生重排的因素:服务器

 

  • 内容改变
    • 文本改变或图片尺寸改变
  • DOM元素的几何属性的变化
    • 例如改变DOM元素的宽高值时,原渲染树中的相关节点会失效,浏览器会根据变化后的DOM从新排建渲染树中的相关节点。若是父节点的几何属性变化时,还会使其子节点及后续兄弟节点从新计算位置等,形成一系列的重排。
  • DOM树的结构变化
    • 添加DOM节点、修改DOM节点位置及删除某个节点都是对DOM树的更改,会形成页面的重排。浏览器布局是从上到下的过程,修改当前元素不会对其前边已经遍历过的元素形成影响,可是若是在全部的节点前添加一个新的元素,则后续的全部元素都要进行重排。
  • 获取某些属性
    • 除了渲染树的直接变化,当获取一些属性值时,浏览器为取得正确的值也会发生重排,这些属性包括:offsetTopoffsetLeft、 offsetWidthoffsetHeightscrollTopscrollLeftscrollWidthscrollHeight、 clientTopclientLeftclientWidthclientHeightgetComputedStyle()
  • 浏览器窗口尺寸改变
    • 窗口尺寸的改变会影响整个网页内元素的尺寸的改变,即DOM元素的集合属性变化,所以会形成重排。
  • 滚动条的出现(会触发整个页面的重排)

总之你要知道,js是单线程的,重绘和重排会阻塞用户的操做以及影响网页的性能,当一个页面发生了屡次重绘和重排好比写一个定时器每500ms改变页面元素的宽高,那么这个页面可能会变得愈来愈卡顿,咱们要尽量的减小重绘和重排。那么咱们对于DOM的优化也是基于这个开始。网络

 

二、优化  

2.1 减小访问

减小访问次数天然是想到缓存元素,可是要注意app

var ele = document.getElementById('ele');

这样并非对ele进行缓存,每一次调用ele仍是至关于访问了一次id为ele的节点。dom

2.1.1 缓存NodeList

var foods = document.getElementsByClassName('food');

咱们能够用foods[i]来访问第i个class为food的元素,不过这里的foods并非一个数组,而是一个NodeList。NodeList是一个类数组,保存了一些有序的节点并能够经过位置来访问这些节点。NodeList对象是动态的,每一次访问都会运行一次基于文档的查询。因此咱们要尽可能减小访问NodeList的次数,能够考虑将NodeList的值缓存起来。异步

// 优化前
var lis = document.getElementsByTagName('li');

for(var i = 0; i < lis.length; i++) {
     // do something...  
}

// 优化后,将length的值缓存起来就不会每次都去查询length的值
var lis = document.getElementsByTagName('li');

for(var i = 0, len = lis.length; i < len; i++) {
     // do something...  
}

并且因为NodeList是动态变化的,因此若是不缓存可能会引发死循环,好比一边添加元素,一边获取NodeList的length。

2.1.2 改变选择器

获取元素最多见的有两种方法,getElementsByXXX()和queryselectorAll(),这两种选择器区别是很大的,前者是获取动态集合,后者是获取静态集合,举个例子。

// 假设一开始有2个li
var lis = document.getElementsByTagName('li');  // 动态集合
var ul = document.getElementsByTagName('ul')[0];
 
for(var i = 0; i < 3; i++) {
    console.log(lis.length);
    var newLi = document.createElement('li'); 
    ul.appendChild(newLi);
}
// 输出结果:2, 3, 4


var lis = document.querySelector('li');  // 静态集合 
var ul = document.getElementsByTagName('ul')[0];
 
for(var i = 0; i < 3; i++) {
    console.log(lis.length);
    var newLi = document.createElement('li'); 
    ul.appendChild(newLi);
}
// 输出结果:2, 2, 2

对静态集合的操做不会引发对文档的从新查询,相比于动态集合更加优化。

2.1.3 避免没必要要的循环

// 优化前
for(var i = 0; i < 10; i++) {
document.getElementById('ele').innerHTML += 'a';
} 

// 优化后
var str = '';
for(var i = 0; i < 10; i++) {
str += 'a';
}
document.getElementById('ele').innerHTML = str;

优化前的代码访问了10次ele元素,而优化后的代码只访问了一次,大大的提升了效率。

2.1.4 事件委托

js中的事件函数都是对象,若是事件函数过多会占用大量内存,并且绑定事件的DOM元素越多会增长访问dom的次数,对页面的交互就绪时间也会有延迟。因此诞生了事件委托,事件委托是利用了事件冒泡,只指定一个事件处理程序就能够管理某一类型的全部事件。

// 事件委托前
var lis = document.getElementsByTagName('li');
for(var i = 0; i < lis.length; i++) {
   lis[i].onclick = function() {
      console.log(this.innerHTML);
   };  
}    

// 事件委托后
var ul = document.getElementsByTagName('ul')[0];
ul.onclick = function(event) {
   console.log(event.target.innerHTML);
};

事件委托前咱们访问了lis.length次li,而采用事件委托以后咱们只访问了一次ul。

2.2 减小重绘重排

2.2.1 改变一个dom节点的多个样式

咱们想改变一个div元素的宽度和高度,一般作法能够是这样

var div = document.getElementById('div1');
div.style.width = '220px';
div.style.height = '300px';

以上操做改变了元素的两个属性,访问了三次dom,触发两次重排与两次重绘。咱们说过优化是减小访问次数以及减小重绘重排次数,从这个出发点可不能够只访问一次元素以及重排次数下降到1呢?显然是能够的,咱们能够在css里写一个class

/* css
.change {
    width: 220px;
    height: 300px;
}
*/
document.getElementById('div').className = 'change';

这样就达到了一次操做多个样式

2.2.2  批量修改dom节点样式

上面代码的状况是针对于一个dom节点的,若是咱们要改变一个dom集合的样式呢?

第一时间想到的方法是遍历集合,给每一个节点加一个className。再想一想这样岂不是访问了屡次dom节点?想一想文章开头说的dom树和渲染树的区别,若是一个节点的display属性为none那么这个节点不会存在于render树中,意味着对这个节点的操做也不会影响render树进而不会引发重绘和重排,基于这个思路咱们能够实现优化:

  • 将待修改的集合的父元素display: none;
  • 以后遍历修改集合节点
  • 将集合父元素display: block;
// 假设增长的class为.change
var lis = document.getElementsByTagName('li');  
var ul = document.getElementsByTagName('ul')[0];

ul.style.display = 'none';

for(var i = 0; i < lis.length; i++) {
    lis[i].className = 'change';  
}

ul.style.display = 'block';

 

三、总结

  • 减小访问dom的次数
    • 缓存节点属性值
    • 选择器的使用
    • 避免没必要要的循环
    • 事件委托
  • 减小重绘与重排
    • 使用className改变多个样式
    • 使父元素脱离文档流再恢复

若是之后看到其余优化方案我会更新,欢迎你们与我交流。

 

参考文档:

相关文章
相关标签/搜索