优化DOM得从重绘和重排讲起,long long ago...
重绘是指一些样式的修改,元素的位置和大小都没有改变;
重排是指元素的位置或尺寸发生了变化,浏览器需要重新计算渲染树,而新的渲染树建立后,浏览器会重新绘制受影响的元素。
去参加面试总会被问到一个问题,那就是“向浏览器输入一行url会发生什么?”,这个问题的答案除了要回答网络方面的知识还牵扯到浏览器渲染页面问题。当我们的浏览器接收到从服务器响应的页面之后便开始逐行渲染,遇到css的时候会异步的去计算属性值,再继续向下解析dom解析完毕之后形成一颗DOM树,将异步计算好的样式(样式盒子)与DOM树相结合便成为了一个Render树,再由浏览器绘制在页面上。DOM树与Render树的区别在于:样式为display:none;的节点会在DOM树中而不在渲染树中。浏览器绘制了之后便开始解析js文件,根据js来确定是否重绘和重排。
产生重绘的因素:
产生重排的因素:
offsetTop
、offsetLeft
、 offsetWidth
、offsetHeight
、scrollTop
、scrollLeft
、scrollWidth
、scrollHeight
、 clientTop
、clientLeft
、clientWidth
、clientHeight
、getComputedStyle()
。总之你要知道,js是单线程的,重绘和重排会阻塞用户的操作以及影响网页的性能,当一个页面发生了多次重绘和重排比如写一个定时器每500ms改变页面元素的宽高,那么这个页面可能会变得越来越卡顿,我们要尽可能的减少重绘和重排。那么我们对于DOM的优化也是基于这个开始。
减少访问次数自然是想到缓存元素,但是要注意
var ele = document.getElementById('ele');
这样并不是对ele进行缓存,每一次调用ele还是相当于访问了一次id为ele的节点。
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。
获取元素最常见的有两种方法,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
对静态集合的操作不会引起对文档的重新查询,相比于动态集合更加优化。
// 优化前 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元素,而优化后的代码只访问了一次,大大的提高了效率。
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。
我们想改变一个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';
这样就达到了一次操作多个样式
上面代码的情况是针对于一个dom节点的,如果我们要改变一个dom集合的样式呢?
第一时间想到的方法是遍历集合,给每个节点加一个className。再想想这样岂不是访问了多次dom节点?想想文章开头说的dom树和渲染树的区别,如果一个节点的display属性为none那么这个节点不会存在于render树中,意味着对这个节点的操作也不会影响render树进而不会引起重绘和重排,基于这个思路我们可以实现优化:
// 假设增加的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';
如果以后看到其他优化方案我会更新,欢迎大家与我交流。
参考文档:
如对本文有疑问, 点击进行留言回复!!
vue源码实战render.js与$nextTick的异步调用
同事牛逼啊,写了个隐藏 bug,我排查了 3 天才解决问题!
【JavaScript笔记(一)】万丈高楼平地起 - 基本概念篇
网友评论