手记

布局与重排

布局

在进行 DOM 节点和样式规则匹配的同时,渲染引擎会为每一个可视节点创建 LayoutObject 对象。LayoutObject 对象保存了该节点所需要的布局信息,比如位置( 包含块模型 )、大小( 盒子模型 )等等。整个计算流程大致可以分为以下 4 步:

  1. 判断该节点是否需要重新布局,如果需要,创建 LayoutObject 对象;
  2. 计算该节点的宽度和垂直方向的外边距,更新 LayoutObject 对象;
  3. 遍历该节点的每一个子节点,依次计算它们的布局;
  4. 根据子节点的布局结果,计算该节点的高度,并最终返回 LayoutObject 对象。

*节点的高度计算,会依据节点类型的不同而采用不同的算法,例如块级元素,定义了自身的宽高,那么就可以据此来确定节点的高度( 期间可能还涉及到溢出时的展示效果 );而内联元素,则需要根据子节点的的大小来确定节点的高度。因此,布局计算通常会采用先确定宽度,再根据子节点的计算结果,确定高度的顺序进行。

渲染引擎的实际布局计算过程,远比我列举的 4 步更为复杂。至于如何做到最高效的计算,这是留给引擎开发者的终极任务,作为前端开发者,简单理解:DOM + CSSOM = LayoutObject

例子:

<style>
    body {
        margin: 0;
        padding: 5px;
    }
    
    div {
        width: 40px;
        height: 40px;
        margin: 5px;
    }
    
    span {
        font-size: 16px;
    }
</style>

<body>
    <div></div>
    <span>hello</span>
</body>

例子中 DOM 和 CSSOM 结合之后,创建出以下 LayoutObject:

body + { width:764px; height: 60px; } = LayoutBlock{ BODY } at( 0,0 ) size( 764*60 )
div + { width:40px; height: 40px; } = LayoutBlock{ DIV } at( 5,5 ) size( 50*50 )
span + { width:37px; height: 20px; } = LayoutInline{ SPAN } at( 5,55 ) size( 37*20 )

*例子中没有设定位置的 CSS 样式,因此全部按照正常流去理解定位。节点大小不够准确,仅供学习。

重排

在布局计算的第一步,就是判断是否需要重新布局。那么,有哪些情况会导致重新布局( 重排 )呢?

首先,网页被初次打开,一定会发生布局计算。

其次,JavaScript 代码通过 DOM、CSSOM 修改节点,致使元素的几何空间发生变化。

最常见的情况有如下几种:

  1. 添加或删除节点;
  2. 节点的位置发生变化( 浮动、定位 );
  3. 节点的大小发生变化( 内容、内外边距和边框宽度 )。

最后,scroll 事件、resize 事件、网页动画( 主要指 JavaScript 动画 )等会造成密集的重排,可能会出现网页卡顿现象。

减少重排的常用方法

重排是比较耗时的,而且,一旦布局发生变化,就会发生重排。我们在实际工作中,可以从以下两个方向,减少重排的发生:

批量修改 DOM

1、使用 display:none 隐藏节点,进行多次修改之后再显示。

例子:

<button>OK</button>

var showBox = (function() {
    var div = document.createElement("div");
    div.innerHTML = "hello";
    div.style.display = "none";
    document.body.appendChild(div);
    return div;
})();

document.querySelector("button").onclick = function() {
    showBox.style.display = "block";
}

2、使用文档片段( DocumentFragment )构建子树,构建完毕之后,添加进 DOM 模型。

例子:

<button>OK</button>

var showList = (function() {
    var ul = document.createElement("ul"),
        docF = document.createDocumentFragment();

    ["A", "B", "C"].forEach(function(v) {
        var li = document.createElement("li");
        li.innerHTML = v;
        docF.appendChild(li);
    });
    ul.appendChild(docF);
    return ul;
})();

document.querySelector("button").onclick = function() {
    document.body.appendChild(showList);
}

3、使用 innerHTML 和 outerHTML 批量替换 HTML 元素。

例子:

<button>OK</button>
<div></div>

var showList = (function() {
    return ul_HTML = "<ul><li>A</li><li>B</li><li>C</li></ul>";
})();

document.querySelector("button").onclick = function() {
    document.querySelector("div").innerHTML = showList;
}

*innerHTML 和 outerHTML 的实现原理,其实也是将字符串经过词法、语法分析之后,通过 DocumentFragment 构建子树,并最终添加进 DOM 模型。

批量修改样式

1、使用 className 批量修改节点的样式。

例子:

<style> 
    p {
        font-size: 20px;
        color: blueviolet;
        border: 1px solid #ccc;
    }
    
    .p_style {
        font-size: 30px;
        color: brown;
        border: 2px solid #ccc;
    }
</style>

<button>OK</button>
<p>hello</p>

<script>
    var change_style = function() {
        var p = document.querySelector("p");
        p.className = "p_style";
    };

    document.querySelector("button").onclick = function() {
        change_style();
    }
</script>

2、另外一个比较不常用的,就是使用 cssText 批量修改样式。

例子:

<style> 
    p {
        font-size: 20px;
        color: blueviolet;
        border: 1px solid #ccc;
    }
</style>

<button>OK</button>
<p>hello</p>

<script>
    var change_style = function() {
        var p = document.querySelector("p");
        p.style.cssText = "font-size: 30px;color: brown;";
    };

    document.querySelector("button").onclick = function() {
        change_style();
    }
</script>

Render 树

经过渲染引擎的处理,LayoutObject 对象保存了 DOM 节点以及其对应的布局信息,LayoutObject 对象已经知道了该如何绘制自己。然后,该对象会带着这些信息,插入到 Render 树上。

Render 树按照 DOM 树的排版规则进行排版,但两者并非一一对应的关系,只有 document 节点和可视节点才会出现在 Render 树中。

Render 树可以看作是渲染引擎为最后绘图,而构建的内部表示对象。但实际情况可能更为复杂。因为 HTML5 的出现,让绘图不再是简单地应对 HTML 元素,还有 2D、3D 图形,多媒体等等。


如有错误,欢迎指正,本人不胜感激。

1人推荐
随时随地看视频
慕课网APP