每天一道面试题(11)- dom是如何渲染的


输入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 如下所示:

image

第二阶段: 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 元素解析完成。

image

image

重复这个过程直到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 脚本是依赖样式表的,这又多了一个阻塞过程

image

总结

通过上面的分析,我们知道了 JavaScript 会阻塞 DOM 生成,而样式文件又会阻塞 JavaScript 的执行,所以在实际的工程中需要重点关注 JavaScript 文件和样式表文件,使用不当会影响到页面性能

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值