# 浏览器渲染流程

渲染引擎首先通过网络获得所请求文档的内容,通常以 8k 分块的方式完成。

在取得内容之后的基本流程:

  1. 解析 Html 构建 DOM 树:渲染引擎开始解析 html 并将标签转化为内容树中的 DOM 节点
  2. 构建 render 树:解析外部 cssstyle 标签中的样式信息,这些样式信息和 html 中的可见性指令将被用来构建 render 树,render 树由包含颜色和大小等属性的矩形组成,它们将被按照正确的顺序显示到屏幕上
  3. 布局 render 树:构建好了以后,将会执行布局过程,将确定每个节点在屏幕上的确切坐标
  4. 绘制 render 树:最后就是绘制,遍历 render 树,并使用 UI 后端层绘制每一个节点
基本流程

为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等所有 html 都解析完成之后再去构建和布局 render 树,它是解析一部分内容就显示一部分内容,同时可能还在通过网络下载其余内容。

不同引擎流程

可以看到,尽管 webkitGecko 使用的术语稍有不同,但是主要流程基本相同,Gecko 称可见的格式化元素组成的树为 frame 树,每个元素都是一个 framewebkit 则使用 render 树来命名由渲染对象组成的树,webkit 中元素的定位称为布局,而 Gecko 中称为回流,webkit 称利用 dom 节点和样式去构建 render 树的过程为 attachmentGeckohtmldom 树之间附加了一层,称为内容接收器,相当于制造 dom 元素的工厂。

# 解析和 DOM 树构建

解析一个文档即将其转换为具有一定意义的结构,解析的结果通常是表达文档结构的节点树,称为解析树或者语法树。

例如,解析 2 + 3 - 1,可能返回这样一棵树:

数学表达书1

文法(Grammars)

解析基于文档依据的语法规则,每种可被解析的格式必须具有由词汇和语法规则组成的特定的文法,称为上下文无关文法。(人类语言不具有这一特性,因此不能被一般的解析技术所解析)

# 解析过程

解析器(Parser)解析分为两个子过程:

  • 语法分析:就是将输入分解为符号
  • 词法分析:对语言应用语法规则

解析器一般先把输入分解为合法的符号,在根据语言的语法规则分析文档结构,从而构建解析树:

从源文档到解析树

解析过程是迭代的,解析器取到一个新的符号,用这个符号去匹配一条语法规则,如果匹配了将对应的节点添加到解析树上,然后解析树继续请求下一个符号,如果没有匹配,解析器将在内部保存该符号,然后取下一个符号,直到所有内部保存的符号能够匹配一项语法规则,如果最终没有找到匹配的规则,解析器将抛出一个异常,这意味着文档无效或者包含语法错误。

转译(Translation)

很多时候,解析树不是最终结果,解析一般在转换中使用,例如编译过程就是先将源码解析为解析树然后将解析树转换为机器码文档:

编译流程

# 解析器类型

  • 自顶向下:查看语法的最高层结构试着匹配其中一个
  • 自底向上:从输出开始逐步将其转换为语法规则,从底层规则开始匹配到高层规则

例子:2 + 3 - 1

自顶向下:先识别出 2+3 视为一个表达式,然后识别出 2+3-1 为一个表达式

自底向上:解析扫描输入直到匹配了一条规则,然后用该规则取代匹配的输入,直到解析完所有输入。部分匹配的表达式被放置在解析堆栈中

输入从左向右移动(一个指针首先指向输入开始处,然后向右移动):

stack input
null 2+3-1
匹配常量 +3-1
常量 运算符 3-1
表达式 -1
表达式 运算符 1
表达式 null

# html 解析

输出的树也就是解析树,是由 DOM 元素和属性节点组成的,树的根是 document 对象。

DOM 和标签基本是一一对应关系,例如下面的标签:

<html>
  <body>
    <p>hello</p>
    <div>
      <img src="1.png" />
    </div>
  </body>
</html>
1
2
3
4
5
6
7
8

将会被转换为下面的 DOM 树:

DOM树

html 不能被一般的自顶向下或自底向上的解析器所解析,因为:

  • 语言本身的宽容特性
  • 浏览器对一些常见的非法语法有容错机制
  • 解析过程是往复的,通常源码不会再解析过程中发生改变,但在 html 中,脚本标签中的内容可能会有修改

浏览器为其定制了专属的解析器,html5 规范中描述了这个算法,包含两个阶段:

  • 符号化:词法分析的过程,将输入解析为符号,包含开始标签、结束标签、属性名和属性值
  • 构建树:符号识别器识别出符号后,将其传递给构建器,然后读取下一个字符,直到所有输入都处理完
html解析流程

# 处理脚本和样式表的顺序

  • 脚本
    • 解析器遇到 <script> 标签的时候,文档停止解析
      • 内部脚本会等待脚本执行完毕
      • 外部脚本会从网络抓取资源完成后继续
    • 可以在 <script> 标签上添加 defer 属性,这样就不会停止文档解析,而是等待解析结束后执行
    • html5 增加了 async 属性,可以将脚本标记为异步,也不会阻塞解析
  • 样式表 理论上样式表不会修改 DOM 树,似乎没有必要等待样式表并停止文档解析,但是脚本可能会请求样式信息,如果这时还没解析样式,那么脚本就会得到错误的信息
    • Firefox 在样式表加载和解析的过程中,会禁止所有脚本
    • WebKit 仅当脚本尝试访问样式属性可能受到尚未加载的样式表影响时,才会禁止该脚本

# 渲染树构建

DOM 树构建完成时,浏览器开始构建渲染树,渲染树由元素显示序列中的可见元素组成,它是文档的可视化表示,构建这颗树是为了以正确的顺序绘制文档内容。

渲染对象和 DOM 元素对应,但不是一一对应,不可见的元素不会被插入渲染树,例如 head 元素,display:nonevisibility:hidden 将会出现在渲染树中)。

还有一些元素对应几个可见对象,一般是具有复杂结构的元素,一些渲染对象和对应的节点不在树上相同的位置,例如浮动和绝对定位的元素在文本流之外,在两棵树上的位置不同,渲染树上标示出真实的结构,并用一个占位结构标示出原来的位置。

创建树的流程:

  • firefox 表述为一个监听 Dom 更新的监听器,通过 Frame Constructor 根据样式创建 frame
  • webkit 通过 attachment 过程将节点插入到树中

# 布局

当渲染对象被创建并添加到树中,它们并没有位置和大小,计算这些值的过程被称为布局(layout)或者回流(reflow)。

html 使用基于流的布局模型,意味着大部分时间,可以以单一的途径进行几何计算。流中靠后的元素并不会影响前面元素的几何特性,所以布局可以在文档中从右向左自上而下的进行。

布局是一个递归的过程,由根渲染对象开始,它对应 html 文档元素,布局继续递归的通过一些或所有 frame 层级,为每个需要几何信息的渲染对象进行计算。

根渲染对象的坐标是 0,0 ,大小是 viewport - 浏览器窗口可见部分

所有渲染对象都有一个 layout 或者 reflow 方法,每个渲染对象调用需要布局的 childrenlayout 方法。

dirty bit

为了不因为每个小变化都全部重新布局,浏览器使用了一个 dirty bit 系统,一个渲染对象发生了变化或是被添加了,就标记它和它的 childrendirty ,所有被标记的渲染对象都需要重新布局

# 过程

布局分为两种情况:

  • 全局:整颗渲染树触发 layout,一般是同步触发
    • 一个全局样式改变影响所有渲染对象,比如字体改变
    • 窗口 resize
  • 增量:只有被标记 dirty 的渲染对象会重新布局,一般是异步,在脚本请求样式信息,比如 offsetHeight 会同步触发增量布局

过程一般为下面几个部分:

  1. parent 渲染对象决定它的宽度
  2. parent 渲染对象读取 children 然后:
  • 放置 child 渲染对象
  • 在需要时调用 child 渲染对象
  • parent 渲染对象使用 child 渲染对象的累积高度,以及 marginpadding 的高度来设置自己的高度
  • dirty 标识设置为 false

# 宽度计算

渲染对象的宽度使用容器的宽度、渲染对象样式中的宽度以及 marginborder 进行计算。

webkit 中宽度的计算过程:

  • 容器的宽度是容器的可用宽度和 0 中的最大值,可用宽度为: contentWidth = clientWidth() - paddingLeft() - paddingRight(),其中 clientWidth 代表一个对象内部的不包括 border 和滑动条的宽度
  • 元素的宽度指样式属性的 width ,它可以通过计算容器的百分比得到一个绝对值
  • 加上水平方向上的 borderpadding

到这里是最佳宽度的计算过程,如果最佳宽度大于最大宽度则使用最大宽度,如果小于最小宽度则使用最小宽度,最后缓存这个值,在需要布局但宽度未变时使用。

# 绘制

在绘制阶段,系统会遍历呈现树,并调用呈现器的 paint 方法,将呈现器的内容显示在屏幕上。绘制工作是使用用户界面基础组件完成的。

  • 全局绘制
  • 增量绘制

绘制顺序(元素进入堆栈样式上下文的顺序,堆栈会从后往前绘制):

  1. 背景颜色
  2. 背景图片
  3. 边框
  4. 子代
  5. 轮廓

在重新绘制之前,WebKit 会将原来的矩形另存为一张位图(Bitmap),然后只绘制新旧矩形之间的差异部分

在发生变化时,浏览器会尽可能做出最小的响应。因此,元素的颜色改变后,只会对该元素进行重绘。元素的位置改变后,只会对该元素及其子元素(可能还有同级元素)进行布局和重绘。添加 DOM 节点后,会对该节点进行布局和重绘。一些重大变化(例如增大元素的字体)会导致缓存无效,使得整个呈现树都会进行重新布局和绘制。