HTML解析器 HTML Parser
说明
尝试回答几个问题:
-
一个htm网页必须包含什么标签?
-
如果这样声明标签会发生什么?
<table> <table> <tr><td>inner table</td></tr> </table> <tr><td>outer table</td></tr> </table> -
浏览器是怎么构建一颗dom树的?
-
为什么即使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树:
html解析算法
Html5规范中描述了这个解析算法,算法包括两个阶段——符号化及构建树。
符号化是词法分析的过程,将输入解析为符号,html的符号包括开始标签、结束标签、属性名及属性值。符号识别器识别出符号后,将其传递给树构建器,并读取下一个字符,以识别下一个符号,这样直到处理完所有输入。
符号识别算法
算法输出html符号,该算法用状态机表示。每次读取输入流中的一个或多个字符,并根据这些字符转移到下一个状态,当前的符号状态及构建树状态共同影响结果,这意味着,读取同样的字符,可能因为当前状态的不同,得到不同的结果以进入下一个正确的状态。
举个例子:
<html>
<body>
Hello world
</body>
</html>
- 初始状态为“Data State”,当遇到“<”字符,状态变为“Tag open state”,读取一个a-z的字符将产生一个开始标签符号,状态相应变为“Tag name state”,一直保持这个状态直到读取到“>”,每个字符都附加到这个符号名上,例子中创建的是一个html符号。
- 当读取到“>”,当前的符号就完成了,此时,状态回到“Data state”,“”重复这一处理过程。到这里,html和body标签都识别出来了。现在,回到“Data state”,读取“Hello world”中的字符“H”将创建并识别出一个字符符号,这里会为“Hello world”中的每个字符生成一个字符符号。
- 这样直到遇到“”中的“<”。现在,又回到了“Tag open state”,读取下一个字符“/”将创建一个闭合标签符号,并且状态转移到“Tag name state”,还是保持这一状态,直到遇到“>”。然后,产生一个新的标签符号并回到“Data state”。后面的“”将和“”一样处理。
构建树的算法
在树的构建阶段,将修改以Document为根的DOM树,将元素附加到树上。每个由符号识别器识别生成的节点将会被树构造器进行处理,规范中定义了每个符号相对应的Dom元素,对应的Dom元素将会被创建。这些元素除了会被添加到Dom树上,还将被添加到开放元素堆栈中。这个堆栈用来纠正嵌套的未匹配和未闭合标签,这个算法也是用状态机来描述,所有的状态采用插入模式。
举个例子:
<html>
<body>
<span style="white-space:pre"> </span>
Hello world
</body>
</html>
将符号识别阶段生成的符号序列构建一颗树。
-
首先是“initial mode”,接收到html符号后将转换为“before html”模式,在这个模式中对这个符号进行再处理。此时,创建了一个HTMLHtmlElement元素,并将其附加到根Document对象上。
-
状态此时变为“before head”,接收到body符号时,即使这里没有head符号,也将自动创建一个HTMLHeadElement元素并附加到树上。
-
现在,转到“in head”模式,然后是“after head”。到这里,body符号会被再次处理,将创建一个HTMLBodyElement并插入到树中,同时,转移到“in body”模式。
-
然后,接收到字符串“Hello world”的字符符号,第一个字符将导致创建并插入一个text节点,其他字符将附加到该节点。
-
接收到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代码的一些基本处理:
- 在未闭合的标签中添加明确禁止的元素。这种情况下,应该先将前一标签闭合。
- 不能直接添加元素。有些人在写文档的时候会忘了中间一些标签(或者中间标签是可选的),比如HTML HEAD BODY TR TD LI等。
- 想在一个行内元素中添加块状元素。关闭所有的行内元素,直到下一个更高的块状元素。
- 如果这些都不行,就闭合当前标签直到可以添加该元素。
举个例子:
将一个表格嵌套在另一个表格中,但不在它的某个单元格内。
<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