vue在浏览器中对DOM渲染探究
Posted yerikyu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue在浏览器中对DOM渲染探究相关的知识,希望对你有一定的参考价值。
“世间万物都由分子构成。用气将万物的分子打散,分解眼前事物,再将分子重组,在短暂的瞬间,可以凝成时空停顿,甚至逆转时空。”
这句熟悉的电影台词想必大家能猜出是哪一招武林绝学吧?是的,万事万物无时无刻不在变化,譬如与我们息息相关的DOM树,用“气”将树打散,再将树重组,我们的页面就动起来了!
Vue渲染流程
vuejs有两个阶段:编译时和运行时。
编译时
我们平常开发时写的.vue
文件是无法直接运行在浏览器中的,所以在webpack
编译阶段,需要通过vue-loader
将.vue
文件编译生成对应的js
代码,vue
组件对应的template
模板会被编译器转化为render
函数。
运行时
接下来,当编译后的代码真正运行在浏览器时,便会执行render
函数并返回VNode
,也就是所谓的
虚拟DOM
,最后将VNode
渲染成真实的DOM
节点。
浏览器渲染过程
浏览器接收到 HTML 文件并转换为 DOM 树,将 CSS 文件转换为 CSSOM
在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。因为样式你可以自行设置给某个节点,也可以通过继承获得。在这一过程中,浏览器需要递归CSSOM
树,然后确定具体的元素到底是什么样式。
生成渲染树(Render Tree)
当我们生成DOM
树和CSSOM
树以后,就需要将这两棵树组合为渲染树。
在这一过程中,不是简单的将两者合并就行了。渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是display: none
的,那么就不会在渲染树中显示。
Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
Display: 将像素发送给GPU,最后通过调用操作系统Native GUI的API绘制,展示在页面上。(这一步其实还有很多内容,比如会在GPU将多个合成层合并为同一个层,并展示在页面中。而css3硬件加速的原理则是新建合成层,这里我们不展开,之后有机会再写一篇博客来介绍)
渲染过程看起来也不复杂,让我们来具体了解下每一步具体做了什么。
构建DOM详细流程
浏览器会遵守一套步骤将html 文件转换为 DOM 树。宏观上,可以分为几个步骤:
浏览器从磁盘或网络读取HTML的原始字节(字节数据),并根据文件的指定编码(例如 UTF-8)将它们转换成字符串。
在网络中传输的内容其实都是 0 和 1 这些字节数据。当浏览器接收到这些字节数据以后,它会将这些字节数据转换为字符串,也就是我们写的代码。
将字符串转换成Token,例如:"yerik"等。Token中会标识出当前Token是“开始标签”或是“结束标签”或着是“文本”等信息。
这时候你一定会有疑问,节点与节点之间的关系如何维护?
事实上,这就是Token要标识起始标签
和结束标签
等标识的作用。
例如“title”Token的起始标签和结束标签之间的节点肯定是属于“head”的子节点。
上图给出了节点之间的关系,例如:“Hello”Token位于“title”开始标签与“title”结束标签之间,表明“Hello”Token是“title”Token的子节点。同理“title”Token是“head”Token的子节点。
生成节点对象并构建DOM
事实上,构建DOM的过程中,不是等所有Token都转换完成后再去生成节点对象,而是一边生成Token一边消耗Token来生成节点对象。换句话说,每个Token被生成后,会立刻消耗这个Token创建出节点对象。注意:带有结束标签标识的Token不会创建节点对象。
接下来我们举个例子,假设有段HTML文本:
<html>
<head>
<title>Web page parsing</title>
</head>
<body>
<div>
<h1>Web page parsing</h1>
<p>This is an example Web page.</p>
</div>
</body>
</html>
上面这段HTML会解析成这样:
构建CSSOM详细流程
DOM会捕获页面的内容,但浏览器还需要知道页面如何展示,所以需要构建CSSOM。构建CSSOM的过程与构建DOM的过程非常相似,当浏览器接收到一段CSS,浏览器首先要做的是识别出Token,然后构建节点并生成CSSOM。
graph LR
A[字节数据]-->B[字符串]
B-->C[Token]
C-->D[Node]
D-->E[CSSOM]
在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。因为样式你可以自行设置给某个节点,也可以通过继承获得。在这一过程中,浏览器得递归 CSSOM 树,然后确定具体的元素到底是什么样式。
注意:CSS匹配HTML元素是一个相当复杂和有性能问题的事情。所以,DOM树要小,CSS尽量用id和class,千万不要过渡层叠下去。
构建渲染树
当我们生成 DOM 树和 CSSOM 树以后,就需要将这两棵树组合为渲染树。
在这一过程中,不是简单的将两者合并就行了。渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是display: none
的,那么就不会在渲染树中显示。
注意:渲染树只包含可见的节点
我们或许有个疑惑:浏览器如果渲染过程中遇到JS文件怎么处理?
渲染过程中,如果遇到<script>
就停止渲染,执行JS
代码。因为浏览器有GUI渲染线程与JS引擎线程,为了防止渲染出现不可预期的结果,这两个线程是互斥的关系。javascript
的加载、解析与执行会阻塞DOM的构建,也就是说,在构建DOM时,HTML解析器若遇到了JavaScript
,那么它会暂停构建DOM,将控制权移交给JavaScript
引擎,等JavaScript
引擎运行完毕,浏览器再从中断的地方恢复DOM构建。
也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将script
标签放在body
标签底部的原因。当然在当下,并不是说script
标签必须放在底部,因为你可以给 script
标签添加 defer(延迟) 或者 async(异步) 属性(下文会介绍这两者的区别)。
JS文件不只是阻塞DOM
的构建,它会导致CSSOM
也阻塞DOM的构建。
原本DOM和CSSOM
的构建是互不影响,井水不犯河水,但是一旦引入了JavaScript
,CSSOM
也开始阻塞DOM的构建,只有CSSOM
构建完毕后,DOM再恢复DOM构建。
阻塞渲染
这是因为JavaScript
不只是可以改DOM
,它还可以更改样式,也就是它可以更改CSSOM
。因为不完整的CSSOM
是无法使用的,如果JavaScript
想访问CSSOM
并更改它,那么在执行JavaScript
时,必须要能拿到完整的CSSOM
。所以就导致了一个现象,如果浏览器尚未完成CSSOM
的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟脚本执行和DOM构建,直至其完成CSSOM
的下载和构建。也就是说,在这种情况下,浏览器会先下载和构建CSSOM
,然后再执行JavaScript,最后在继续构建DOM
。
首先渲染的前提是生成渲染树,所以HTML
和CSS
肯定会阻塞渲染。如果你想渲染的越快,你越应该降低一开始需要渲染的文件大小,并且扁平层级,优化选择器。
然后当浏览器在解析到script
标签时,会暂停构建DOM
,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载JS
文件,这也是都建议将script
标签放在body
标签底部的原因。
当然在当下,并不是说script
标签必须放在底部,因为你可以给script
标签添加defer
或者async
属性。
当script
标签加上defer
属性以后,表示该JS
文件会并行下载,但是会放到HTML
解析完成后顺序执行,所以对于这种情况你可以把script
标签放在任意位置。
对于没有任何依赖的JS
文件可以加上async
属性,表示JS
文件下载和解析不会阻塞渲染。
为什么操作 DOM 慢
想必大家都听过操作DOM
性能很差,但是这其中的原因是什么呢?
因为DOM
属于渲染引擎中的东西,而JS
又是JS
引擎中的东西。当我们通过JS
操作DOM
的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗。操作DOM
次数一多,也就等同于一直在进行线程之间的通信,并且操作DOM
而且可能还会带来重绘回流的情况,所以也就导致了性能上的问题。
经典面试题:插入几万个 DOM,如何实现页面不卡顿?
首先我们肯定不能一次性把几万个DOM
全部插入,这样肯定会造成卡顿,所以解决问题的重点应该是如何分批次部分渲染DOM
。大部分人应该可以想到通过requestAnimationFrame
的方式去循环的插入DOM
,其实还有种方式去解决这个问题:虚拟滚动virtualized scroller
。
这种技术的原理就是只渲染可视区域内的内容,非可见区域的那就完全不渲染了,当用户在滚动的时候就实时去替换渲染的内容。
重绘(Repaint)和回流(Reflow)
当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为自动重排。
布局流程的输出是一个盒模型,它会精确地捕获每个元素在视口内的确切位置和尺寸,所有相对测量值都将转换为屏幕上的绝对像素。
布局完成后,浏览器会立即发出“Paint Setup”和“Paint”事件,将渲染树转换成屏幕上的像素。
重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能。
- 重绘是当节点需要更改外观而不会影响布局的,比如改变
color
就叫称为重绘 - 回流是布局或者几何属性需要改变就称为回流。
回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。
以下几个动作可能会导致性能问题:
- 改变
window
大小 - 改变字体
- 添加或删除样式
- 文字改变
- 定位或者浮动
- 盒模型
并且很多人不知道的是,重绘和回流其实也和Eventloop
有关。
当Eventloop
执行完Microtasks
后,会判断document
是否需要更新,因为浏览器是60Hz
的刷新率,即每16.6ms
才会更新一次。
然后判断是否有resize
或者scroll
事件,有的话会去触发事件,所以resize
和scroll
事件也是至少16ms
才会触发一次,并且自带节流功能。
判断是否触发了 media query
- 更新动画并且发送事件
- 判断是否有全屏操作事件
- 执行
requestAnimationFrame
回调 - 执行
IntersectionObserver
回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好 - 更新界面
以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行requestIdleCallback
回调。
结语
综上所述,我们得出这样的结论:
- 浏览器工作流程:构建DOM -> 构建CSSOM -> 构建渲染树 -> 布局 -> 绘制。
- CSSOM会阻塞渲染,只有当CSSOM构建完毕后才会进入下一个阶段构建渲染树。
- 通常情况下DOM和CSSOM是并行构建的,但是当浏览器遇到一个不带defer或async属性的script标签时,DOM构建将暂停,如果此时又恰巧浏览器尚未完成CSSOM的下载和构建,由于JavaScript可以修改CSSOM,所以需要等CSSOM构建完毕后再执行JS,最后才重新DOM构建。
事实上,不管再怎么打散,再怎么重组,本质上都是套娃,都是卷起来,想当年,上古时代,前端开发们都是手撕html,手撸css,本意是为了减少前端开发难度,然而为了实现这个初衷,进行相关的框架开发,又引入新的概念,为了学习这些概念又要增加新的学习难度,套娃了其实,但本质上都是在冯诺依曼模型指导下的具体实现,浏览器也好,编译器也罢,都是在承担这个运算器和控制器的角色
参考资料
- https://blog.51cto.com/nu1l/5176189
- https://juejin.cn/post/7046211513161351204
- https://segmentfault.com/a/1190000017329980
- https://github.com/ljianshu/Blog/issues/51
以上是关于vue在浏览器中对DOM渲染探究的主要内容,如果未能解决你的问题,请参考以下文章