blog icon indicating copy to clipboard operation
blog copied to clipboard

HTML代码是如何被解析成浏览器中的DOM对象的

Open jackiewillen opened this issue 5 years ago • 0 comments

目录:

一、代码如何被解析成DOM对象

二、节点的方法是如何添加到DOM上的

三、简单模仿浏览器挂载DOM方法

四、看JSDom源码的疑问

五、了解元素的实例化

一、代码如何被解析成DOM对象

就以如下的一行代码为例,开始探索代码解析的历程,先提出第一个疑问,这三行代码,是怎样解析成DOM对象的?

// 代码如下
<div id="exampleId">
    <span>hello world</span>
</div>

DOM对象如下:

image

截图中生成AST的链接,你可以去点击尝试修改HTML代码生成抽象语法树试试。这是浏览器拿到我们所写的HTML文本代码第一步所要完成的事,因为文本的格式是不便于操作的,比如现在要去一个div节点的属性id的内容(exampleId),你说怎么取,估计你能想到用正则表达式,但是不能就靠正则表达式过日子啊,要取的属性各种各样,还有自定义的属性要取,这时候正则表达式表示也顶不住了。只有把文本的内容生成一定的数据结构才方便对其进行操作(如这边生成的AST抽象语法树)。这样通过类似document.html.body.div.attrs.id就可以成功取到。

如果你对浏览器将HTML生成抽象语法树的细节感兴趣,看chrome源代码几乎是不太可行的,你可以查看这样一个GitHub仓库:parse5,这个是用JS实现的,方便查看,你甚至还可以查看一个正则表达式版本的,也就是我前面提到的啥都用正则表达式来处理的,GitHub仓库:html-parse-stringify

二、节点的方法是如何添加到DOM上的

解决了第一个问题,产生了一个简单的抽象语法树,接下来问题又来了,平时我们用的document.getElementByTagName在我们这个抽象语法树上并没有,我们只有简单的HTML结点的属性nodeName,tagName之类的,并没有这些方法,那么这些获取节点的方法又是从哪来的呢?

注:在chrome的控制台中直接输入document或使用console.log(document)是没法查看document属性的,
需要使用console.dir(document)来查看其属性。查看的属性如下图所示:

image

打印出浏览器的docuemnt对象后,我们在原型链上向下翻了好几层,终于找到了我们想要找的getElementByTagName,是挂载在Document这样的一个原型链处,如下图所示。为什么又来一个Document,和我们之前的console.dir(document)中的document有什么不同?

image image

到了这边就需要搬出我们的W3C标准来了,为什么打印出来的document对象上有那么多的属性,有那么长的原型链?因为这些都是W3C标准规定的,就以一个上图中的属性字段URL为例,都是W3C规定的Docuemnt上需要有哪些属性,可以点击这里查看所定义的属性,查看这里。可以看出document是一个类似与Document的实例,实际上他们中间只是又隔了一层HTMLDocument。

那么原型链又是如何定义的呢,在W3C标准中可以看到是通过接口继承来实现的,你可以在标准中查到如下字样:

interface Document : Node 

这就说明Document是继承自Node接口的。这就是原型链的由来,我们把原型链从顶端到低端都画出来,大概就是下面这样了。 image

从这里我们就可以看出,一个Document是一个W3C定义好的接口,另一个是HTMLDocument的实例化。

接下来我们再一起看看,这条原型链上每个节点的定义:

HTMLDocument接口的定义
现行标准:https://html.spec.whatwg.org/multipage/window-object.html#htmldocument
MDN:https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLDocument

Document接口的定义
现行标准:https://dom.spec.whatwg.org/#interface-document
MDN: https://developer.mozilla.org/zh-CN/docs/Web/API/Document

Node接口的定义
现行标准:https://dom.spec.whatwg.org/#interface-node
MDN: https://developer.mozilla.org/zh-CN/docs/Web/API/Node

EventTarget接口的定义
现行标准:https://dom.spec.whatwg.org/#interface-eventtarget
MDN: https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget

三、简单模仿浏览器挂载DOM方法

看了这些标准之后,你是不是想看看浏览器是如何将这些标准中定义的各种属性及方法挂载到我们刚才通过parse5生成的简单的AST(抽象语法树)上的。还是之前说的,看Chrome源代码不太现实,作为前端工程师最好的就是看看有没有JS实现版本的,恭喜,找到了一个JSDom,虽然和Chrome源代码实现机理不可能100%相似,当原理应该相差不多。接下来就一起看看getElementByTagName这个方法是怎么挂载到我们的AST上去的。

在JSDom这里找到了whatwg接口的定义,和我们看到的标准中的定义相差无几。这边是JSDom中给出的定义文件。接下来看看JSDom的实现

image

可以看出其大概原理就是遵守w3c的标准将所需要的方法挂载到prototype上去了。如果在我们生成的AST树上挂载,大概就是这样

ASTTree.prototype.getElementsByTagName = function(tagName) {
    // 如下代码只是为说明大致含义,并不能正常运行
    return this.ASTTree.html.body.childNodes.forEach((child) => {
         child.tagName = tagName;
   })
}

这样你要根据TagName获取到对应的节点,就可以很方便的使用已经封装好的这个getElementsByTagName了,这样就基本有了我们的DOM树的雏形了。

四、看JSDom源码的疑问

其实当你看JSDom代码和w3c标准的时候,可能会遇到一些疑惑,这里我把我自己遇到的几个疑惑耗时较长的一个说明一下,避免你也踩坑。

为什么在chrome devtools中用console.dir(document)中有好多的事件,如onauxclick、 onblur、onabort这些在jsdom对于document实现的文档中都没有。 image

后来才发现,原来是通过使用Mixin来实现的,这些在标准中也都有定义

// 具体在jsdom中的实现
mixin(DocumentImpl.prototype, GlobalEventHandlersImpl.prototype);
// 标准中的定义
Document includes GlobalEventHandlers;

五、了解元素的实例化

以上只是大致了解了浏览器document实例化的形成过程,趁热打铁,把很相似的元素的实例化也了解一下。不懂元素的实例化没关系,来看个例子就了解。打开devtools,在Element的Tab中随意选中一个节点,然后选择右侧的Properties的Tab,就可以看到这个元素实例的原型链。

image 上图中的div#wrapper就是对HTMLDivElement的实例化。类似于浏览器中的document是对HTMLDocument的实例化是一个道理。对比上面画出的原型链图,这边也可以画出一个:

image

每个原型链节点都可以在w3c标准或者MDN中找到定义,实现的过程也和上面的AST上添加方法类似,这些在jsdom中都可以找到。

至此,我们基本上搞清楚了我们平时为什么写一段文本的HTML代码,到了浏览器中就有个document对象可用,就可以有好多类似onclick,getElementBy....可以用。通过getElementById获取到某一个节点后又会有innerHTML,outerHTML等可以使用。这些原来都是根据代码建立AST后在树上根据W3C标准添加上了各种方法就形成了你的document,和element等等。

jackiewillen avatar Sep 15 '19 10:09 jackiewillen