图解-你可能需要知道的DOM

583 阅读4分钟

在去年我写过一篇关于vue转小程序的原生的webpack-loader的文章,里面主要就是对于vue-template的语法解析。其中我看了mpvue源码,结果是懂非懂。最近在看一个浏览器工作原理的实践课程,其中就有说到dom树是如何构建的,看完后恍然大悟,今天就来掰扯掰扯。

浏览器渲染引擎内部,有一个叫HTML解析器(HTMLParser)的模块,它的职责就是负责将HTML字节流转换成DOM结构。

DOM 树如何生成

生产中我们会通过网络请求到需要的页面,浏览器通过响应头中的content-type:text/html字段判断出是一个HTML类型的文件,然后为该请求选择或者创建一个渲染进程。渲染进程准备好后,网络进程和渲染进程之间会建立一个共享的管道,你可以想象网络进程把源源不断的数据放入管道,而渲染引擎在管道另一头接受,渲染引擎中的HTML解析器会动态的接受字节流,将其解析成DOM。

V8 编译 JavaScript 过程中的第一步是做词法分析,将 JavaScript 先分解为一个个 Token。解析 HTML 也是一样的,需要通过分词器先将字节流转换为一个个 Token,分为 Tag Token 和文本 Token,下面是一个token的组成结构。

由图可以看出,Tag Token 又分 StartTag 和 EndTag,比如就是 StartTag ,就是EndTag,分别对于图中的蓝色和红色块,文本 Token 对应的绿色块。

在这里HTML解析器维持着一个TOKEN栈结构,这个栈会对应着DOM的节点。如果我们有如下代码:

<html>
<body>
    <div>test</div>
    <div>test2</div>
</body>
</html>

对应的token栈:

注意:HTML 解析器开始工作时,会默认创建了一个根为 document 的空 DOM 结构

对应的dom:

然后按照同样的流程解析出来 StartTag body 和 StartTag div,其 Token 栈和 DOM 的状态如下图所示:

解析出第一个文本的Token时的状态

再接下来,分词器解析出来第一个 EndTag div,这时候 HTML 解析器会去判断当前栈顶的元素是否是 StartTag div,如果是则从栈顶弹出 StartTag div,

按照同样的规则,一路解析,最终结果如下图所示

通过上面的介绍,相信你已经清楚 DOM 是怎么生成的了。不过在实际生产环境中,HTML 源文件中既包含 CSS 和 JavaScript,又包含图片、音频、视频等文件,所以处理过程远比上面这个示范 Demo 复杂。

番外

JavaScript 是如何影响 DOM 生成的

我们知道js进程和ui渲染进程是互斥的,当执行js代码的时候会暂停dom的生成,原因也是确保能输出正常安全的节点出现在页面上。在我们正常的生产环境中,我们JavaScript文件都是需要下载的,而js文件的下载过程也是会阻塞dom解析,通常下载的过程又是非常耗时且容易受到环境,文件大小等外界因素的影响。 这里会提到一个番外知识,deferasync来做到优化。

 <script async type="text/javascript" src='foo.js'></script>
 <script defer type="text/javascript" src='foo.js'></script>

async 和 defer 虽然都是异步的,不过还有一些差异,使用 async 标志的脚本文件一旦加载完成,会立即执行;而使用了 defer 标记的脚本文件,需要在 DOMContentLoaded 事件之前执行。

那么CSS呢?

举个栗子:

//theme.css
div {color:blue}
<html>
    <head>
        <style src='theme.css'></style>
    </head>
<body>
    <div>1</div>
    <script>
            let div1 = document.getElementsByTagName('div')[0]
            div1.innerText = 'time.geekbang' //需要DOM
            div1.style.color = 'red'  //需要CSSOM
        </script>
    <div>test</div>
</body>
</html>

在这里js操作了style.color去修改CSSOM,所以在执行javascript之前,需要解析js语句上所有的css样式。如果引用了外部的css文件,那么在实行js之前还需要等待css文件下载完成,并解析生成CSSOM对象,才能执行js脚本。

而 JavaScript 引擎在解析 JavaScript 之前,是不知道 JavaScript 是否操纵了 CSSOM 的,所以渲染引擎在遇到 JavaScript 脚本时,不管该脚本是否操纵了 CSSOM,都会执行 CSS 文件下载,解析操作,再执行 JavaScript 脚本。所以说 JavaScript 脚本是依赖样式表的,这又多了一个阻塞过程。

总结

首先我们介绍了 DOM 是如何生成的,然后又基于 DOM 的生成过程分析了 JavaScript 是如何影响到 DOM 生成的。因为 CSS 和 JavaScript 都会影响到 DOM 的生成,希望能帮助你对浏览器中的dom又有了更深的认识。