前端必须知道的游览器底层原理(重绘和重排以及css解析选择器)!!!

845 阅读6分钟

关于游览器重绘重排以及解析选择器这块,大家可能并不了解.虽然这一块不了解,代码也能敲好.但是知道以后可以帮助我们更好的去敲好代码,避免犯一些不该犯的低级错误.

游览器底层原理

1.游览器是如何解析css选择器的?

在生成渲染数的过程中,渲染引擎会根据选择器提供的信息来遍历DOM数,找到对应的DOM节点然后将样式规则附加到上面. 来看一段样式选择器代码,以及一段要应用样式的HTML:

.mod-nav h3 span {
            font-style: 16px;
        }
<div class="mod-nav">
        <header>
            <h3>
                <span>标题</span>
            </h3>
        </header>
    </div>

    <div>
        <ul>
            <li>项目一</li>
            <li>项目一</li>
            <li>项目一</li>
            <li>项目一</li>
        </ul>
    </div>

渲染引擎是怎样根据以上样式选择器去遍历这个DOM树的呢?是按照从左往右的选择器顺序去匹配,还是从右往左呢?为了更直观的观察,我们先将这颗DOM数先绘制成图

image.png

然后我们来对比一下两种顺序的匹配:

从左往右:.mod-nav => h3 => span

  1. 遍历所有的元素,找到有.mod-nav类的节点
  2. .mod-nav开始遍历所有的子孙节点header,div,h3,ul......遍历所有的后代元素后,知道了,整个子孙后代只有一个h3
  3. 找到h3,还要继续重新遍历h3的所有子孙节点,去找span...

问题:会进行大量属性结构子孙结点的遍历,这是非常消耗成本的!

这在真实页面中一棵DOM树的节点成百上千的情况下,这种便利方式的效率会非常的低,根本不适合采用.

从左往右:span => h3 => .mod-nav

  1. 先找到所有的span节点,然后基于每一个span再向上查找h3
  2. h3再向上查找.mod-nav的节点
  3. 最后触及根元素html结束该分支遍历...

从右向左的匹配规则,只有第一次会遍历所有元素找节点,而剩下的就是在看父辈祖辈是否满足选择器的条件,匹配效率大大提升!

因此,游览器遵循 "从右往左" 的规则来解析css选择器!

2.浏览器是如何进行界面渲染的?

不同的渲染引擎的具体做法稍有差异,但是大体流程都是差不多的,下面以chrome渲染引擎的渲染流程来说明:

image.png 上图展示的流程是:

  1. 获取HTML文件并进行解析,生成一棵DOM树(DOM Tree)
  2. 解析HTML的同时也会解析css,生成样式规则(Style Rules)
  3. 根据DOM树和样式规则,生成一棵渲染树(Render Tree)
  4. 进行布局(Layout)(重排),即为每个节点分配一个在屏幕上应显示的确切坐标位置
  5. 进行绘制(Paint)(重绘),遍历渲染树节点,调用GPU(图形处理器)将元素呈现出来

3.重绘(repaint)和重排(回流reflow)是什么?

重排

重排是指部分或整个渲染树需要重新分析,并且节点的尺寸需要重新计算。

表现为重新生成布局,重新排列元素。

重绘

重绘是由于节点的几何属性发生改变,或由于样式发生改变(例如:改变元素背景色)。

表现为某些元素的外观被改变。

两者的关系

重绘不一定会出现重排,重排必定会触发重绘。每个页面至少需要一次回流+重绘。(初始化渲染)

重排和重绘的代价都很高昂,频繁重排重绘,会破坏用户体验、让界面显示变迟缓。我们需要尽可能避免频繁触发重排和重绘,尤其是重排

4.何时会触发重排?

重排什么时候发生?

  1. 添加或者删除可见的DOM元素;
  2. 元素位置改变;
  3. 元素尺寸改变——边距、填充、边框、宽度和高度
  4. 内容改变——比如文本改变或者图片大小改变而引起的计算值宽度和高度改变;
  5. 页面渲染初始化;
  6. 浏览器窗口尺寸改变——resize事件发生时;

5.游览器对重绘重排的优化

思考下述代码的重绘重排过程!

image.png

聪明的浏览器:

从上个实例代码中可以看到几行简单的JS代码就引起了4次重排、6次重绘。

而且我们也知道重排的花销也不小,如果每句JS操作都去重排重绘的话,浏览器可能就会受不了!所以浏览器会优化这些操作,浏览器会维护1个队列,把所有会引起重排、重绘的操作放入这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会flush队列,进行一个批处理。这样就会让多次的重排、重绘变成了一次重排重绘。

虽然有了浏览器的优化,但有时候我们写的一些代码可能会强制浏览器提前flush队列,这样浏览器的优化可能起不到作用了

比如当你请求向浏览器获取一些样式信息的时候(保证获取结果的准确性),就会让浏览器flush队列

  1. offsetTop, offsetLeft, offsetWidth, offsetHeight
  2. scrollTop/Left/Width/Height
  3. clientTop/Left/Width/Height
  4. 请求了getComputedStyle()
  5. ....

猜一猜,页面效果是什么:

image.png

这里在设置div宽度的时候 打印了div盒子信息和没打印盒子信息的完全是两种不同的展示形态.感兴趣的同学可以在评论区留言

6.重绘重排角度,我们应该如何优化页面渲染性能?

优化页面渲染性能的角度:尽可能减少重绘和重排的次数

主要有几大方式来避免︰

  • 集中修改样式(这样可以尽可能利用浏览器的优化机制,一次重排重绘就完成渲染)
  • 尽量避免在遍历循环中,进行元素offsetTop 等样式值的获取操作
  • 利用transform 实现动画变化效果,去代替left top的变换(轮播图等)
  • 使用文档碎片(DocumentFragment)

文档碎片的理解: documentFragment是一个保存多个元素的容器对象(保存在内存)当更新其中的一个或者多个元素时,页面不会更新。当documentFragment容器中保存的所有元素操作完毕了,只有将其插入到页面中才会更新页面。

下面是两种方法,都是往页面中添加元素:

image.png

第一种方法: 每次更新都会插入的页面中,如果插入的元素数量足够多,就会引起多次重排,这是非常没有必要的

第二种方法: 使用文档碎片,统一添加.不管插入多少子元素,都只可能触发一次重排和重绘

(vue底层源码,对于DOM的操作,就运用了大量的文档碎片)

结语

关于游览器底层原理这些,暂时就说到这了.有更详细的我会补充说明.如果有不足的,欢迎大家评论区指正.