一、首先看一段简单的代码
const sNode = document.getElementById('span');
let i = 0;
while(1){
if(i>10000000000){
break;
}
i++;
}
})
sNode.innerText = '32'
return (
<div>
<span id='span'>你好</span>
</div>
)
它的效果如下图
二、由此我们得出一个大体的形状
因为浏览器好像在等我们的js代码执行完毕之后再进行了渲染。
三、介绍现代浏览器的组成
- 浏览器进程:负责控制chrom浏览器除了标签页外的用户界面,包括地址栏,书签,后退,前进按钮等以及负责与浏览器其他进程协调工作
- 网络进程:负责发起接受网络请求
- GPU进程:负责整个浏览器界面的渲染
- 插件进程:负责控制网站还用的插件。这里说的插件不是chrom 市场里开发模式安装的扩展,而是想flash这样的插件
- 渲染器进程:用来控制和显示tab标签内的所有内容,浏览器默认情况下会为每一个TAb标签页创建一个进程
这里为什么说到默认,这是因为跟你运行chrom时选择的进程模型有关!默认的进程模型是 Process - pre-site-instance,还有其他的自己看 process - pre -site - instance 进程模型的优点是:安全,快速,流畅 。 缺点也是显而易见的:每次打开新的siteinstance 都会占据内存。 在此模式下:chrome会为每一个siteinstance创建一个渲染器进程(从一个页面链开的属于同一个siteinstance),但是这个进程是有限的!他是根据你的机器来的,你的机器内内存大,那么你的渲染器进程数就多,如果我本身只能创建20个siteinstance(实例),那么当我在打开一个标签输入网址时会怎么样,这个时候渲染器进程,就会随意选择前面20个进程中的一个来执行你现在创建的siteinstance
三、工作流程
首先省去在地址栏输入网址到获取数据的一系列过程,我们直接到后台返回数据到浏览器进程
-
这个时候浏览器进程(Browser process)通过 IPC进程通信管道给 渲染器进程(Browser process)传递数据(就是我们的页面) ------- 此时正式进入渲染流程。 ------- 渲染器进程(render process)和核心任务就是把html、css、js、image等资源渲染成用户可交互的web页面
-
渲染器进程(Browser process)的主线程(main thread)将HTML进行解析,构造Dom数据结构。
-
html首先进过标记化tokeniser,通过词法分析将输入的html内容解析成多个标记,进行DOM树构造 --->tree construction 树构造
-
在DOM Tree构造过程中,会创建document 对象,然后以document为根节点的DOM Tree不断进行修改,向其添加元素。
-
在html 代码中往往会引入一些额外的资源,比如图片、css、js脚本等,图片和css这些资源要通过网络下载或者从缓存中直接加载这些资源是不会阻塞HTML的解析的,因此他们并不会影响DOM的生成,但是当HTML文件解析过程中,遇到script标签时。
-
当在解析HTML文件过程中,如果遇到script标签时,就会停止HTML解析流程,转而去加载解析并执行java script ,为什么HTML解析过程不能像遇到CSS、图片那样跳过,非要在js这块停止,等待并执行js后才去接着执行HTML的解析
-
这是因为浏览器不清楚js执行的脚本,会不会改变到当前已经在构造的DOM Tree(当前页面的HTML结构)比如:js代码里调用了document.write方法来修改HTML,那之前的HTML解析就没有任何意义了。这也就是说我们为什么要把script标签放到body的下面。因为在解析HTML过程中,遇到js会停下来!等运行完js才会接着解析HTML,这样的后果是白屏!当然将script放在body下面是一种方式,还可以通过 defer 和 async 来加载script
-
当HTML解析完成后,我们就会获得一个DOM Tree,但是此时我们还不知道DOM Tree每个节点应该长什么样子。
-
此时渲染器进程(Browser Process)的主线程(main Thread)就需要解析下载好的css了,并确定每个DOM节点的每个计算样式(即使你没有提供自定义的css样式,浏览器也是会有自己默认的样式表,chrome里的源码是有的)
-
这个时候就是这样的DOM Tree 了
-
接下来我们要知道每个节点对应放到页面的哪个位置(也就是节点的坐标和该节点占用多大的区域) -------- 这个阶段称为 Layout 布局
-
渲染器进程(render Process)的主线程(main Thread)通过遍历DOM和计算好的样式来生成Layout Tree
-
Layout Tree每一个节点上都记录了x,y坐标和边框尺寸。注意点:DOM Tree 和 Layout Tree 并不是一一对应的!例如 DOM Tree 上的有个节点的属性是display:none ,那么DOM Tree上这个节点是存在的但是Layout Tree上节点是不存在的。
同样的比如在before伪类中添加了content属性值,他会显示在Layout Tree ,不会显示在DOM Tree上。这是因为DOM是通过HTML解析获得的并不关心样式。而Layout Tree是根据DOM Tree 和计算好的样式来生成的
DOM Tree : 他是根据HTML解析获得的
Layout Tree: 他是根据DOM Tree 和计算好的样式生成的。它和最后展示在屏幕上的节点是一一对应的。
- 到这里我们已经知道了元素的大小、形状和位置。这还是不够的,我们必须还要知道以什么样的顺序进行绘制Paint这个节点。我们知道z-index 可以改变层级,所以一定要按顺序来绘制Paint节点,不然就会出现狗头放在人前面的笑话
- 所以为了保证屏幕上展示正确的层级,主线程(main Thread) 遍历Layout Tree创建一个绘制记录表(paint record)该表记录(record)了绘制(paint)的顺序 。 ------------- 这个阶段称为绘制(paint)然后主线程遍历layout Tree 生成 Layer Tree(第一次layout tree遍历生成paint record 第二次layout tree 遍历生成 layer tree)
到此我们可以总结出渲染器进程的主线程的运行方式:
-
现在确定了文档的绘制顺序和 Layer Tree,终于要把这些信息转化成像素点,显示在屏幕上了。这种行为被称为栅格化(Rastering)
-
早期的栅格化(Rastering)只栅格用户可视区域(Viewport),等用户滚动页面的时候,再栅格化更多的内容来填充缺失的部分。这种方法带来的问题显而易见!!这是会导致展示的延迟。
-
现在的chrome,使用了一种更加复杂的栅格化(rastering)流程叫做 合成(composting).合成(composting)是一种将页面各个部分分成多个图层,分别对其栅格化(rastering),并在合成器线程(composting Thread)中单独进行合成页面的技术简单来说:就是页面所有的元素按照莫种规则进行分图层,并把各自图层栅格化(rastering)好了。然后只需要把可视区域(viewport)的内容组合成一帧。展示给用户即可
> 简化的具体的流程如下!
- 渲染器进程(render Process)的主线程(main Thread)在解析好HTML,生成DOM Tree
2.通过遍历DOM Tree 和计算好的样式生成 Layout Tree
- 通过遍历Layout Tree 绘制(Paint)了绘制记录表
- 然后主线程(main Thread)将这些绘制记录表(Paint record)和Layout Tree 放到 合成器线程(composting Thread )
- 合成器线程(composting Thread)按照规则进行分图层,并把每个图层分成更小的图块(tiles)传给栅格线程(rastering Thread)进行栅格化。
- 栅格化(rastering)完成后会,合成器线程(composting Thread)会接收到栅格化线程(rastering Thread)传过来的 (图块信息) drow quads
- 根据这些图块信息(drow quads),合成器线程合成一个合成器帧(composting frame)
- 然后渲染器进程 通过 IPC 将合成器帧(composting Frame)传给了浏览器进程(Browser Process)
- 浏览器进程再将合成器帧传给GPU进程进行渲染,最后就展示在你的屏幕上了。
- 以此类推,当我们下滑屏幕时,合成器线程会重新生成合成器帧(Frame),然后返回给浏览器进程,在到GPU进程渲染到我们的页面上
> 回流和重绘
回流:当我们改变一个元素的几何属性时,会重新进行样式计算,layout布局,绘制(paint)等一系列操作
重绘: 当我们改变某个元素的颜色,外观,不影响布局的属性时,会重新进行样式计算,但不进行布局计算和绘制
通过上面的分析我们发现:重绘和重排都会占据我们渲染器进程的主线程的!!他们的结果就是会导致屏幕掉帧
> 除了重绘和重排会占用到主线程?还有什么占用主线程
答案是javascript 他是会占据我们渲染器进程的主线程的。既然他们都会占用到主线程。这样会导致一个问题:抢占执行时间!如果你写了一个不断导致重排重绘的动画,浏览器则需要在每一帧都运行样式,布局和绘制操作。我们知道当一个页面以每秒60帧(frame) 的刷新率,才不会让页面出现卡顿的现象。如果在运行动画时,还需要执行大量的jS任务。当在一帧的时间内,样式计算、布局、绘制结束后还有剩余时间,这个时候js 会抢占主线程,获取它的使用权。如果此时的js任务过长!导致在下一帧开始时没有及时归还主线程给样式计算、布局、绘制。进而导致下一帧动画没有渲染,这个时候就会导致卡顿的效果
> 那怎么解决这个js长时间霸占主线程呢?
可以通过requestIdleCallback 这个api. 他可以做到吧js任务分割开,在每一帧渲染后执行,这样就不会影响到渲染
四、我们看看这样处理后的代码执行会是怎么样的效果会和我们想象的一样吗?
他的效果是不会影响渲染,但是延长js执行时间!