游览器工作原理:HTML的渲染

我们已经熟悉JavaScript提供的一套Dom模型和接口来操作网页元素。这篇文章主要解释作为游览器核心的dom部分如何被表示、渲染和处理。

DOM模型

DOM 是文档对象模型 (Document Object Model) 的缩写。它是 HTML 文档的对象表示,同时也是外部内容(例如 JavaScript)与 HTML 元素之间的接口。
解析树的根节点是“Document”对象。HTML中的Tag也是一种节点,称为元素节点;此外,主要的节点还有属性节点、注释节点等。

DOM 与节点之间几乎是一一对应的关系。比如下面这段HTML:

<html>
  <body>
    <p>
      Hello World
    </p>
    <div> <img src="example.png"/></div>
  </body>
</html>

被转移为DOM树的结果是:

                  +---------------------+
                  |   HTMLHtmlElement   |
                  +---------+-----------+
                            |
                            |
                            |
                  +---------+-----------+
                  |   HTMLBodyElement   |
                  +---------------------+
                            X
                           X X
                          X   X
                         X     X
+------------------------+      +--------------------+
|  HTMLParagraphElement  |      |   HTMLDivElement   |
+-------------+----------+      +----------+---------+
              |                            |
              |                            |
              |                            |
        +-----+----+            +----------+---------+
        |   Text   |            |  HTMLImageElement  |
        +----------+            +--------------------+

HTML解释器

游览器获取到HTML资源后的整体处理过程如下:

词法分析

词法解析主要是将字符流转换为词语(tokens)。在webkit内部有一个HTMLTokenizer的类完成词法分析的工作。主要提供一个nextToken方法,这个方法非常复杂,简单描述一下:

这个算法相对于一个状态机。每一个状态接收来自输入信息流的一个或多个字符,并根据这些字符更新下一个状态。当前的标记化状态和树结构状态会影响进入下一状态的决定。这意味着,即使接收的字符相同,对于下一个正确的状态也会产生不同的结果,具体取决于当前的状态。最终输出结果是 HTML Token。

对于以下的一段HTML代码:

<html>
  <body>
    Hello world
  </body>
</html>

1.初始状态是数据状态

2.遇到字符 < 时,状态更改为标记打开状态

3.接收一个 a-z 字符会创建起始标记,状态更改为标记名称状态。这个状态会一直保持到接收 > 字符。在此期间接收的每个字符都会附加到新的标记名称上。

4.遇到 > 标记时,会发送当前的标记,状态改回数据状态<body> 标记也会进行同样的处理

5.接收到 Hello world 中的 H 字符时,将创建并发送字符标记,直到接收 </body> 中的 <。我们将为 Hello world 中的每个字符都发送一个字符标记。

6.接收下一个输入字符 / 时,会创建 end tag token 并改为标记名称状态。我们会再次保持这个状态,直到接收 >。然后将发送新的标记,并回到数据状态</html> 输入也会进行同样的处理。

语法分析

在创建解析器的同时,也会创建 Document 对象。在树构建阶段,以 Document 为根节点的 DOM 树也会不断进行修改,向其中添加各种元素。标记生成器发送的每个节点都会由树构建器进行处理。规范中定义了每个标记所对应的 DOM 元素,这些元素会在接收到相应的标记时创建。这些元素不仅会添加到 DOM 树中,还会添加到开放元素的堆栈中。此堆栈用于纠正嵌套错误和处理未关闭的标记。其算法也可以用状态机来描述。这些状态称为插入模式

然后状态将改为before head。此时我们接收body标签。即使我们的示例中没有head标记,系统也会隐式创建一个 HTMLHeadElement,并将其添加到树中。

现在进入了in head模式,然后转入after head模式。系统对 body 标记进行重新处理,创建并插入 HTMLBodyElement,同时模式转变为in body

现在,接收由Hello world 字符串生成的一系列字符标记。接收第一个字符时会创建并插入Text节点,而其他字符也将附加到该节点。

接收 body 结束标记会触发after body模式。现在我们将接收 HTML 结束标记,然后进入after after body模式。接收到文件结束标记后,解析过程就此结束。

浏览器的容错机制

在早些年写html的过程中,我经常纳闷为什么我写的很多并不规范,也被正常解析,例如只写了一个<br></br>,用错了标准的标记等等。HTML 网页时从来不会有语法无效的错误。这是因为浏览器会纠正任何无效内容,然后继续工作。

例如以下HTML代码:

<html>
  <mytag>
  </mytag>
  <div>
  <p>
  </div>
    Really lousy HTML
  </p>
</html>

这段代码已经违反了很多语法规则:“mytag”不是标准的标记,“p”和“div”元素之间的嵌套有误等等。但是因为容错机制的存在,浏览器仍然会正确地显示这些内容。

解析器对标记化输入内容进行解析,以构建文档树。如果文档的格式正确,就直接进行解析。

遗憾的是,我们不得不处理很多格式错误的 HTML 文档,所以解析器必须具备一定的容错性。

我们至少要能够处理以下错误情况:

1.明显不能在某些外部标记中添加的元素。在此情况下,我们应该关闭所有标记,直到出现禁止添加的元素,然后再加入该元素。

2.我们不能直接添加的元素。这很可能是网页作者忘记添加了其中的一些标记(或者其中的标记是可选的)。这些标签可能包括:HTML HEAD BODY TBODY TR TD LI(还有遗漏的吗?)。

3.向 inline 元素内添加 block 元素。关闭所有 inline 元素,直到出现下一个较高级的 block 元素。

4.如果这样仍然无效,可关闭所有元素,直到可以添加元素为止,或者忽略该标记。

线程化的解释器

线程化的解释器就是利用单独的线程来解释 HTML 文档。因为在WebKit中,网络资源的字节流自IO线程传递给渲染线程之后,后面的解释、布局、渲染都工作在该线程。
DOM树必须是单独的线程,但从字节流到tokens的阶段可以由单独的线程去做这个工作,为了提升性能。


参考:
http://www.w3.org/TR/html5/syntax.html#html-parser
https://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/#The_HTML_grammar_definition

发表评论

电子邮件地址不会被公开。 必填项已用*标注

😉😐😡😈🙂😯🙁🙄😛😳😮:mrgreen:😆💡😀👿😥😎😕