你知道浏览器是怎么解析html代码的吗?

696 阅读6分钟

HTML解析器 HTML Parser

说明

尝试回答几个问题:

  1. 一个htm网页必须包含什么标签?

  2. 如果这样声明标签会发生什么?

    <table>
        <table>
            <tr><td>inner table</td></tr>
        </table>
        <tr><td>outer table</td></tr>
    </table>
    
  3. 浏览器是怎么构建一颗dom树的?

  4. 为什么即使html代码不符合规范也不会报错?

HTML定义

W3C组织制定规范定义了HTML的词汇表和语法。Html有一个正式的格式定义——DTD(Document Type Definition文档类型定义)——但它并不是上下文无关文法,html更接近于xml,现在有很多可用的xml解析器,html有个xml的变体——xhtml,它们间的不同在于,html更宽容,它允许忽略一些特定标签,有时可以省略开始或结束标签。总的来说,它是一种soft语法,不像xml呆板、固执。

显然,这个看起来很小的差异却带来了很大的不同。一方面,这是html流行的原因——它的宽容使web开发人员的工作更加轻松,但另一方面,这也使很难去写一个格式化的文法。所以,html的解析并不简单,它既不能用传统的解析器解析,也不能用xml解析器解析。

DOM

HTML Parser解析出来的树,也就是解析树,也叫dom树,由DOM元素及属性节点组成。DOM是文档对象模型的缩写,它是html文档的对象表示,作为html元素的外部接口供js等调用。

dom树的根是“document”对象,dom元素和标签是一一对应的关系。例如:

<html>
    <body>
        <p>
            Hello DOM
        </p>
        <div><img src=”example.png” /></div>
    </body>
</html>

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

image.png

html解析算法

Html5规范中描述了这个解析算法,算法包括两个阶段——符号化及构建树。

符号化是词法分析的过程,将输入解析为符号,html的符号包括开始标签、结束标签、属性名及属性值。符号识别器识别出符号后,将其传递给树构建器,并读取下一个字符,以识别下一个符号,这样直到处理完所有输入。

image.png

符号识别算法

算法输出html符号,该算法用状态机表示。每次读取输入流中的一个或多个字符,并根据这些字符转移到下一个状态,当前的符号状态及构建树状态共同影响结果,这意味着,读取同样的字符,可能因为当前状态的不同,得到不同的结果以进入下一个正确的状态。

举个例子:

<html>
    <body>
        Hello world
    </body>
</html>
  1. 初始状态为“Data State”,当遇到“<”字符,状态变为“Tag open state”,读取一个a-z的字符将产生一个开始标签符号,状态相应变为“Tag name state”,一直保持这个状态直到读取到“>”,每个字符都附加到这个符号名上,例子中创建的是一个html符号。
  2. 当读取到“>”,当前的符号就完成了,此时,状态回到“Data state”,“”重复这一处理过程。到这里,html和body标签都识别出来了。现在,回到“Data state”,读取“Hello world”中的字符“H”将创建并识别出一个字符符号,这里会为“Hello world”中的每个字符生成一个字符符号。
  3. 这样直到遇到“”中的“<”。现在,又回到了“Tag open state”,读取下一个字符“/”将创建一个闭合标签符号,并且状态转移到“Tag name state”,还是保持这一状态,直到遇到“>”。然后,产生一个新的标签符号并回到“Data state”。后面的“”将和“”一样处理。

image.png

构建树的算法

在树的构建阶段,将修改以Document为根的DOM树,将元素附加到树上。每个由符号识别器识别生成的节点将会被树构造器进行处理,规范中定义了每个符号相对应的Dom元素,对应的Dom元素将会被创建。这些元素除了会被添加到Dom树上,还将被添加到开放元素堆栈中。这个堆栈用来纠正嵌套的未匹配和未闭合标签,这个算法也是用状态机来描述,所有的状态采用插入模式。

举个例子:

<html>
   <body>
        <span style="white-space:pre">  </span>
        Hello world
   </body>
</html>

将符号识别阶段生成的符号序列构建一颗树。

  1. 首先是“initial mode”,接收到html符号后将转换为“before html”模式,在这个模式中对这个符号进行再处理。此时,创建了一个HTMLHtmlElement元素,并将其附加到根Document对象上。

  2. 状态此时变为“before head”,接收到body符号时,即使这里没有head符号,也将自动创建一个HTMLHeadElement元素并附加到树上。

  3. 现在,转到“in head”模式,然后是“after head”。到这里,body符号会被再次处理,将创建一个HTMLBodyElement并插入到树中,同时,转移到“in body”模式。

  4. 然后,接收到字符串“Hello world”的字符符号,第一个字符将导致创建并插入一个text节点,其他字符将附加到该节点。

  5. 接收到body结束符号时,转移到“after body”模式,接着接收到html结束符号,这个符号意味着转移到了“after after body”模式,当接收到文件结束符时,整个解析过程结束。

解析结束

在这个阶段,浏览器将文档标记为可交互的,并开始解析处于延时模式(defer)中的脚本——这些脚本在文档解析后执行。

 文档状态将被设置为完成,同时触发DOMContentLoaded事件。

浏览器容错

因为有浏览器容错的存在,即使我们写了不符合规范的html代码,浏览器也不会报错,浏览器会修复无效内容并继续工作。

举个例子:

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

这段代码明显不符合规范,但是浏览器还是会在解析的过程中修复html代码出现的错误。这段代码将被浏览器修复为:

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

浏览器对不符合规范的html代码的一些基本处理:

  1. 在未闭合的标签中添加明确禁止的元素。这种情况下,应该先将前一标签闭合。
  2. 不能直接添加元素。有些人在写文档的时候会忘了中间一些标签(或者中间标签是可选的),比如HTML HEAD BODY TR TD LI等。
  3. 想在一个行内元素中添加块状元素。关闭所有的行内元素,直到下一个更高的块状元素。
  4. 如果这些都不行,就闭合当前标签直到可以添加该元素。

举个例子:

将一个表格嵌套在另一个表格中,但不在它的某个单元格内。

<table>
    <table>
        <tr><td>inner table</td></tr>
    </table>
    <tr><td>outer table</td></tr>
</table>

webkit将会将嵌套的表格变为两个兄弟表格;webkit使用堆栈存放当前的元素内容,它将从外部表格的堆栈中弹出内部的表格,则它们变为了兄弟表格。

<table>
    <tr><td>outer table</td></tr>
</table>
<table>
    <tr><td>inner table</td></tr>
</table>

总结

务必要写符合W3C规范的html代码,dom结构的层次不要太深、dom元素数量不要太多,否者都会影响html的解析速度。减少dom