共计 4547 个字符,预计需要花费 12 分钟才能阅读完成。
浏览器基本的工作流程
进入主话题之前,先罗列一下浏览器的主要构成:
用户界面- 包括地址栏、后退 / 前进按钮、书签目录等,也就是你所看到的除了用来显示你所请求页面的主窗口之外的其他部分
浏览器引擎- 用来查询及操作渲染引擎的接口
渲染引擎- 用来显示请求的内容,例如,如果请求内容为 html,它负责解析 html 及 css,并将解析后的结果显示出来
网络- 用来完成网络调用,例如 http 请求,它具有平台无关的接口,可以在不同平台上工作
UI 后端- 用来绘制类似组合选择框及对话框等基本组件,具有不特定于某个平台的通用接口,底层使用操作系统的用户接口
JS 解释器- 用来解释执行 JS 代码
数据存储- 属于持久层,浏览器需要在硬盘中保存类似 cookie 的各种数据,HTML5 定义了 web database 技术,这是一种轻量级完整的客户端存储技术
解析
当浏览器获得了资源以后要进行的第一步工作就是 HTML 解析,,它由几个步骤组成: 编码、预解析、标记和构建树。
编码
HTTP 响应主体的有效负载可以是从 HTML 文本到图像数据的任何内容。解析器的第一项工作是找出如何转制刚刚从服务器接收到的 bit。
假设我们正在处理一个 HTML 文档,解码器必须弄清楚文本文档是如何被转换成比特 (bit) 的,以便反转这个过程。
记住,最终即使是文本也会被计算机翻译成二进制,如上图所示,在本例中是 ASCII 编码—定义二进制值,如“01000100”表示字母“D”。
对于文本存在许多可能的编码—浏览器的工作是找出如何正确地解码文本。服务器应该通过 Content-Type 提供的信息同时在文本文件头部使用 Byte Order Mark 告知浏览器编码格式。
如果仍然无法确定编码,浏览器还会自行匹配一种解码格式来处理数据。有时候,解码格式也会写在 <meta> 标签中。
最坏的情况是,浏览器进行了有根据的猜测,然后开始解析之后发现一个矛盾的 <meta> 标签。在这些罕见的情况下,解析器必须重新启动,丢弃之前解码的内容。浏览器有时必须处理旧的 web 内容(使用遗留编码),许多这样的系统都支持这一点。
我们现在经常在 HTML 中使用的文件格式是 UTF-8,那是因为 UTF-8 能较完整的支持 Unicode 字符范围,同时与 CSS、JavaScript 中常见的节字符具有良好的 ASCII 兼容性。一般浏览器默认的解码格式也是 UTF-8。当解码出错的时候,我们会看到屏幕上全部都是乱码字符。
预解析
在执行脚本时,其他线程会解析文档的其余部分,找出并加载需要通过网络加载的其他资源。通过这种方式,资源可以在并行连接上加载,从而提高总体速度。请注意,预解析器不会修改 DOM 树,而是将这项工作交由主解析器处理;预解析器只会解析外部资源(例如外部脚本、样式表和图片)的引用。
预解析器不是完整的解析器,如,它不理解 HTML 中的嵌套级别或父 / 子关系。但是,预解析可以识别特定的 HTML 标签的名称和属性,以及 URL。例如,如果你的 HTML 内容中有一个 <img src=”https://somewhere.example.com/images/dog.png” alt=””>,预解析将注意到 src 属性,并将获取这个图片的请求加到请求队列中。
请求图片的速度越快越好,将等待它从网络到达的时间降到最低。预解析还会注意到 HTML 中的某些显式请求,比如 preload 和 prefetch 指令,并将它们加入等待队友中进行处理。
标记化(Tokenization)
该算法的输出结果是 HTML 标记。该算法使用状态机来表示。每一个状态接收来自输入信息流的一个或多个字符,并根据这些字符更新下一个状态。当前的标记化状态和树结构状态会影响进入下一状态的决定。这意味着,即使接收的字符相同,对于下一个正确的状态也会产生不同的结果,具体取决于当前的状态。该算法相当复杂,无法在此详述,所以我们通过一个简单的示例来帮助大家理解其原理。
基本示例 – 将下面的 HTML 代码标记化:
<html>
<body>
Hello world
</body>
</html>
初始状态是数据状态。遇到字符 < 时,状态更改为“标记打开状态”。接收一个 a-z 字符会创建“起始标记”,状态更改为“标记名称状态”。这个状态会一直保持到接收 > 字符。在此期间接收的每个字符都会附加到新的标记名称上。在本例中,我们创建的标记是 html 标记。
遇到 > 标记时,会发送当前的标记,状态改回“数据状态”。<body> 标记也会进行同样的处理。目前 html 和 body 标记均已发出。现在我们回到“数据状态”。接收到 Hello world 中的 H 字符时,将创建并发送字符标记,直到接收 </body> 中的 <。我们将为 Hello world 中的每个字符都发送一个字符标记。
现在我们回到“标记打开状态”。接收下一个输入字符 / 时,会创建 end tag token 并改为“标记名称状态”。我们会再次保持这个状态,直到接收 >。然后将发送新的标记,并回到“数据状态”。</html> 输入也会进行同样的处理。
构建树(tree construction)
在创建解析器的同时,也会创建 Document 对象。在树构建阶段,以 Document 为根节点的 DOM 树也会不断进行修改,向其中添加各种元素。标记生成器发送的每个节点都会由树构建器进行处理。规范中定义了每个标记所对应的 DOM 元素,这些元素会在接收到相应的标记时创建。这些元素不仅会添加到 DOM 树中,还会添加到开放元素的堆栈中。此堆栈用于纠正嵌套错误和处理未关闭的标记。其算法也可以用状态机来描述。这些状态称为“插入模式”。
在上一步符号化以后,解析器获得这些标记, 然后以合适的方法创建 DOM 对象并将这些符号插入到 DOM 对象中。DOM 对象的数据结构是树状的,所以这个过程称为构造树(tree construction)。另外,在 IE 的历史中,大部分时间里没有使用树结构。
在创建解析器的同时,也会创建 Document 对象。在树构建阶段,以 Document 为根节点的 DOM 树也会不断进行修改,向其中添加各种元素。标记生成器发送的每个节点都会由树构建器进行处理。
规范中定义了每个标记所对应的 DOM 元素,这些元素会在接收到相应的标记时创建。这些元素不仅会添加到 DOM 树中,还会添加到开放元素的堆栈中。此堆栈用于纠正嵌套错误和处理未关闭的标记。其算法也可以用状态机来描述。这些状态称为“插入模式”。
例如,考虑这个 HTML:
<p>sincerely<p>The authors</p>
这样可以确保结果树中的两个段落对象是兄弟节点,而忽略第二个打开的标签则与一个段落对象相对。HTML 表可能是解析器规则试图确保表具有适当结构的最复杂的表。
尽管存在所有复杂的解析规则,但是一旦创建了 DOM 树,所有试图创建正确 HTML 结构的解析规则就不再强制执行了。
使用 JavaScript,网页可以几乎以任何方式重新排列 DOM 树,即使它没有意义,例如,添加表格单元格作为 <video> 标签的子项,渲染系统负责弄清楚如何处理任何前后不一致标签。
HTML 解析中的另一个复杂因素是 JavaScript 可以在解析器执行其工作时添加更多要解析的内容。<script> 标签包含解析器必须收集的文本,然后发送到脚本引擎进行评估。当脚本引擎解析并评估脚本文本时,解析器会等待。如果 JavaScript 文件内调用了 document.writeAPI,解析器将重新开始解析过程。
事件(Events)
当解析器完成时,它通过一个名为 DOMContentLoaded 的事件宣布完成。事件是内置在浏览器中的广播系统,JavaScript 可以侦听和响应它。除了 DOMContentLoaded 事件,还有 load 事件(表示所有资源已经加载完成,包括图片、视频、CSS 等等)、unload 事件表示界面即将关闭、鼠标事件键盘事件等等。
浏览器在 DOM 中创建一个事件对象,并将其打包成有用的状态信息(例如屏幕上触摸的位置、按下的按键等等),当 JavaScript 触发事件的时候,就会同时产生事件对象。
DOM 的树结构通过允许在树的任何级别监听事件(如在树根、树叶或两者之间的任何地方)。在目标元素上触发事件的时候,需要 从 DOM 树的根元素开始向子元素查找,这个过程俗称事件捕捉阶段。到达目标元素以后,还要逐级向上返回到根元素上,这个过程俗称事件冒泡阶段。
还可以取消一些事件,例如,如果表单没有正确填写,则可以停止表单提交。(提交事件是从 <form> 元素触发的,JavaScript 侦听器可以检查表单,如果字段为空或无效,还可以选择取消事件。)
DOM
HTML 语言提供了丰富的特性集,远远超出了解析器处理的标记。解析器构建一个结构,其中的元素包含其他元素,以及这些元素最初具有什么状态(它们的属性)。结构和状态的组合足以提供基本渲染和一些交互(例如通过内置控件,如 <textarea>,<video>,<button> 等)。但是如果不添加 CSS 和 JavaScript,网络将非常枯燥(和静态)。DOM 为 HTML 元素和与 HTML 无关的其他对象提供了额外的功能层。
元素接口
在解析器将元素放入 DOM 树之前,解析器会根据不同元素的名称赋予元素不同的接口功能。些通用特性包括:
访问代表元素子元素的全部或子集的 HTML 集合
能够查找元素的属性、子元素和父元素
重要的是,创建新元素的方法(不使用解析器),并将它们附加到树中(或将它们从树中分离出来)
对于像 <table> 这样的特殊元素,该接口包含用于查找表中所有行,列和单元格的其他特定于表的功能,以及用于从表中删除和添加行和单元格的快捷方式。同样,<canvas> 接口具有绘制线条,形状,文本和图像的功能。使用这些 API 需要 JavaScript 仅仅使用 HTML 标签是不够的。
每当我们使用 JavaScript 操作 DOM 的时候,将会触发浏览器的一些连锁反应,这些反应是为了让更改后的页面更快的渲染在屏幕上。例如:
用数字代表通用的元素名称和属性, 浏览器用使用哈希表进行快速识别这些数字
将频繁变更的子元素进行缓存,方便子元素快速迭代
将 sub-tree 的跟踪变化降到最低,避免‘污染’整个 DOM 树
其他 API
DOM 中的 HTML 元素及其接口是浏览器在屏幕上显示内容的唯一机制。CSS 可以影响布局,但仅限于 HTML 元素中存在的内容。最终,如果你想在屏幕上看到内容,它必须通过作为树的一部分的 HTML 接口来完成。
访问存储系统(数据库,key/value 存储,网络缓存存储(network cache storage));
设备(各种类型的地理定位,距离和方向传感器,USB,MIDI,蓝牙,游戏手柄);
网络(HTTP 交换,双向服务器套接字,实时媒体流);
图形(2D 和 3D 图形基元,着色器,虚拟和增强现实);
和多线程(具有丰富消息传递功能的共享和专用执行环境)。
随着主要浏览器引擎开发和实施新的 Web 标准,DOM 公开的功能不断增加。然而,DOM 的这些“额外”API 中的大多数都超出了本文的范围。
总结
希望这部分对你关于 DOM 解析过程多多少少有点帮助,共进步!
你的点赞是我持续分享好东西的动力,欢迎点赞!
一个笨笨的码农,我的世界只能终身学习!
更多内容请关注公众号《大迁世界》!