刨析——浏览器如何工作

浏览器如何工作

  • 浏览器可以被认为是使用最为广泛的软件,以下篇幅将介绍浏览器的工作原理,我们将看到,从你在地址栏输入google.com到你看到google主页过程中都发生了什么。

浏览器的种类介绍

  • 今天,有五种主流浏览器 ——— ChromeFirefoxIESafariOpera
  • 本文将基于一些开源浏览器的例子 —— ChromeFirefoxSafari, Safari是部分开源的

浏览器的主要功能

  • 浏览器的主要功能是将用户选择的web资源呈现出来,它需要从服务器请求资源,并将其显示在浏览器窗口中,资源的格式通常是HTML,也包括PDF、Image以及其他格式。用户用URI(Uniform Resource Identifier 统一资源标识符)来指定所请求资源的位置。
  • HTML和CSS规范中规定了浏览器解释html文档的方式,由W3C组织对这些规范进行维护,W3C是负责制定Web标准的组织
  • HTML规范的最新版本是http://www.w3.org/TR/html401/,HTML5规范还在定制中,最新的CSS规范版本是http://www.w3.org/TR/CSS2, CSS3规范还在定制中。
  • 浏览器问世以来,浏览器厂商之间的竞争没有停息过,纷纷开发自己浏览器的拓展,对规范的遵循并不完善,这为web开发者带来了严重的兼容性问题。
  • 但是,浏览器的用户界面则差不多,常见的用户界面元素包括:
    • 用来输入URI的地址栏;
    • 前进、后退按钮;
    • 书签选项;
    • 用于刷新以及暂停当前加载文档的刷新、暂停按钮;
    • 用于到达主页的主页按钮
  • 奇怪的是,并没有哪个正式公布的规范对于用户界面做出规定,这些是多年来浏览器厂商之间相互模仿和不断改进的结果。
  • HTML5并没有规定浏览器必须具有的UI元素,但列出了一些常用元素, 包括地址栏状态栏以及工具栏。还有一些浏览器有自己专有的功能,比如firefox的下载管理。

浏览器的主要构成High Level Structure

  • 浏览器的主要组件包括:
      1. 用户界面 —— 包括地址栏、后退/前进按钮、书签目录等,也就是你所看到的除了用来显示你所请求页面的主窗口之外的其他部分;
      1. 浏览器引擎 —— 用来查询以及操作渲染引擎的接口;
      1. 渲染引擎 —— 用来显示请求的内容,例如,如果请求内容为html,它负责解析html以及css,并将解析后的结果显示出来;
      1. 网络 —— 用来完成网络调用,例如http请求,它具有与平台无关的接口进行通信的功能,可以在不同平台上工作;
      1. UI(User Interface)后端 —— 用来绘制类似组合选择框及对话框等基本组件,具有不特定某个平台的通用接口,底层使用操作系统的用户接口;
      1. JS解释器 —— 用来解释执行JS代码
      1. 数据存储 —— 属于持久层,浏览器需要在硬盘中保存类似cookie的各种数据,HTML5定义了web database技术,这是一种轻量级完整的客户端存储技术。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2fICMFAF-1650371633191)(./images/2011110316262567.png)]

需要注意的是, Google Chrome浏览器不同于大部分浏览器,Google Chrome浏览器为每个Tab分配了各自的渲染引擎实例,每个Tab就是一个独立的进程。

  • 对于构成浏览器的这些组件详细原理分析,后面会持续更新。

组件间的通信Communication between the components

  • Firefox和Chrome都开发了一个特殊的通信结构,后面会详细讨论

渲染引擎 The rendering engine

  • 渲染引擎的职责就是渲染,即在浏览器窗口中显示所请求的内容。
    • 默认情况下,渲染引擎可以显示HTMLXML文档以及图片,它也可以借助插件(一种浏览器扩展)显示其他类型数据,例如使用PDF阅读器插件(PDF.js),可以显示PDF格式,将由专门一章讲解插件以及其扩展,这里只讨论渲染引擎最主要的用途——显示应用了CSS之后的HTML以及图片

渲染引擎 Rendering engines

  • 本文所讨论的浏览器 —— FirefoxChromeSafari是基于两种渲染引擎构建的,Firefox基于Geoko —— Firefox公司自主研发的一款渲染引擎,Safari和Chrome是基于Webkit。
  • Webkit是一款开源渲染引擎,它本来是为Linux平台研发的,后来由Apple移植到Mac以及Windows上,Webkit参考http://webkit.org

主流程 The main flow

  • 渲染引擎首先通过网络获取所请求文档的内容,通常以8K分块的方式完成。
下面是渲染引擎在取得内容之后的基本流程
  • 解析html以构建dom树->构建render树->布局render树->绘制render树
  • 如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SoLzpTiU-1650371633192)(./images/2011110316263715.png)]

  • 渲染引擎开始解析html,并将标签转换为内容树中的dom节点。接着,它解析外部CSS文件以及style标签中的样式信息。这些样式信息以及html中的可见性指令将被用来构建另一棵树——render树;
  • render树由一些包含有颜色和大小等属性的矩形组成,它们将被按照正常的顺序显示到屏幕上。
  • render树构建好了之后,将会执行布局过程,它将确定每个节点在屏幕上的确切坐标。 再下一步就是绘制,即遍历render树,并使用UI后端层绘制每个节点。
  • 值得注意的是,这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的html都解析完成之后再去构建和布局render树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iVNqlWzy-1650371633193)(./images/2011110316264892.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GzmbKeGy-1650371633193)(./images/2011110316270146.jpg)]

  • 从图3和图4中可以看出,尽管webkit和Gecko使用的术语稍有不同,他们的主要流程基本相同。Gecko称可见的格式化元素组成的树为frame树,每个元素都是一个frame,webkit则使用render树这个名词来命名由渲染对象组成的树。Webkit中元素的定位称为布局,而Gecko中称为回流。Webkit称利用dom节点以及样式信息去构建render树的过程为attachment,Gecko在html和dom树之间附加了一层,这层称为内容接收器,相当制造dom元素的工厂。下面将讨论流程中的各个阶段。

解析 Parsing-general

  • 既然解析是渲染引擎中一个非常重要的过程,我们将稍微深入的研究它。首先简要介绍一下解析。
  • 解析一个文档即将其转换为具有一定意义的结构——编码可以理解和使用的东西。解析的结果通常是表达文档结构的节点树,称为解析树或者语法树。
  • 例如,解析"2+3-1"这个表达是,可能返回这样一棵树。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iUS5vAS6-1650371633193)(./images/2011110316271310.png)]

文法
  • 解析基于文档依据的语法规则——文档的语法或者格式。每种可以被解析的格式必须具有由词汇以及语法规则组成的特定的文法,称为上下文无关文法。

人类语言不具有这一特性,因此不能被一般的解析技术所解析。

解析器 —— 词法分析器
  • 解析可以分为两个子进程——词法分析以及语法分析
  • 词法分析就是将输入分解为符号,符号是语言的词汇表——基本有效单元的集合。对于人类语言来说,它相当于我们字典中出现的所有单词。
  • 语法分析指对语言应用语法规则。
  • 解析器一般将工作分配给两个组件——词法分析器(有时也叫分词器)负责将输入分解为合法的符号,解析器则根据语言的语法规则分析文档结构,从而构建解析树,词法分析器知道怎么跳过空白和换行之类的无关字符。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dvejzk4v-1650371633194)(./images/2011110316272491.png)]

  • 解析过程是迭代的,解析器从词法分析器处取到一个新的符号,并试着用这个符号匹配一条语法规则,如果匹配了一条规则,这个符号对应的节点将被添加到解析树上,然后解析器请求另一个符号。如果没有匹配到规则,解析器将在内部保存该符号,并从词法分析器下一个符号,直到所有内部保存的符号能够匹配一项语法规则。如果最终没有找到匹配的规则,解析器将抛出一个异常,这意味着文档无效或者是包含语法错误。
转换
  • 很多时候,解析树并不是最终结果。解析一般在转换中使用——将输入文档转换为另一种格式。编译就是个例子,编译器在将一段源码编译为机器码的时候,先将源码解析为解析树,然后将该树转换为一个机器文档码。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wEt4BvWP-1650371633194)(./images/2011110316274796.png)]

解析实例
  • 图5中,我们从一个数学表达式构建了一个解析树,这里定义了一个简单的数学语言来看下解析过程。
  • 词汇表: 我们的语言包括整数、加号以及减号。
  • 语法:
      1. 该语言的语法基本单元包括表达式、term以及操作符;
      1. 该语言可以包括多个表达式;
      1. 一个表达式定义为两个term通过一个操作符连接;
      1. 操作符可以是加号或者减号;
      1. term可以是一个证书或者是一个表达式。
  • 现在来分析一下"2+3-1"这个输入
  • 第一个规则的子字符串是"2",根据规则5,它是一个term,第二个匹配的是"2+3",它符合第2条规则——一个操作符连接两个term,下一次匹配发生在输入的结束处。"2+3-1"是一个表达式,因为我们知道"2+3"是一个term,所以我们有了一个term紧跟着一个操作符以及另一个term。"2++"将不会匹配任何规则,因此是一个无效输入。
词汇表以及语法的定义
  • 词汇表通常利用正则表达式来定义。
  • 例如上面的语言可以定义为:
    • INTEGER: 0 | [1-9] [0-9] *
    • PLUS: +
    • MINUS: -
  • 正如看到的,这里用正则表达式定义整数。
  • 语法通常用BNF格式定义,我们的语言可以定义为:
    • expression := term operation term
    • operation := PLUS | MINUS
    • term := INTEGER | expression
  • 如果一个语言的文法是上下文无关的,则它可以用正则解析器来解析。对上下文无关文法的一个直观的定义是,该文法可以用BNF来完整的表达。可查看http://en.wikipedia.org/wiki/Context-free_grammar
解析器类型
  • 有两种基本的解析器——自顶向下解析以及自底向上解析。比较直观的解释是,自顶向下解析,查看语法的最高层结构并试着匹配其中一个;自底向上解析则从输入开始,逐步将其转换为语法规则,从底层规则开始直到匹配高层规则。
  • 来看一下这两种解析器如何解析上面的例子:
    • 自顶向下解析器从最高层规则开始——它先识别出"2+3",将其视为一个表达式,然后识别出"2+3-1"为一个表达式(识别表达式的过程中匹配了其他规则,但出发点是最高层规则)。
    • 自底向上解析会扫描输入直到匹配了一条规则,然后用该规则取代匹配的输入,直到解析完成所有所有输入。部分匹配的表达式被放置在解析堆栈中。
StackInput
2 + 3 - 1
term+ 3 - 1
term operation3-1
expression-1
expression operation1
expression
  • 自底向上解析器称为shift reduce解析器,因为输入向右移动(想象一个指针首先指向输入开始处,并向右移动),并逐渐简化为语法规则。
自动化解析
  • 解析生成器这个工具可以自动生成解析器,只需要指定语言的文法——词汇表以及语法规则,它就可以生成一个解析器。创建一个解析器需要对解析有深入的理解,而且手动的创建一个由较好性能的解析器并不容易,所以解析生成器很有用。Webkit使用两个知名的解析生成器——用于创建语法分析器的Flex以及创建解析器的Bison(你可能接触过Lex和Yacc)。Flex的输入是一个包含了符号定义的正则表达式,Bison的输入是用BNF格式表示的语法规则。

HTML解析器

  • HTML解析器的工作是将html表示解析为解析树。
HTML文法定义

-W3C组织制定规范定义了HTML的词汇表和语法。

非上下文无关文法
  • 正如在解析简介中提到的,上下文无关文法的语法可以用类似BNF的格式来定义。
HTML文法定义
  • W3C组织制定规范定义了HTML的词汇表和语法。
非上下文无关文法
  • 正如在解析简介中提到的,上下文无关文法的语法可以用类似BNF的格式来定义。
  • 不幸的是,所有的传统解析方式都不适用html(当然我们提出它们并不只是因为好玩,它们将用来解析css和js),html不能简单的用解析所需的上线文无关文法来定义。
  • 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在https://www.w3.org/TR/html4/strict.dtd
DOM
  • 输出的树,也就是解析树,是由DOM元素以及属性节点组成的。DOM是文档对象模型的缩写,它是html文档的对象表示,作为html元素的外部接口供js等调用。
  • 树的根是"document"对象。
  • DOM和标签基本是一一对应的关系,例如,如下的标签:
<html>
    <body>
        <p>
          Hello DOM
        </p>
        <div><img src=”example.png” /></div>
    </body>
</html>
  • 将会被转换为下面的DOM树:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bhsLF5rv-1650371633194)(./images/2011110316280265.png)]

解析算法
  • 正如前面章节中讨论的,html不能被一般的自顶向下或者自底向上的解析器所解析。
  • 原因是:
      1. 这门语言本身的宽容特性;
      1. 浏览器对一些常见的非法html有容错机制;
      1. 解析过程是往复的,通常源码不会在解析过程中发生改变,但在html中,脚本标签包含的"document.write"可能添加标签,这说明在解析过程中实际上修改了输入不能使用正则解析技术,浏览器为html定制了专属的解析器。
  • Html5规范中描述了这个解析算法,算法包括两个阶段——符号化以及构建树
  • 符号化是词法分析的过程,将输入解析为符号,html的符号包括开始标签、结束标签、属性名以及属性值。
  • 符号识别器识别出符号后,将其传递给树构建器,并读取下一个字符,以识别下一个符号,这样直到处理完所有输入。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sy6wPFbm-1650371633195)(./images/2011110316282058.png)]

符号识别算法
  • 算法输出html符号,该算法用状态机表示。每次读取输入流中的一个或者多个字符,并根据这些字符转移到下一个状态,当前的符号状态以及构建树状态共同影响结果,这意味着,读取同样的字符,可能因为当前状态的不同,得到不同的结果可以进入下一个正确的状态。
  • 这个算法很复杂,这里用一个简答的例子来解释这个原理。
  • 基本示例——符号化下面的html:
<html>
    <body>
        Hello world
    </body>
</html>
  • 初始状态为"Data State",当遇到"<“字符,状态变为"Tag open state”,读取一个a-z的字符将产生一个开始标签符号,状态相应变为"Tag name state",一直保持这个状态直到读取到">",每个字符都附加到这个符号名上,例子中创建的是一个html符号。
  • 当读取到">“,当前的符号就完成了,此时,状态回到"Data state”,"<body>"重复这一处理过程。到这里, html和body标签都识别出来了。现在,回到"Data state",读取"Hello world"中的字符"H"将创建并识别出一个字符符号,这里会为"Hello world"中的每个字符生成一个字符符号。
  • 这样直到遇到"</body>"中的"<“。现在,有回到了"Tag open state”, 读取下一个字符"/“将创建一个闭合标签符号,并且状态转移到"Tag name state”, 还是保持这一状态,直到遇到">“。然后,产生一个新的标签符号并回到"Data state”。后面的"</html>"将和"</body>"一样处理。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HjU15Jzn-1650371633195)(./images/2011110316283115.png)]

树的构建算法
  • 在树的构建阶段,将修改以Document为根的DOM树,将元素附加到树上。每个由符号识别器识别生成的节点将会被树构造器进行处理,规范中定义了每个符号相对应的Dom元素,对应的Dom元素将会被创建。这些元素除了会被添加到Dom树上,还将被添加到开发元素堆栈中。这个堆栈用来纠正嵌套的未匹配和未闭合标签,这个算法也是用状态机来描述,所有的状态采用插入模式。
  • 来看一下示例中树的创建过程:
<html>
	<body>
		Hello world
	</body>
</html>
  • 构建树这一阶段的输入是符号识别阶段生成的符号序列。
  • 首先是"initial mode",接受到html符号后将转换为"before html"模式,在这个模式中对这个符号进行再处理。此时,创建了一个HTML Html Element元素,并将其附加到根Document对象上。
  • 状态此时变为"before head",接受到body符号时,即使这里没有head符号,也将自动创建一个HTML Head Element元素并附加到树上。
  • 现在,转到"in head"模式,然后是"after head"。 到这里,body符号会被再次处理,将创建一个HTML Body Element并插入到树中,同时,转移到"in body"模式。
  • 然后,接受到字符串"Hello world"的字符符号,第一个字符将导致创建并插入一个text节点,其他字符将附加到该节点。
  • 接收到body结束符号时,转移到"after body"模式,接着接收到html结束符号,这个符号意味着转移到了"after after body"模式,当接收到文件结束符号时,整个解析过程结束。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z7acxHAR-1650371633195)(./images/2011110316284511.gif)]

解析结束时的处理
  • 在这个阶段,浏览器将文档标记为可交互的,并开始解析处于延时模式中的脚本——这些脚本在文档解析后执行。
  • 文档状态将被设置为完成,同时触发一个load事件。
  • Html5规范中有符号化以及构建树的完整算法(https://html.spec.whatwg.org/multipage/syntax.html#html-parser)
浏览器容错
  • 你从来不会在一个html页面上看到"无效语法"这样的错误,浏览器修复了无效内容并继续工作。
  • 以下面这段html为例:
<html>
	<mytag>
	</mytag>
	<div>
	<p>
	</div>
		Really lousy HTML
	</p>
</html>
  • 这段html违反了很多规则(mytag不是合法的标签,p以及div错误的嵌套等等),但是浏览器仍然可以没有任何怨言的继续显示,它在解析的过程中修复了html作者的错误。
  • 浏览器都具有错误处理的能力,但是,令人惊讶的是,这并不是html最新规范的内容,就像书签以及前进后退按钮一样吗,它只是浏览器长期发展的结果。一些比较知名的非法html结构,在许多站点中出现过,浏览器都试着以一种和其他浏览器一致的方式去修复。
  • Html规范定义了这方面的需求,webkit在html解析类开始部分的注释中做了很好的总结。
  • 解析器将符号化的输入解析为文档并创建文档,但不幸的是,我们必须处理很多没有很好格式化的html文档,至少要小心下面几种错误情况。
    1. 在未闭合的标签中添加明确禁止的元素。这种情况下,应该先将前一标签闭合;
    1. 不能直接添加元素。有些人在写文档的时候会忘了中间一些标签(或者中间标签是可选的),比如HTML HEAD BODY TR TD LI等;
    1. 想在一个行内元素中添加块状元素。关闭所有的行内元素,直到下一个更高的块状元素;
    1. 如果这些都不行,就闭合当前标签直到可以添加该元素。
  • 下面来看一些webkit容错的例子:
</br>替代<br>
  • 一些网站使用</br>替代<br>,为了兼容IE和Firefox,webkit将其看作<br>
  • 代码:
if(t->isCloseTag(brTag)&&m_document->inCompatMode()) {
	reportError(MalformedBRError);
	t->beginTag = true;
}

Note - 这里的错误处理在内部进行,用户看不到

迷路的表格
  • 这指一个表格嵌套在另一个表格中,但不在它的某个单元格内。
  • 比如下面的这个例子:
<table>
	<table>
		<tr><td>inner table</td></tr>
	</table>
	<tr><td>outer table</dt></tr>
</table>
  • webkit将会将嵌套的表格变为两个兄弟表格:
<table>
    <tr><td>outer table</td></tr>
</table>
<table>
    <tr><td>inner table</td></tr>
 </table>
  • webkit将会将嵌套的表格变为两个兄弟表格:
<table>
	<tr><td>outer table</td></tr>
</table>
<table>
	<tr><td>inner table</td></tr>
</table>
  • 代码:
if (m_inStrayTableContent && localName == tableTag)
	popBlock(tableTag);
  • webkit使用堆栈存放当前的元素内容,它将从外部表格的堆栈中弹出内部的表格,则它们变为了兄弟表格。
嵌套的表单元素
  • 用户将一个表单嵌套到另外一个表单中,则第二个表单将被忽略。
  • 代码:
if (!m_currentFormElement) {
	m_currnetFormElement = new HTMLFormElement(formTag, m_document);
}
  • 太深的标签继承
  • www.liceo.edu.mx是一个由嵌套层次的站点的例子,最多只允许20个相同类型的标签嵌套,多出来的将被忽略。
  • 代码:
bool HTMLParser::allowNestedRedundantTag(const AtomicString& tagName)
{
	unsigned i = 0;
	for (HTMLStackElem* curr = m_blockStack; 
			i < cMaxRedundantTagDepth && curr && curr -> tagName == tagName;
		curr = curr->next, i++) { }
	return i != cMaxRedundantTagDepth;
}
  • 放错了地方的html、body闭合标签
  • 又一次不言自明。
  • 支持不完整的html。我们从来不闭合body,因为一些愚蠢的网页总是在还未真正结束时就闭合它。我们依赖调用end方法去执行关闭的处理。
  • 代码:
if (t->tagName == htmlTag || t->tagName == bodyTag)
	return;
  • 所以,web开发者要小心了,除非你想成为webkit容错代码的范例,否则还是写格式良好的html吧。

CSS解析

  • 还记得简介中提到的解析的概念吗,不同于html, css属于上下文无关文法,可以用前面所描述的解析器解析。CSS规范定义了css的词法以及语法文法。
  • 看一些例子:
  • 每个符号都由正则表达式定义了词法文法(词汇表):
comment ///*[^*]*/*+([^/*][*]*/*+)*//
num [0-9]+|[0-9]*"."[0-9]+
nonascii [/200-/377]
nmstart [_a-z]|{nonascii}|{escape}
nmchar [_a-z0-9-]|{nonascii}|{escape}
name {nmchar}+
ident {nmstart}{nmchar}*
  • "ident"是识别器的缩写,相当于一个class名,“name"是一个元素id(用”#"引用)。
  • 语法用BNF进行描述:
ruleset
	: selector [ ',' S* selector ]*
		'{' S* declaration [ ';' S* declaration ]* '}' S*
	;

Thinking in JackDan

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值