三. 浏览器内部工作原理-解析方向(一)
前言
为方便读者阅读,贴出以下参考链接,在此感谢前辈笔记。
浏览器解析过程分析:前端必读:浏览器内部工作原理_知识库_博客园 (cnblogs.com)
浏览器工作过程分析:万字详文:深入理解浏览器原理 - 知乎 (zhihu.com)
树的构建流程
HTML 和 CSS 的页面解析是从上之下的,线程的。文档流从上往下,若先碰到了 CSS,那么开始解析,接着不被阻塞的继续向下解析 HTML,这样既符合并行解析,又符合文档流自上往下解析规则。浏览器先下载 HTML 文件开始解析,遇到 CSS 标签就开始下载 CSS 并解析,这个过程不会阻塞 DOM 的构建。最后 DOM 树和 CSS 规则树生成渲染树,HTML 解析完成。
注意几点:
- CSS 加载不会阻塞 DOM 树的解析
- CSS 加载会阻塞 DOM 树的渲染(不阻塞解析),即合并成渲染树的过程,因为样式可能会导致重排重绘
- CSS 加载会阻塞后面 JS 语句的执行
解析、渲染这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的 html 都解析完成之后再去构建和布局 render 树。它是解析完一部分内容就显示一部分内容,同时还可能通过网络下载其余内容。
1.1 HTML Parser
1.1.1 解析
解析的目的是:将文档转换为具有一定意义的结构,编码时可以理解和使用的东西。解析的结果通常是表达文档结构的节点树,称为解析树或语法树。
解析的目的在于让目标更接近于需要使用的形式,或更具体的描述一些信息。如高级语言先转换为汇编等低级语言,最后再转换成机器码,又或者是家具的运输与组装,零件形式更接近于运输模式,成品形式更接近于使用模式,由成品拆分成零件就是使其更接近于运输模式,同样的道理将文档解析成另外一个格式会更适合某一个场景的使用。
如解析“2+3-1”这个表达式,可能返回这样一棵树。
解析过程与翻译过程类似,需要具备两个条件,字典和语法规则。
文法(Grammars):解析基于文档依据的语法规则——文档的语言或格式。每种可被解析的格式必须具有由词汇及语法规则组成的特定的文法,称为上下文无关文法。简单来说就是由词汇表、语法规则组成的对某一个格式的限制,与当前文件内容无关。
文法不等于语法规则,文法是词汇表和语法规则的结合,如语法规则定义为“的字后面跟着表示事物的名词,地字跟在动词后面,得字后面跟着形容词”,词汇表则有“谁,得,的,地,花,走,快”,文法规则就是词汇表和语法规则的结合,包含任何一个可能选择,如“谁的花,走得快,的,得,地”等。
解析可以分成两个子过程:
- 词法分析
- 语法分析
词法分析是将文档中的字词按照一定地规则(词汇表)分成符号,符号对应的便是词汇表(基本有效单元的集合)中的某一项。对于人类语言来说,它相当于我们字典中出现的所有单词。
语法分析是根据语法规则匹配词法分析后的符号,生成特定状态的过程。
词法分析器(有时也叫分词器)负责将输入分解为合法的符号,语法分析器则根据语言的语法规则分析文档结构,从而构建解析树,词法分析器知道怎么跳过空白和换行之类的无关字符。
解析过程是迭代的,语法分析器从词法分析器处取到一个新的符号,并试着用这个符号匹配一条语法规则,如果匹配了一条规则,这个符号对应的节点将被添加到解析树上,然后语法分析器分析下一个符号。如果没有匹配到规则,语法分析器将在内部保存该符号,并从词法分析器取下一个符号,直到所有内部保存的符号能够匹配一项语法规则。如果最终没有找到匹配的规则,解析器将抛出一个异常,这意味着文档无效或是包含语法错误。
词法分析器和语法分析器的工作是同步的,正如组装家具时,找到一个一个零件,根据说明书进行组装,如果没有找齐零件,是无法组装某一个个体的。
1.1.2 转换
转换对工作人员来说是一个隐式的过程,正如在浏览器开发者控制台运行一些代码、打印日志信息一样,开发者可能感受不到转换到机器码的过程。但是编写的源代码并不是最终的目标,底层机器码才是机器识别、运行的格式。
很多时候,解析树并不是最终结果。解析一般在转换中使用——将输入文档转换为另一种格式。编译就是个例子,编译器在将一段源码编译为机器码的时候,先将源码解析为解析树,然后将该树转换为一个机器码文档。
简单来说转换过程在编译运行时就存在,将源代码转换为机器码,转换时是无感的。
详细的例子可以看看:前端必读:浏览器内部工作原理_知识库_博客园 (cnblogs.com),如不熟悉抽象语法树的概念,可以看看:手把手带你入门 AST 抽象语法树 - 掘金 (juejin.cn)。
1.1.3 深入
HTML解析器(HTML Parser):HTML解析器的工作是将html标识解析为解析树。
HTML文法定义(The HTML grammar definition):W3C组织制定规范定义了HTML的词汇表和语法。
非上下文无关文法(Not a context free grammar):正如在解析简介中提到的,上下文无关文法的语法可以用类似BNF的格式来定义。
HTML 有一个正式的格式定义—— DTD(Document Type Definition文档类型定义),但它并不是上下文无关文法,HTML 更接近于 XML,现在有很多可用的xml 解析器,html 有个 xml 的变体——xhtml,不同在于 html 更宽容,它允许忽略一些特定标签,有时可以省略开始或结束标签。总的来说,它是一种 soft 语法,不像 xml 呆板、固执。
显然,这个看起来很小的差异却带来了很大的不同。一方面,这是 html 流行的原因——它的宽容使 web 开发人员的工作更加轻松,但另一方面,这也使很难去写一个格式化的文法。所以,html 的解析并不简单,它既不能用传统的解析器解析,也不能用 xml 解析器解析。
HTML DTD
html 适用 DTD 格式进行定义,这一格式是用于定义 SGML 家族的语言,包括了对所有允许元素及它们的属性和层次关系的定义。正如前面提到的,html DTD 并没有生成一种上下文无关文法。DTD 有一些变种,标准模式只遵守规范,而其他模式则包含了对浏览器过去所使用标签的支持,这么做是为了兼容以前内容。最新的标准 DTD 在http://www.w3.org/TR/html4/strict.dtd。
DOM
解析输出的树也就是解析树,是由 DOM 元素及属性节点组成的。DOM 是文档对象模型的缩写,它是 html 文档的对象表示,作为 html 元素的外部接口供 JS 等调用。DOM 和标签基本是一一对应的关系。
解析器类型(Types of parsers)
有两种基本的解析器——自顶向下解析及自底向上解析。比较直观的解释是,自顶向下解析,查看语法的最高层结构并试着匹配其中一个;自底向上解析则从输入开始,逐步将其转换为语法规则,从底层规则开始直到匹配高层规则。
来看一下这两种解析器如何解析 “2+3-1” 的例子:自顶向下解析器从最高层规则开始——它先识别出“2+3”,将其视为一个表达式,然后识别出 “2+3-1” 为一个表达式(识别表达式的过程中匹配了其他规则,但出发点是最高层规则)。自底向上解析会扫描输入直到匹配了一条规则,然后用该规则取代匹配的输入,直到解析完所有输入。部分匹配的表达式被放置在解析堆栈中。
自顶向下与递归过程类似,从整体开始逐步检查子问题,而自底向上与递推过程类似,从子问题开始推向整体。
自动化解析(Generating parsers automatically)
解析器生成器这个工具可以自动生成解析器,只需要指定语言的文法——词汇表及语法规则,它就可以生成一个解析器。创建一个解析器需要对解析有深入的理解,而且手动的创建一个由较好性能的解析器并不容易,所以解析生成器很有用。Webkit使用两个知名的解析生成器——用于创建语法分析器的Flex及创建解析器的Bison(你可能接触过Lex和Yacc)。Flex的输入是一个包含了符号定义的正则表达式,Bison的输入是用BNF格式表示的语法规则。
稍微总结:HTML 解析器按照 HTML DTD 文法定义进行 HTML 解析,可以认为解析生成的 DOM 树与解析树一一映射,DOM 树就是解析树。
1.1.4 解析算法
HTML 不能被一般的自顶向下或自底向上的解析器所解析,不能使用正则解析技术,浏览器为 HTML 定制了专属的解析器,原因是:
这门语言本身的宽容特性
浏览器对一些常见的非法html有容错机制
解析过程是往复的,通常源码不会在解析过程中发生改变,但在 HTML 中,脚本标签包含的 “document.write” 可能添加标签,这说明在解析过程中实际上修改了输入,所以不能使用正则解析技术
HTML5 规范中描述了这个解析算法,算法包括两个阶段——符号化及构建树。
符号化是词法分析的过程,将输入解析为符号,HTML 的符号包括开始标签、结束标签、属性名及属性值。符号识别器识别出符号后,将其传递给树构建器,并读取下一个字符,以识别下一个符号,直到处理完所有输入。
符号识别算法(The tokenization algorithm)
算法输出 HTML 符号,该算法用状态机表示。每次读取输入流中的一个或多个字符,并根据这些字符转移到下一个状态,当前的符号状态及构建树状态共同影响结果,这意味着,读取同样的字符,可能因为当前状态的不同,得到不同的结果以进入下一个正确的状态。
这个算法很复杂,这里用一个简单的例子来解释这个原理。
1 | <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”。后面的“