《WebKit 技术内幕》之五(2): HTML解释器和DOM 模型

2.HTML 解释器

2.1 解释过程

HTML 解释器的工作就是将网络或者本地磁盘获取的 HTML 网页和资源从字节流解释成 DOM 树结构。

这一过程中,WebKit 内部对网页内容在各个阶段的结构表示。 WebKit 中这一过程如下:首先是字节流,经过解码之后是字符流,然后通过词法分析器会被解释成词语(Tokens),之后经过语法分析器构建成节点,最后这些节点被组建成一棵 DOM 树。

WebKit为完成这一过程,引入比较复杂的基础设施类。

左边部分网页框结构,框对应于Frame类,而文档对应于HTMLDocument类,所以框包含文档、HTMLDocument类继承自Document类。也是遵循DOM标准的,因为Document有两个子类,另外一个是XMLDocument。这里没有描述内嵌的复杂框结构,但是足以说明网页基本结构内部表示。在实际应用中,网页内嵌框也会重复这样的动作。

右边部分是WebKit为建立网页框结构所建立的设施,先看FrameLoder类,它是框中内容的加载器,类似于资源和资源加载器。因为Frame对象中包含Document对象,所以WebKit同样需要DocumentLoader类帮助加载HTML文档并从字节流到构建的DOM树,DocumentWriter类是一个辅助类,它会创建DOM树的根节点HTMLDocument对象,同时该类包括两个成员变量,一个用于文档的字符解码类,另一个就是HTML解释器HTMLDocumentParser类。

HTMLDocumentParser类是一个管理类,包括了用于各种工作的其他类,如字符串到词语需要用到词法分析器HTMLTokenizer类。该管理类读入字符串,输出一个词语,这些词语经过XSSAuditor做完安全检查之后,就会输出到HTMLTreeBuilder类。

HTML Tree Builder类负责DOM树的建立,它本身能够通过词语创建一个个的节点对象,然后,借由HTMLConstructionSite类来将这些点对象构建成一颗树。

下图是WebKit收到网络回复的字节流的时候,从字节流到构建DOM树的时序图,详述了调用过程,但省略了一些次要调用。ResourceLoader类和CachedRawResource类收到网络栈的数据后,调用DocumentLoader类的commitData方法,然后DocumentWriter类会创建一个根节点HTMLDocument对象,然后将数据append输送到HTMLDocumentParser对象,后面就是将其解释成词语,创建节点对象然后建立以HTMLDocument为根的DOM树。

2.2 词法分析

在进行词法分析之前,解释器首先要做的事情就是检查该网页内容使用的编码格式,以便后面使用合适的解码器。如果解释器在 HTML 网页中找到了设置的编码格式, WebKit 会使用相应的解码器来将字节流转换成特定格式的字符串。如果没有特殊格式,词法分析器 HTMLTokenizer 类可以直接进行词法分析。

词法分析的工作都是由 HTMLTokenizer 来完成 ,简单来说,它就是一个状态机---输入的是字符串,输出的是一个个词语。因为字节流可能是分段的,所以输入的字符串可能也是分段的,但是这对词法分析器来说没有什么特别之处,它会自己维护内部的状态信息。

词法分析器的主要接口是 "nextToken" 函数,调用者只需要关键字符串传入,然后就会得到一个词语,并对传入的字符串设置相应的信息,表示当前处理完的位置,如此循环,如果词法分析器遇到错误,则报告状态错误码,主要逻辑在图中给予了描述。

对于 "nextToken" 函数的调用者而言,它首先设置输入需要解释的字符串,然后循环调用 NextToken 函数,直到处理结束。 "nextToken" 方法每次输出一个词语,同时会标记输入的字符串,表明哪些字符已经被处理过了。因此,每次词法分析器都会根据上次设置的内部状态和上次处理之后的字符串来生成一个新的词语。 "nextToken" 函数内部使用了超过 70 种状态,图中只显示了 3 种状态。对于每个不同的状态,都有相应的处理逻辑。

而对于词语的类别,WebKit只定义了很少,HTMLToken类定义了6种词语类别,包括DOCTYPE、StartTag、EndTag、Comment、Character和EndOfFile,这里不涉及HTML的标签类型等信息,那是后面语法分析的工作。

2.3 XSSAuditor 验证词语

当词语生成之后,WebKit 需要使用 XSSAuditor 来验证词语流(Token Stream)。XSS 指的是 Cross Site Security , 主要是针对安全方面的考虑。

根据 XSS 的安全机制,对于解析出来的这些词语,可能会阻碍某些内容的进一步执行,所以 XSSAuditor 类主要负责过滤这些被阻止的内容,只有通过的词语才会作后面的处理。

2.4 词语到节点

经过词法分析器解释之后的词语随之被 XSSAuditor 过滤并且在没有被阻止之后,将被 WebKit 用来构建 DOM 节点。从词语到构建节点的步骤是由 HTMLDocumentParser 类调用 HTMLTreeBuilder 类的 "constructTree" 函数来实现。该函数实际上是利用ProcessToken函数来处理6种词语类型。

2.5 节点到 DOM 树

从节点到构建 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标准中的定义,节点有很多类型,如元素节点、属性节点等。同 DOM 标准一样,一切的基础都是 Node 类。在 WebKit 中, DOM 中的接口 Interface 对应于 C++ 的类,Node 类是其他类的基类,在下面的图中 显示了 DOM 的主要相关节点类。图中的 Node 类实际上继承自 EventTarget 类,它表明 Node 类能够接受事件,这个会在 DOM 事件处理中介绍。Node 类还继承自另外一个基类 ------ScriptWrappable,这个跟 JavaScript 引擎相关。

Node 的子类就是 DOM 中定义的同名接口,元素类,文档类和属性类均继承自一个抽象出来的 ContainerNode 类,表明它们能够包含其他的节点对象。回到 HTML 文档来说,元素和文档对应的类注是 HTMLElement 类和 HTMLDocument 类,实际上 HTML 规范还包含众多的 HTMLElement 子类,用于表示 HTML 语法中众多的标签。

2.6 网页基础设施

上面介绍了 Frame 、Document 等 WebKit 中的基础类,这些都是网页内部的概念,实际上,WebKit 提供了更高层次的设施,用于表示整个网页的一些类,WebKit 中的 接口部分 就是基于它们来提供的,表示网页的类既提供了构建 DOM 树等操作,同时也提供了接口用于布局。渲染等操作。

在上图中右边的部分还有一些WebKit表示网页的一些基础设施类,来描述WebKit是如何被该移植使用从而提供对外使用的接口,它们实际上是Chromium项目调用WebKit项目的接口。右边是WebKit中的WebCore提供的部分类,这些类都是公共的,也就是被不同的WebKit移植所共享。如Frame、Chrome(是个类)、ChromeClient和Page类。图中左边是WebKit的Chromium移植实现使用的接口类,其中RenderViewImpl来自Chromium项目,是Chromium使用WebKit的主要桥接类,用于Renderer进程。在WebKit的Chromium移植中,WebView和WebFrame是不得不提的两个类,它们是Chromium项目用于表示网页和网页框的接口类,另外两个类WebViewImpl和WebFrameImpl是这两个类的子类,它们负责使用Page、Frame等WebCore中类来支持两个对外类的接口。

图中右上角是Chrome(类)和ChromeClient类,这两个类很重要,它们使用一个WebKit普遍使用设计模式:

  • Chrome类需要具备获取各个平台资源的能力,可以调用Chrome类来创建一个新窗口。
  • Chrome类需要把WebKit的状态和进度等信息派发给外部的调用者或者说是WebKit的使用者。

WebKit内部同这两类需求相关的要求都是通过Chrome类的接口来完成的,WebKit使用ChromeClient抽象类ChromeClient抽象类来解决既让webKIt和外部调用者既不紧密耦合,又能方便地支持不同的平台 。Chrome类是一些公共的操作流程,而ChromeClient类是Chrome需要用到的一些接口,这些接口在不同的移植上必须有不同的实现。WebKit的Chromium移植中有ChromeClientImpl实现类。ChromeClient类可以有两类接口,第一类用来监听WebKit的内部状态信息,这其实是回调函数;第二类用来实现Chrome类所需要的跟移植相关的工作。

当Chrome类接收到网页的大小发生改变的消息时,它就会调用ChromeClient类的contentSizeChanged函数来通知Chromium浏览器。而当Chrome类需要创建一个窗口的时候,WebKit同样使用ChromeClient类来帮助创建,因为Chrome类本身没有能力创建与移植相关的这些信息。

综上所述Chrome类,它是一个非常重要的类,是WebKit与它的使用者之间的桥梁,主要负责用户界面和渲染相关的需求,实现这些需求会用到平台的接口,以上是它的这些功能。

  • 跟用户界面和渲染显示相关的需要各个移植实现接口的结合类,
  • 继承自HostWindow(宿主窗口)类,该类包含一系列接口,用来通知重绘或者更新整个窗口、滚动窗口等。
  • 窗口相关操作,如显示、隐藏窗口等。
  • 显示和隐藏窗口中的工具栏、状态栏、滚动条等。
  • 显示JavaScript相关的窗口,如JavaScript的alert、confirm、prompt窗口。

2.7 线程化的解释器

在 Renderer 进程中有一个线程,该线程用来处理 HTML 文档的解释任务,在 HTML 解释器的步骤中,WebKit 的 Chromium 移植跟其他的 WebKit 移植也存在不同之处。

线程化的解释器就是利用单独的线程来解释 HTML 文档。因为在WebKit 中,网络资源的字节流自 IO 线程传递给渲染线程之后,后面的解释、布局和渲染等工作基本上就是工作在该线程,也就是渲染线程完成的(这不是绝对的)。因为 DOM 树只能在渲染线程上创建和访问,这也就是说构建 DOM 树的过程只能在渲染线程中进行。但是,从字符到词语这个阶段可以交给单独的线程来做,Chromium 浏览器使用的就是这个思想。

具体的实现过程:

字符串 (传给)=> HTMLDocumentParser类 (创建一个新的对象)=> BackgroundHTMLParser 来负责处理 (交给)=> 前一步创建的对象

WebKit 会检查是否需要创建用于解释字符串的线程 HTMLParserThread 。如果该线程已存在,WebKit 就将刚刚的任务传递给这一新线程, 下图描述了这一过程。

在 HTMLParserThread 线程中,WebKit 所做的事情包括将字符串解释成一个个词语,然后使用之前提到的 XSSAuditor 进行安全检查。这是在一个新的线程中执行。主要区别在于解释成词语之后,WebKit 会分批次地将结果词语传递给渲染线程。

2.8 JavaScript 的执行

在 HTML 解释器的工作过程中,可能会有 JavaScript 代码(全局作用域的代码)需要执行,它发生在将字符串解释成词语之后、创建各种节点的时候。这也是全局执行的 JavaScript 代码不能访问 DOM 树的原因------因为 DOM 树还没有被创建完。

WebKit将DOM树创建过程中需要执行的JavaScript代码交由HTMLScriptRunner类来负责。工作方式很简单,就是利用JavaScript引擎来执行Node节点中包含的代码。细节见HTML Script Runner::excuteParsingBlockingScript方法。

因为JavaScript代码可能会调用如"document.write()"来修改文档结构,所以JavaScript代码的执行会阻碍后面节点的创建,同时当然也会组在后面资源的下载,这时候WebKit对需要什么资源一无所知,这导致了资源不能够并发地下载这一严重影响性能的问题。下图示例一,JavaScript代码的执行阻碍了后面img图片的下载。当该Script节点使用src属性的时候,情况变得更差,因为WebKit还需要等待网络获取JavaScript文档,所以关于JavaScript的使用有以下两点建议。

1、将 "script" 元素加上 "async" 属性,表明这是一个可以异步执行的 JavaScript 代码。在HTMLScriptRunner类中就有相应的函数执行异步的JavaScript代码,图中示例二给出了使用方法。

2、 将 "script" 元素放在 "body" 元素的最后,这样它不会阻碍其他资源的并发下载。

但是不这样做的时候,WebKit 使用预扫描和预加载机制来实现资源的并发下载而不被 JavaScript 的执行所阻碍。

具体做法是:当遇到需要执行 JavaScript 代码的时候,WebKit 先暂停当前 JavaScript 代码的执行,使用预先扫描器 HTMLPreloadScanner 类来扫描后面的词语。如果 WebKit 发现它们需要使用其他资源,那么使用预资源加载器 HTMLPreloadScanner 类来发送请求,在这之后,才执行 JavaScript代码。预先扫描器本身并不创建节点对象,也不会构建 DOM 树,所以速度比较快。

当 DOM 树构建完之后,WebKit 触发 "DOMContentLoaded" 事件,注册在该事件上的 JavaScript 函数会被调用。当所在资源都被加载完之后,WebKit 触发 "onload" 事件。

2.9 实践:理解DOM树

实际查看网页的DOM树结构,具体的步骤如下:

(1)打开Chrome浏览器的开发者工具,点击console控制台按钮。

(2)在控制台中输入"document",可以看到整个文档,而且有"#document"表示的根节点,这个是额外附加在上面的,表示的是HTMLDocumen。下图左边第一行显示的就是document节点。另外,还可以尝试输入"document.firstChild",显然是html节点。接下来就是对DOM树的遍历,可以使用firstChild获得第一个子女,然后使用nextSibling来获得子女的兄弟。

(3)在控制台中输入"document",会看到Chrome浏览器提供一个候选列表,该列表中列举了所有的Document对象的属性和方法,可以得到一个非常长的列表,这些方法和属性在JavaScript代码中都可以被访问或者调用、

​​​​​​​

相关推荐
su1ka1113 分钟前
re题(35)BUUCTF-[FlareOn4]IgniteMe
前端
测试界柠檬5 分钟前
面试真题 | web自动化关闭浏览器,quit()和close()的区别
前端·自动化测试·软件测试·功能测试·程序人生·面试·自动化
多多*6 分钟前
OJ在线评测系统 登录页面开发 前端后端联调实现全栈开发
linux·服务器·前端·ubuntu·docker·前端框架
2301_801074156 分钟前
TypeScript异常处理
前端·javascript·typescript
小阿飞_8 分钟前
报错合计-1
前端
caperxi9 分钟前
前端开发中的防抖与节流
前端·javascript·html
霸气小男9 分钟前
react + antDesign封装图片预览组件(支持多张图片)
前端·react.js
susu108301891110 分钟前
前端css样式覆盖
前端·css
学习路上的小刘12 分钟前
vue h5 蓝牙连接 webBluetooth API
前端·javascript·vue.js
&白帝&12 分钟前
vue3常用的组件间通信
前端·javascript·vue.js