浏览器下载完页面所有的资源后,就要开始构建DOM树,与此同时还会构建渲染树(Render Tree)。(其实在构建渲染树之前,和DOM树同期会构建Style Tree。DOM树与Style Tree合并为渲染树)
浏览器下载完成所有的资源后,开始构建DOM树和Style树,DOM树和Style树合并为渲染树。
DOM树:表示页面的结构
渲染树:表示页面节点如何显示
一旦渲染树构建完成,就要开始绘制(paint)页面元素了。
当DOM的变化引发了元素几何属性的变化,比如改变元素的宽高,元素的位置,导致浏览器不得不重新计算元素的几何属性,并重新构建渲染树,这个过程称为“重排”。完成重排后,要将重新构建的渲染树渲染到屏幕上,这个过程就是“重绘”。
简单的说,重排负责元素的几何属性更新,重绘负责元素的样式更新,而且,重排必然带来重绘,但是重绘未必带来重排。比如,改变某个元素的背景,这个就不涉及元素的几何属性,所以只发生重绘。
上面已经提到了,重排发生的根本原理就是元素的几何属性发生了改变,那么我们就从能够改变元素几何属性的角度入手
var el = document.querySelector('.el');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';
这个例子其实和上面那个例子是一回事儿,在最糟糕的情况下,会触发浏览器三次重排。更高效的方式就是合并所有的改变一次处理。这样就只会修改DOM节点一次,比如改为使用cssText属性实现:
var el = document.querySelector('.el');
el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px';
但cssText也有个缺点,会覆盖之前的样式。如
<div style="color:red;">TESTdiv>
// 想给该div在添加个css属性width
div.style.cssText = "width:200px;";
// 这时虽然width应用上了,但之前的color被覆盖丢失了。因此使用cssText时应该采用叠加的方式以保留原有的样式。
function setStyle(el, strCss){
el.style.cssText = el.style.cssText + strCss;
}
沿着这个思路,直接切换类名。没错,还有一种减少重排的方法就是切换类名,而不是使用内联样式的cssText方法。使用切换类名就变成了这样:
// css
.active {
padding: 5px;
border-left: 1px;
border-right: 2px;
}
// javascript
var el = document.querySelector('.el');
el.className = 'active';
批量修改DOM元素的核心思想是:
这个过程引发俩次重排,第一步和第三步,如果没有这两步,可以想象一下,第二步每次对DOM的增删都会引发一次重排。
<ul class="ul_box">
<li><a href="https://www.baidu.com/">百度a>li>
<li><a href="https://map.baidu.com/">百度地图a>li>
ul>
// 把以下数据插入到上面的节点中
const data = [
{
name: '百度翻译',
url: 'https://fanyi.baidu.com/'
},
{
name: '百度知道',
url: 'https://zhidao.baidu.com/'
}
]
// 封装的插入节点方法
appendNode(node,data){
data.forEach(item => {
const li = document.createElement('li');
const a = document.createElement('a');
a.href = item.url
const textNode = document.createTextNode(item.name);//createTextNode() 方法创建文本节点,该方法返回 Text 对象。
a.appendChild(textNode)
li.appendChild(a)
node.appendChild(li)
});
},
// 不考虑优化,每次插入新的节点都会造成一次重排
const ul_box = document.getElementsByClassName('ul_box')[0]
this.appendNode(ul_box,data)
const ul_box = document.getElementsByClassName('ul_box')[0]
ul_box.style.display = 'none';// 隐藏元素
this.appendNode(ul_box,data); // 进行修改
ul_box.style.display = 'block';// 显示元素
这种方法造成俩次重排,分别是控制元素的显示与隐藏。对于复杂的,数量巨大的节点段落可以考虑这种方法。为啥使用display属性呢,因为display为none的时候,元素就不在文档流
const ul_box = document.getElementsByClassName('ul_box')[0]
let fragment = document.createDocumentFragment();
this.appendNode(fragment, data);
ul_box.appendChild(fragment);// 一次重排
文档片段是一个轻量级的document对象,它设计的目的就是用于更新,移动节点之类的任务,而且文档片段还有一个好处就是,当向一个节点添加文档片段时,添加的是文档片段的子节点群,自身不会被添加进去。不同于第一种方法,这个方法并不会使元素短暂消失造成逻辑问题。上面这个例子,只在添加文档片段的时候涉及到了一次重排。
const old = document.getElementsByClassName('ul_box')[0]
const clone = old.cloneNode(true);//clone节点,true为clone后代节点,否则只是clone当前节点只有
this.appendNode(clone, data);// 操作clone的节点
old.parentNode.replaceChild(clone,old);// 获取老节点的父节点,替换原有子节点,只有一次重排,parentNode 属性以 Node对象的形式返回指定节点的父节点,如果指定节点没有父节点,则返回 null。replaceChild 实现子节点(对象)的替换。返回被替换对象的引用。replaceChild(new,old)
cloneNode
cloneNode(true);true为clone后代节点,否则只是clone当前节点。
// 内联事件处理函数,将会被绑定到克隆所得的新节点上
<div style='width: 100px;height: 200px;background:pink' onclick="divClick()">aaadiv>
<script>
// 内联事件处理函数,将会被绑定到克隆所得的新节点上
function divClick(){
console.log('aaaa');
}
const div = document.getElementsByTagName('div')[0]
// 元素属性: 事件将不会被绑定到被克隆的节点上
div.onclick = function (){
console.log('bbb');
}
//addEventListener函数: 事件将不会被绑定到克隆所得的新节点上
div.addEventListener('click',function(){
console.log('cccc');
})
console.log('div',div);
const cloneDiv = div.cloneNode(true);
console.log('cloneDiv',cloneDiv);
document.body.appendChild(cloneDiv);
script>
Vue中通过v-on或其语法糖@指令来给元素绑定事件并且提供了事件修饰符,基本流程是进行模板编译生成AST,生成render函数后并执行得到VNode,VNode生成真实DOM节点或者组件时候使用addEventListener方法进行事件绑定。
所以在vue中cloneNode(true),不会clone事件
缓存布局信息这个概念,在《高性能JavaScript》DOM性能优化中,多次提到类似的思想.比如我现在要得到页面ul节点下面的100个li节点,最好的办法就是第一次获取后就保存起来,减少DOM的访问以提升性能,缓存布局信息也是同样的概念。
前面有讲到,当访问诸如offsetLeft,clientTop这种属性时,会冲破浏览器自有的优化————通过队列化修改和批量运行的方法,减少重排/重绘版次。所以我们应该尽量减少对布局信息的查询次数,查询时,将其赋值给局部变量,使用局部变量参与计算。
看以下样例:
将元素div向右下方平移,每次移动1px,起始位置100px, 100px。性能糟糕的代码:
div.style.left = 1 + div.offsetLeft + 'px';
div.style.top = 1 + div.offsetTop + 'px';
这样造成的问题就是,每次都会访问div的offsetLeft,造成浏览器强制刷新渲染队列以获取最新的offsetLeft值。更好的办法就是,将这个值保存下来,避免重复取值
current = div.offsetLeft;
div.style.left = 1 + ++current + 'px';
div.style.top = 1 + ++current + 'px';