微信公众号:爱写bugger的阿拉斯加
若有问题或建议,请后台留言,我会尽力解决你的问题。
此文章是我最近在看的【WebKit 技术内幕】一书的一些理解和作的笔记。
而【WebKit 技术内幕】是基于 WebKit 的 Chromium 项目的讲解。html
书接上文 浏览器内核之资源加载与网络栈程序员
本文介绍 W3C 的 DOM 模型以后,深刻 WebKit 的核心部分,剖析 WebKit 的 HTML 解释器是如何将从网络或者本地文件获取的字节流转成内部表示的结构 --- DOM 树。web
DOM (Document Object Model)的全称是文档对象模型,它能够以一种独立于平台和语言的方式访问和修改一个文档的内容和结构。这里的文档能够是 HTML 文档、XML 文档或者 XHTML 文档。DOM 以面向对象的方式来描述文档,在 HTML 文档中,Web 开发者可使用 JavaScript 语言来访问、建立、删除或者修改 DOM 结构,其主要目的是动态改变 HTML 文档的结构。算法
使用 DOM 表示的文档被描述成一个树形结构,使用 DOM 的接口能够对 DOM 树结构进行操做。浏览器
每一级的版本都对之前的版本进行了补充并伴随新功能的加入,每一个版本都对 DOM 的不一样部分进行了定义。安全
DOM 结构构成的基本要素是 “节点” ,而文档的 DOM 结构就是由层次化的节点组成。在 DOM 模型中,节点的概念很宽泛,整个文档(Document )就是一个节点,称为文档节点。HTML 中的标记(Tag)也是一种节点,称为元素(Element)节点。还有一些其余类型的节点,例如 属性节点(标记的属性)、Entity 节点、ProcessingIntruction 节点、CDataSection 节点、注释(Comment)节点等。微信
众多的节点按照层次组织构成一个 DOM 树结构。
如图 5 - 4 网络
DOM 树的根就是 HTMLDocument , HTML 网页中的标签则被转换成一个个的元素节点。同数据结构中的树形结构同样,这些节点之间也存在父子或兄弟关系。数据结构
HTML 解释器的工做就是将网络或者本地磁盘获取的 HTML 网页和资源从字节流解释成 DOM 树结构。这一过程大体能够理解成图 5-5所述的步骤。并发
这过程当中,WebKit 内部对网页内容在各个阶段的结构表示。 WebKit 中这一过程以下:首先是字节流,通过解码以后是字符流,而后经过词法分析器会被解释成词语(Tokens),以后通过语法分析器构建成节点,最后这些节点被组建成一棵 DOM 树。
在进行词法分析以前,解释器首先要作的事情就是检查该网页内容使用的编码格式,以便后面使用合适的解码器。若是解释器在 HTML 网页中找到了设置的编码格式, WebKit 会使用相应的解码器来将字节流转换成特定格式的字符串。若是没有特殊格式,词法分析器 HTMLTokenizer 类能够直接进行词法分析。
词法分析的工做都是由 HTMLTokenizer 来完成 ,简单来讲,它就是一个状态机---输入的是字符串,输出的是一个个词语。由于字节流多是分段的,因此输入的字符串可能也是分段的,可是这对词法分析器来讲没有什么特别之处,它会本身维护内部的状态信息。
词法分析器的主要接口是 “nextToken” 函数,调用者只须要关键字符串传入,而后就会获得一个词语,并对传入的字符串设置相应的信息,表示当前处理完的位置,如此循环,若是词法分析器遇到错误,则报告状态错误码,主要逻辑在图 5-8 中给予了描述。
对于 “nextToken” 函数的调用者而言,它首先设置输入须要解释的字符串,而后循环调用 NextToken 函数,直处处理结束。 “nextToken” 方法每次输出一个词语,同时会标记输入的字符串,代表哪些字符已经被处理过了。所以,每次词法分析器都会根据上次设置的内部状态和上次处理以后的字符串来生成一个新的词语。 “nextToken” 函数内部使用了超过 70 种状态,图中只显示了 3 种状态。对于每一个不一样的状态,都有相应的处理逻辑。
当词语生成以后,WebKit 须要使用 XSSAuditor 来验证词语流(Token Stream)。XSS 指的是 Cross Site Security , 主要是针对安全方面的考虑。
根据 XSS 的安全机制,对于解析出来的这些词语,可能会阻碍某些内容的进一步执行,因此 XSSAuditor 类主要负责过滤这些被阻止的内容,只有经过的词语才会做后面的处理。
通过词法分析器解释以后的词语随之被 XSSAuditor 过滤而且在没有被阻止以后,将被 WebKit 用来构建 DOM 节点。从词语到构建节点的步骤是由 HTMLDocumentParser 类调用 HTMLTreeBuilder 类的 “constructTree” 函数来实现。
从节点到构建 DOM 树,包括为树中的元素节点建立属性节点等工做由 HTMLConstructionSite 类来完成。正如前面介绍的,该类包含一个 DOM 树的根节点 ——HTMLDocument 对象,其余的元素节点都是它的后代。
由于 HTML 文档的 Tag 标签是有开始和结束标记的,因此构建这一过程可使用栈结构来帮忙。HTMLConstructionSite 类中包含一个 “HTMLElementStack” 变量,它是一个保存元素节点的栈,其中的元素节点是当前有开始标记可是尚未结束标记的元素节点。想象一下 HTML 文档的特色,例如一个片断 “<body><div><img></img></div></body>”,当解释到 img 元素的开始标记时,栈中的元素就是 body 、div 和 img ,当遇到 img 的结束标记时,img 退栈, img 是 div 元素的子女;当遇到 div 的结束标记时,div 退栈,代表 div 和它的子女都已处理完,以此类推。
同 DOM 标准同样,一切的基础都是 Node 类。在 WebKit 中, DOM 中的接口 Interface 对应于 C++ 的类,Node 类是其余类的基类,图 5-10 显示了 DOM 的主要相关节点类。图中的 Node 类实际上继承自 EventTarget 类,它代表 Node 类可以接受事件,这个会在 DOM 事件处理中介绍。Node 类还继承自另一个基类 ——ScriptWrappable,这个跟 JavaScript 引擎相关。
Node 的子类就是 DOM 中定义的同名接口,元素类,文档类和属性类均继承自一个抽象出来的 ContainerNode 类,代表它们可以包含其余的节点对象。回到 HTML 文档来讲,元素和文档对应的类注是 HTMLElement 类和 HTMLDocument 类,实际上 HTML 规范还包含众多的 HTMLElement 子类,用于表示 HTML 语法中众多的标签。
上面介绍了 Frame 、Document 等 WebKit 中的基础类,这些都是网页内部的概念,实际上,WebKit 提供了更高层次的设施,用于表示整个网页的一些类,WebKit 中的 接口部分 就是基于它们来提供的,表示网页的类既提供了构建 DOM 树等操做,同时也提供了接口用于布局。渲染等操做。
在 Renderer 进程中有一个线程,该线程用来处理 HTML 文档的解释任务,在 HTML 解释器的步骤中,WebKit 的 Chromium 移植跟其余的 WebKit 移植也存在不一样之处。
线程化的解释器就是利用单独的线程来解释 HTML 文档。由于在WebKit 中,网络资源的字节流自 IO 线程传递给渲染线程以后,后面的解释、布局和渲染等工做基本上就是工做在该线程,也就是渲染线程完成的(这不是绝对的)。由于 DOM 树只能在渲染线程上建立和访问,这也就是说构建 DOM 树的过程只能在渲染线程中进行。可是,从字符到词语这个阶段能够交给单独的线程来作,Chromium 浏览器使用的就是这个思想。
具体的实现过程:
字符串 (传给)=> HTMLDocumentParser类 (建立一个新的对象)=> BackgroundHTMLParser 来负责处理 (交给)=> 前一步建立的对象
WebKit 会检查是否须要建立用于解释字符串的线程 HTMLParserThread 。若是该线程已存在,WebKit 就将刚刚的任务传递给这一新线程, 图 5-13 描述了这一过程。
在 HTMLParserThread 线程中,WebKit 所作的事情包括将字符串解释成一个个词语,而后使用以前提到的 XSSAuditor 进行安全检查。这是在一个新的线程中执行。主要区别在于解释成词语以后,WebKit 会分批次地将结果词语传递给渲染线程。
在 HTML 解释器的工做过程当中,可能会有 JavaScript 代码(全局做用域的代码)须要执行,它发生在将字符串解释成词语以后、建立各类节点的时候。这也是全局执行的 JavaScript 代码不能访问 DOM 树的缘由——由于 DOM 树尚未被建立完。
因此建议 JavaScript 的使用以下:
一、将 “script” 元素加上 “async” 属性,代表这是一个能够异步执行的 JavaScript 代码。
二、将 “script” 元素放在 “body” 元素的最后,这样它不会阻碍其余资源的并发下载。
可是不这样作的时候,WebKit 使用预扫描和预加载机制来实现资源的并发下载而不被 JavaScript 的执行所阻碍。
具体作法是:当遇到须要执行 JavaScript 代码的时候,WebKit 先暂停当前 JavaScript 代码的执行,使用预先扫描器 HTMLPreloadScanner 类来扫描后面的词语。若是 WebKit 发现它们须要使用其余资源,那么使用预资源加载器 HTMLPreloadScanner 类来发送请求,在这以后,才执行 JavaScript代码。预先扫描器自己并不建立节点对象,也不会构建 DOM 树,因此速度比较快。
当 DOM 树构建完以后,WebKit 触发 “DOMContentLoaded” 事件,注册在该事件上的 JavaScript 函数会被调用。当所在资源都被加载完以后,WebKit 触发 “onload” 事件。
WebKit 将 DOM 树建立过程当中须要执行的 JavaScript 代码交由 HTMLScriptRunner 类来负责。工做方式很简单,就是利用 JavaScript 引擎来执行 Node 节点中包含的代码,具体能够参考 “HTMLScriptRunner::executeParsingBlockingScript” 方法。
事件在工做过程当中使用两个主体,第一个是事件(event),第二个是事件目标(EventTarget)。WebKit 中用 EventTarget 类来表示 DOM 规范中 Events 部分定义的事件目标。
每一个 事件都有属性来标记该事件的事件目标。当事件到达事件目标(如一个元素节点)的时候,在这个目标上注册的监听者(Event Listeners)都会有触发调用,而这些监听者的调用顺序不是固定的,因此不能依赖监听者注册的顺序来决定你的代码逻辑。
图 5-17 是 EventTarget 接口的定义。图中的接口是用来注册和移除监听者的。
事件处理最重要就是事件捕获(Event capture)和事件冒泡(Event bubbling)这两种机制。图 5-18 是事件捕获和事件冒泡的过程。
当渲染引擎接收到一个事件的时候,它会经过 HitTest(WebKit 中的一种检查触发gkwrd哪一个区域的算法)检查哪一个元素是直接的事件目标。在图 5-18 中,以 “img” 为例,假设它是事件的直接目标,这样,事件会通过自顶向下和自底向上的两个过程。
事件的捕获是自顶向下,事件先是到 document 节点,而后一路到达目标节点。在图 5-18 中,顺序就是 “#document” -> "HTML" -> "body" -> "img" 这样一个顺序。事件能够在这一传递过程当中被捕获,只须要在注册监听者的时候设置相应参数便可。默认状况下,其余节点不捕获这样的事件。若是网页注册了这样的监听者,那么监听者的回调函数会被调用,函数能够经过事件的 “stopPropagation” 函数来阻止事件向下传递。
事件的冒泡过程是从下向上的顺序,它的默认行为是不冒泡,可是是事件包含一个是否冒泡的属性。当这一属性为真的时候,渲染引擎会将该事件首先传递给事件的目标节点的父亲,而后是父亲的父亲,以此类推。同捕获动做同样,这此监听函数也可使用 “stopPropagation” 函数来阻止事件向上传递。
DOM 的事件分为不少种,与用户相关的只是其中的一种,称为 UIEvent ,其余的包括 CustomEvent、MutationEvent 等。UIEvent 又能够分为不少种,包括可是不限于 FocusEvent、MouseEvent、KeyboardEvent、Composition 等。
基于 WebKit 的浏览器事件处理过程,首先是作 HitTest ,查找事件发生处的元素,检查该元素有无监听者。若是网页的相关节点注册了事件的监听者,那么浏览器会把事件派发给 WebKit 内核来处理。同时,浏览器也可能须要理解和处理这样的事件。这主要是由于,有些事件浏览器必须响应从而对网页做默认处理。
EventHandler 类是处理事件的核心类,它除了须要将各类事件传给 JavaScript 引擎以调用响应的监听者以外,它还会识别鼠标事件,来触发调用右键菜单、拖放效果等与事件密切相关的工做,并且 EventHandler 类还支持网页的多框结构。EventHandler 类的接口比较容易理解,可是它的处理逻辑极其复杂。
图 5-20 简单描述了鼠标事件的调用过程,这一过程自己是比较简单的,复杂之处在于 WebKit 的 EventHandler 类。
WebKit 中还有些跟事件处理相关的其余类,例如 EventPathWalker、EventDispatcher 类等,这些类都是为了解决事件在 DOM 树中传递的问题。
影子 DOM 是一个新东西,主要解决了一个文档中可能须要大量交互的多个 DOM 树创建和维护各自的功能边界的问题。
当开发这样一个用户界面的控件——这个控件可能由一些 HTML 的标签元素组成,这些元素能够组成一颗 DOM 树的子树。这样一个 HTML 控件能够被处处使用,可是问题随之而来,那就是每一个使用控件的地方都会知道这个子树的结构。
当网页的开发者须要访问网页 DOM 树的时候,这些控件内部的 DOM 子树都会暴露出来,这些暴露的节点不只可能给 DOM 树的遍历带来不少麻烦,并且也可能给 CSS 的样式选择带来问题,由于选择器无心中可能会改变这些内部节点的样式,从而致使很奇怪的控件界面。
如何将内部的节点信息封装起来,就像 C++ 语言的类同样,同时又可以将这些节点渲染出来呢 ? W3C 工做组提出的影子 DOM 概念。影子 DOM 的规范草案可以使得一些 DOM 节点在特定范围内可见,而在网页的 DOM 树中却不可见,可是网页渲染的结果中包含了这些节点,这就使得封装变得容易不少。
图 5-21 描述了 HTML 文档对应的 DOM 树和 “div” 元素包含的一个影子 DOM 子树。当使用 JavaScript 代码访问 HTML 文档的 DOM 树的时候,一般的接口是不能直接访问到影子 DOM 子树中的节点的,JavaScript 代码只能经过特殊的接口方式。
HTML5 支持了不少新的特性,例如对视频、音频的支持,读者会发现这些元素实际上是由很复杂的控制界面组成,这些界面也是使用 HTML 元素编写,可是在 DOM 树中,你没法找到相应的节点,这其实也是使用了影子 DOM 的思想。
由于影子 DOM 的子树在整个网页的 DOM 树中不可见,那么事件是如何处理的呢 ?事件中须要包含事件目标,这个目标固然不能是不可见的 DOM 节点,因此事件目标其实就是包含影子 DOM 子树的节点对象。事件捕获的逻辑没有发生变化,在影子 DOM 子树内也会继续传递。当影子 DOM 子树中的事件向上冒泡的时候, WebKit 会同时向整个文档的 DOM 上传递该事件,以免一些很奇怪的行为。
WebKit 已经支持影子 DOM 的规范草案,虽然还存在一些问题。支持影子 DOM 的相关类在目录 “Source/core/dom/shadow” 下,里面的主要类是 ShadowRoot ,表示的是影子 DOM 的根节点。ShadowRoot 类继承自 DocumentFragment 类,因此它一样有 Node 节点的属性和方法,于是在影子 DOM 树的内部,遍历树没有什么特别不一样的地方。
当遍历 HTML 文档对应 DOM 树的时候,WebKit 须要作特别的判断,因此读者会发如今 WebKit 的 Node 类实现中存在大量的条件语句,用来检查当前节点是不是 ShadowRoot 对象,若是是该类的对象,把它做为不一样 DOM 树之间的边界。有时候 WebKit 还须要对 ShadowRoot 对象做出特别处理,好比某些状况会略过它的子树,一样的,在事件处理的支持类 EventPathWalker 和 EventRetargeter 中,也须要作一些特别的处理逻辑,原理就是上面所述,细节再也不介绍。
示例代码 5-2 给出了一个简单的使用 webkitCreateShadowRoot 接口来建立影子 DOM 子树的例子。网页只包含了一个 “div” 元素,JavaScript 代码使用该元素建立了一个影子 DOM 子树的根节点,而后该根节点下加入了两个子女,第一个是图片元素,第二个是 “div” 元素,该元素内部包含了一些文本。
读者能够打开 Chrom 浏览器的开发者工具,而后打开控制台,在其中输入 “document.firstChild.firstChild.nextElementSibling.firstElementChild.firstElementChild” 后会发现结果是空的,根据对应关系 “#document-> html -> head -> body -> div -> null”,虽然网页中没有 ‘head’ 元素,可是 DOM 树仍然会建立该节点。同时读者会发现 “div” 元素没有子女,影子 DOM 子树真的被隐藏起来了,成为真正的影子。
但愿本文对你有点帮助。
下期分享 第六章 CSS 解释器和样式布局 敬请期待。
对 全栈开发 有兴趣的朋友能够扫下方二维码关注个人公众号 —— 爱写bugger的阿拉斯加
分享 web 开发相关的技术文章,热点资源,全栈程序员的成长之路。