在 输入url到界面渲染完成发生了什么 这道题中有一个流程是 浏览器根据请求到的html 渲染一棵DOM树
那么请问, 这个DOM树是怎么渲染的?
在渲染引擎内部,有一个叫HTML 解析器(HTMLParser)的模块,它的职责就是负责将 HTML 字节流转换为 DOM 结构
HTML 解析器并不是等整个文档加载完成之后再解析的,而是网络进程加载了多少数据,HTML 解析器便解析多少数据。
DOM树渲染的主要工作原理
- 根据响应头中的 content-type 字段来判断文件的类型. content-type 的值是“text/html”,那么浏览器就会判断这是一个 HTML 类型的文件,然后为该请求选择或者创建一个渲染进程
- 渲染进程准备好之后,网络进程和渲染进程之间会建立一个共享数据的管道,网络进程接收到数据后就往这个管道里面放,而渲染进程则从管道的另外一端不断地读取数据,并同时将读取的数据“喂”给 HTML 解析器。
具体工作流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UDDb8qIV-1632822936953)(http://blog.poetries.top/img-repo/2019/11/57.png)]
第一阶段,通过分词器将字节流转换为 Token。
V8 编译 JavaScript 过程中的第一步是做词法分析,将 JavaScript 先分解为一个个 Token。解析 HTML 也是一样的,需要通过分词器先将字节流转换为一个个 Token,分为 Tag Token 和文本 Token。上述 HTML 代码通过词法分析生成的 Token 如下所示:
第二阶段: Token 解析为 DOM 节点
第三阶段: DOM 节点添加到 DOM 树中
这两个阶段是同步进行的
HTML 解析器维护了一个Token 栈结构,该 Token 栈主要用来计算节点之间的父子关系,在第一个阶段中生成的 Token 会被按照顺序压到这个栈中。
- 如果压入到栈中的是StartTag Token,HTML 解析器会为该 Token 创建一个 DOM 节点,然后将该节点加入到 DOM 树中,它的父节点就是栈中相邻的那个元素生成的节点。
- 如果分词器解析出来是文本 Token,那么会生成一个文本节点,然后将该节点加入到 DOM 树中,文本 Token 是不需要压入到栈中,它的父节点就是当前栈顶 Token 所对应的 DOM 节点。
- 如果分词器解析出来的是EndTag 标签,比如是 EndTag div,HTML 解析器会查看 Token 栈顶的元素是否是 StarTag div,如果是,就将 StartTag div 从栈中弹出,表示该 div 元素解析完成。
重复这个过程直到DOM解析完成
总结
所以由HTML解析为DOM的流程如下
- 浏览器根据响应头中的
content-type
字段为text/html
判断是一个html, 于是创建一个渲染进程, 渲染进程和网络进程在管道中同步工作 - V8引擎拿到数据之后先进行词法分析, 将数据先解析成为token
- html解析器 维护了一个token栈, 将V8引擎解析出来的token一个个压入token栈中.
- 如果是开始标签, 则入栈并创建一个DOM
- 如果是文本token, 就直接加入DOM树中
- 如果是结束标签, 则判断栈顶标签是否匹配, 如果匹配则弹出栈, 代表这个元素就解析完成, 直到所有token都处理完
其他特殊情况
有js标签怎么办
<html>
<body>
<div>1</div>
<script>
let div1 = document.getElementsByTagName('div')[0]
div1.innerText = 'time.geekbang'
</script>
<div>test</div>
</body>
</html>
解析到script标签时,渲染引擎判断这是一段脚本, HTML 解析器就会暂停 DOM 的解析,JavaScript 引擎介入,并执行 script 标签中的这段脚本,因为这段 JavaScript 脚本修改了 DOM 中第一个 div 中的内容,所以执行这段脚本之后,div 节点内容已经修改为 time.geekbang 了。脚本执行完成之后,HTML 解析器恢复解析过程,继续解析后续的内容,直至生成最终的 DOM。
引入外部js的情况
//foo.js
let div1 = document.getElementsByTagName('div')[0]
div1.innerText = 'time.geekbang'
<html>
<body>
<div>1</div>
<script type="text/javascript" src='foo.js'></script>
<div>test</div>
</body>
</html>
和上面代码的区别是执行 JavaScript 时,需要先下载这段 JavaScript 代码, 这会阻塞DOM的渲染, 而且下载过程很耗时
不过 Chrome 浏览器做了很多优化,其中一个主要的优化是预解析操作。当渲染引擎收到字节流之后,会开启一个预解析线程,用来分析 HTML 文件中包含的 JavaScript、CSS 等相关文件,解析到相关文件之后,预解析线程会提前下载这些文件。
所以, 我们通常会对js的加载做一些优化, 比如异步加载, 延迟加载等等
<script async type="text/javascript" src='foo.js'></script>
<script defer type="text/javascript" src='foo.js'></script>
async和defer的区别?
async与defer的区别是:前者一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染;后者要等到整个页面正常渲染结束,才会执行。
一句话,async是“下载完就执行”,defer是“渲染完再执行”。 另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。
css对渲染的影响
<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>
该示例中,JavaScript 代码出现了 div1.style.color = ‘red’ 的语句,它是用来操纵 CSSOM 的,所以在执行 JavaScript 之前,需要先解析 JavaScript 语句之上所有的 CSS 样式。所以如果代码里引用了外部的 CSS 文件,那么在执行 JavaScript 之前,还需要等待外部的 CSS 文件下载完成,并解析生成 CSSOM 对象之后,才能执行 JavaScript 脚本。
而 JavaScript 引擎在解析 JavaScript 之前,是不知道 JavaScript 是否操纵了 CSSOM 的,所以渲染引擎在遇到 JavaScript 脚本时,不管该脚本是否操纵了 CSSOM,都会执行 CSS 文件下载,解析操作,再执行 JavaScript 脚本。
所以 JavaScript 脚本是依赖样式表的,这又多了一个阻塞过程
总结
通过上面的分析,我们知道了 JavaScript 会阻塞 DOM 生成,而样式文件又会阻塞 JavaScript 的执行,所以在实际的工程中需要重点关注 JavaScript 文件和样式表文件,使用不当会影响到页面性能