从输入URL到页面呈现发生了什么?(第二篇)

1,005 阅读6分钟

上一篇写了输入URL之后发生的网络请求和网络响应 从输入URL到页面呈现发生了什么?(第一篇),这一篇接着写算法解析。

完成了网络请求和响应之后,如果响应头中Content-Type值是text/html,那么接下来就是浏览器的解析和渲染工作了。

浏览器解析有以下几个步骤:

  • 构建DOM树
  • 样式计算
  • 生成布局树(Layout Tree)

构建DOM树

由于浏览器无法直接HTML字符串,因此将这一系列的字节流,解码成字符流,然后通过词法分析器解释成词语(Tokens),之后经过语法分析器构建成节点,最后这些节点被组建成一颗 DOM 树。

在这个过程中,每一个环节都会调用相应的类去处理:

  • 词法分析: HTMLTokenizer 类
  • 词语验证:XSSAuditor 类
  • 从词语到节点: HTMLDocumentParser 类、 HTMLTreeBuilder 类
  • 从节点到 DOM 树: HTMLConstructionSite 类

qq_pic_merged_1618233872723.jpg

DOM树

在介绍DOM树之前,首先要清楚,在DOM规范中,对于文档的对于文档的表示方法并没有任何限制,因此,DOM 树只是多种文档结构中的一种较为普遍的实现方式。

DOM 结构构成的基本要素是 “节点“,而文档的结构就是由层次化的节点组成。在 DOM 模型中,节点的概念很宽泛,整个文档 (Document) 就是一个节点,称为文档节点。除此之外还有元素(Element)节点、属性节点、Entity节点、注释(Comment)节点等。

了解了 DOM 的结构是由各种的子节点组成的,那么以 HTMLDocument 为根节点,其余节点为子节点,组织成一个树的数据结构的表示就是 DOM树。

qq_pic_merged_1618233828360.jpg

解析算法

HTML5 规范详细地介绍了解析算法。这个算法分为两个阶段:

  1. 标记化。
  2. 建树。

对应的两个过程就是词法分析语法分析

标记化算法:

这个算法输入为HTML文本,输出为HTML标记,也成为标记生成器。其中运用有限自动状态机来完成。即在当当前状态下,接收一个或多个字符,就会更新到下一个状态。

<html>
<body>
    Hello World
</body>
</html>

通过一个简单的例子来演示一下标记化的过程。

遇到<, 状态为标记打开。

接收[a-z]的字符,会进入标记名称状态。

这个状态一直保持,直到遇到>,表示标记名称记录完成,这时候变为数据状态。

接下来遇到body标签做同样的处理。

这个时候html和body的标记都记录好了。

现在来到<body额>(额字为了防止body不显示,实在是不知道怎么弄,求教)中的>,进入数据状态,之后保持这样状态接收后面的字符hello sanyuan。

接着接收 <body额> 中的<,回到标记打开, 接收下一个/后,这时候会创建一个end tag的token。

随后进入标记名称状态, 遇到>回到数据状态。

接着以同样的样式处理 </body额>。

建树算法:

之前提到过,DOM 树是一个以document为根节点的多叉树。因此解析器首先会创建一个document对象。标记生成器会把每个标记的信息发送给建树器。建树器接收到相应的标记时,会创建对应的 DOM 对象。创建这个DOM对象后会做两件事情:

  1. 将DOM对象加入 DOM 树中。
  2. 将对应标记压入存放开放(与闭合标签意思对应)元素的栈中。
<html>
<body>
    Hello World
</body>
</html>

首先,状态为初始化状态。

接收到标记生成器传来的html标签,这时候状态变为before html状态。同时创建一个HTMLHtmlElement的 DOM 元素, 将其加到document根对象上,并进行压栈操作。

接着状态自动变为before head, 此时从标记生成器那边传来body,表示并没有head, 这时候建树器会自动创建一个HTMLHeadElement并将其加入到DOM树中。

现在进入到in head状态, 然后直接跳到after head。

现在标记生成器传来了body标记,创建HTMLBodyElement, 插入到DOM树中,同时压入开放标记栈。

接着状态变为in body,然后来接收后面一系列的字符: Hello sanyuan。接收到第一个字符的时候,会创建一个Text节点并把字符插入其中,然后把Text节点插入到 DOM 树中body元素的下面。随着不断接收后面的字符,这些字符会附在Text节点上。

现在,标记生成器传过来一个body的结束标记,进入到after body状态。

标记生成器最后传过来一个html的结束标记, 进入到after after body的状态,表示解析过程到此结束。

样式计算

关于CSS样式,它的来源一般是三种:

  1. link标签引用
  2. style标签中的样式
  3. 元素的内嵌style属性

格式化样式表

首先,浏览器是无法直接识别 CSS 样式文本的,因此渲染引擎接收到 CSS 文本之后第一件事情就是将其转化为一个结构化的对象,即styleSheets。

在浏览器控制台能够通过document.styleSheets来查看这个最终的结构。当然,这个结构包含了以上三种CSS来源,为后面的样式操作提供了基础。

标准化样式属性

有一些 CSS 样式的数值并不容易被渲染引擎所理解,因此需要在计算样式之前将它们标准化,如em->px,red->#ff0000,bold->700等等。

计算每个节点的具体样式

样式已经被格式化和标准化,接下来就可以计算每个节点的具体样式信息了。

其实计算的方式也并不复杂,主要就是两个规则: 继承和层叠。

每个子节点都会默认继承父节点的样式属性,如果父节点中没有找到,就会采用浏览器默认样式,也叫UserAgent样式。这就是继承规则,非常容易理解。

然后是层叠规则,CSS 最大的特点在于它的层叠性,也就是最终的样式取决于各个属性共同作用的效果,甚至有很多诡异的层叠现象,看过《CSS世界》的同学应该对此深有体会,具体的层叠规则属于深入 CSS 语言的范畴,这里就不过多介绍了。

不过值得注意的是,在计算完样式之后,所有的样式值会被挂在到window.computedStyle当中,也就是可以通过JS来获取计算后的样式,非常方便。

生成布局树

现在已经生成了DOM树和DOM样式,接下来要做的就是通过浏览器的布局系统确定元素的位置,也就是要生成一棵布局树(Layout Tree)。

布局树生成的大致工作如下:

  1. 遍历生成的 DOM 树节点,并把他们添加到布局树中。
  2. 计算布局树节点的坐标位置。 值得注意的是,这棵布局树值包含可见元素,对于 head标签和设置了display: none的元素,将不会被放入其中。