重排(回流)和重绘

什么是重排和重绘

浏览器下载完页面所有的资源后,就要开始构建DOM树,与此同时还会构建渲染树(Render Tree)。(其实在构建渲染树之前,和DOM树同期会构建Style Tree。DOM树与Style Tree合并为渲染树)

浏览器下载完成所有的资源后,开始构建DOM树和Style树,DOM树和Style树合并为渲染树。

DOM树:表示页面的结构

渲染树:表示页面节点如何显示
重排(回流)和重绘_第1张图片
一旦渲染树构建完成,就要开始绘制(paint)页面元素了。

当DOM的变化引发了元素几何属性的变化,比如改变元素的宽高,元素的位置,导致浏览器不得不重新计算元素的几何属性,并重新构建渲染树,这个过程称为“重排”。完成重排后,要将重新构建的渲染树渲染到屏幕上,这个过程就是“重绘”。

简单的说,重排负责元素的几何属性更新,重绘负责元素的样式更新,而且,重排必然带来重绘,但是重绘未必带来重排。比如,改变某个元素的背景,这个就不涉及元素的几何属性,所以只发生重绘。

重排触发机制

上面已经提到了,重排发生的根本原理就是元素的几何属性发生了改变,那么我们就从能够改变元素几何属性的角度入手

  1. 添加或删除可见的DOM元素
  2. 元素位置改变(外边距、position)
  3. 元素本身的尺寸发生改变(内边距、边框厚度、宽高等几何属性)
  4. 内容改变
  5. 页面渲染器初始化
  6. 浏览器窗口大小发生改变

如何进行性能优化,最小化重排和重绘

1.改变样式

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';

2.批量修改DOM

批量修改DOM元素的核心思想是:

  1. 让该元素脱离文档流
  2. 对其进行多重改变
  3. 将元素带回文档中

这个过程引发俩次重排,第一步和第三步,如果没有这两步,可以想象一下,第二步每次对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)
优化1:隐藏元素,进行修改后,然后再显示该元素
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的时候,元素就不在文档流

优化2:使用文档片段创建一个子树,然后再拷贝到文档中
const ul_box = document.getElementsByClassName('ul_box')[0]
let fragment = document.createDocumentFragment();
this.appendNode(fragment, data);
ul_box.appendChild(fragment);// 一次重排

文档片段是一个轻量级的document对象,它设计的目的就是用于更新,移动节点之类的任务,而且文档片段还有一个好处就是,当向一个节点添加文档片段时,添加的是文档片段的子节点群,自身不会被添加进去。不同于第一种方法,这个方法并不会使元素短暂消失造成逻辑问题。上面这个例子,只在添加文档片段的时候涉及到了一次重排。

优化3:将原始元素拷贝到一个独立的节点中,操作这个节点,然后覆盖原始元素
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';

你可能感兴趣的:(整理,前端)