浏览器的架构,渲染进程与JS异步(回流与重绘,微任务与宏任务)

多进程的浏览器

现代浏览器是多进程的,包括主进程,渲染进程,网络进程,插件进程,GPU进程

主进程:浏览器界面,用户交互,管理子进程,提供存储功能

网络进程:负责网络资源的请求和接收

GPU进程:处理网页的图像和视频,渲染CSS3的一些效果

插件进程:负责运行浏览器中的插件,插件作为一个单独的进程,运行在操作系统受限制的环境中(沙箱模式),防止造成页面崩溃或不安全问题

多线程的渲染进程

浏览器的内核是渲染进程

渲染进程是多线程的,包括UI线程,渲染线程,JavaScript引擎线程,合成线程,事件触发线程,定时器触发线程,异步HTTP请求线程等

渲染进程中的各个线程
渲染线程

负责将HTML、CSS和JavaScript代码解析并渲染成页面内容,渲染引擎运行在渲染线程

渲染引擎

解析HTML,生成用于构建页面的信息,遇到script标签时停止解析HTML

JavaScript引擎线程

负责解析和执行JavaScript代码,JS解析引擎运行在JavaScript引擎线程

JS解析引擎

处理JavaScript脚本,运行代码

渲染引擎与JS解析引擎的互斥性

浏览器规定:渲染引擎与JS解析引擎必须互斥,也就是说当渲染引擎开始工作时JS解析引擎就会挂起,当JS解析引擎开始工作时渲染引擎就会挂起

为什么?

如果不互斥的话,当渲染引擎在渲染进程中解析和渲染HTML,CSS代码时,JS引擎可能会在JS引擎线程执行JS代码,如果JS代码涉及对页面内容的修改或操作,就会出现数据竞争的情况,导致页面显示出现问题

为了方便,我们也常将渲染线程与JavaScript引擎线程统称为主线程(很好理解,同处与一个主线程所以其中一个在工作时另一个就不能工作)

JS的执行机制

JS是单线程的,代码必须从上往下执行

再加上JS解析引擎与渲染引擎是互斥的,因此如果当JS解析引擎工作的时间过长,长时间阻塞主线程,就会导致页面卡顿,渲染不连贯,这也是我们要引入异步编程的原因,减少JS解析引擎的工作时间

<!-- 渲染引擎解析完button标签后遇到script标签就停止解析,将渲染引擎挂起,由JS解析引擎处理script标签内的代码,由于这段js代码是一个死循环,因此JS解析引擎一直在工作,导致渲染引擎无法工作,浏览器自然无法渲染出button按钮 -->
<body>
    <button>按钮</button>
    <script>
        while (true) {

        }
    </script>
</body>

上面的代码就是一段由于JS解析引擎长时间占用主线程导致页面卡顿的例子,可能有人会存在疑惑,明明是button按钮写在上面,JS代码写在下面,为什么浏览器无法渲染出按钮呢?

那是因为浏览器的工作流程是先进行加载阶段,也就是解析HTML和CSS文件,构建DOM树和CSSOM树, 然后才进行布局与绘制阶段

而如果在加载阶段时遇到script标签,就会停止解析HTML文档,转而解析和执行script标签中的代码,所以浏览器在解析上面的代码时无法进行布局与绘制

任务队列

渲染进程的主线程一旦开始工作后就会进入一个无限循环中,循环检查任务队列中是否有新的任务,这个循环也就是事件循环

任务队列的出现是为了解决以下两个问题

解决渲染引擎与JS解析引擎互斥的问题

在事件循环中,如果遇到渲染任务(修改了DOM结构的就属于渲染任务)就交给渲染引擎去执行而将JS解析引擎挂起,如果遇到script任务就交给JS解析引擎去执行而将渲染引擎挂起

解决JS单线程的问题

引入异步编程,将一些特定的任务(认为其耗时长)交给渲染进程其他特定的线程去处理,同时JS解析引擎继续向下解析

特定的任务比如定时器,与用户的交互,网络请求等等

定时器线程

当JS解析引擎解析到setTimeout()setInterval()时,会将整个定时器封装成一个定时器任务交给定时器线程

setTimeout()setInterval()接收两个参数,回调函数和等待的时间

定时器线程接收到定时器任务之后就会在指定的毫秒数(等待时间)过后把回调函数封装成一个任务添加进任务队列里,等待主线程执行

console.log("123");
// 当JS引擎解析到这里时会将定时器任务交给定时器线程,自己继续向下解析,所以先输出123和789,再输出456
setTimeout(()=>console.log("456"), 0);
console.log("789");
// 123
// 789
// 456
// 只有当主线程的执行栈执行完后才会去检查任务队列是否有新的任务,也就是说JS解析引擎会先执行同步任务,再去执行异步任务
事件触发线程

当JS解析引擎解析到事件时(不管是DOM0的on+事件,还是DOM2的addEventListener()),会将整个事件交给事件触发线程

addEventListener()接收三个参数:事件、回调函数和一个布尔值

事件触发线程监听用户操作触发的事件,并将事件派发到相应的事件处理器中执行,当事件处理器执行完毕后,会将回调函数封装成一个任务添加进任务队列里,等待主线程执行

异步HTTP请求线程

当JS解析引擎解析到XMLHttpRequest对象或由fetch API等方式发起异步HTTP请求时,异步HTTP请求线程会创建HTTP请求对象,并将创建好的HTTP请求发送给服务器,等待服务器响应

一旦服务器响应到达,异步HTTP请求线程就会接收响应数据并将该数据封装成一个任务添加进任务队列里,等待主线程执行

渲染进程的工作流程
什么是渲染?

将HTML字符串通过一系列程序转换成像素信息,最终显示在网页上

HTML包括元素标签,CSS和JS

怎么做?
  1. 当我们在导航栏中输入一个地址,渲染进程的UI线程会判断输入的是不是URL,如果是则通知网络进程处理

  2. 网络进程处理完后将网页数据交给UI线程(这些数据是HTML字符串

  3. UI线程拿到数据后将数据包装成渲染任务添加进任务队列中,渲染引擎开始执行渲染任务,JS解析引擎挂起(如果执行过程中遇到<script>则JS解析引擎开始执行JS任务,渲染引擎挂起)

  4. 渲染引擎解析HTML代码,构建DOM树

    DOM树

    DOM树反映了节点的层级关系,之所以要生成树(对象),是因为对象具有的键值对特征方便对文档进行查询,提高效率

    节点类型有元素标签、文本、注释、属性等等

    通常情况下浏览器在解析HTML时会忽略空白文本节点

    <!DOCTYPE html>
    <html lang="en">
    <head> <!-- 这里是一个由换行符构成的空白文本节点 -->
        <title>test</title>
    </head>
    <body>
        <button>按钮</button>
    </body>
    </html>
    

    上面这段代码生成的DOM树如下:
    在这里插入图片描述

  5. 在渲染线程解析HTML的同时预解析线程解析CSS代码,构建CSSOM树,渲染线程与预解析线程的工作流程如下:
    在这里插入图片描述

    CSSOM树

    CSS的来源有两个,一个是外部文件下载通过link标签,一个是style标签

    预解析线程解析CSS代码时会将元素节点和其对应的样式记录下来

    body h1{
        color: red;
    }
    body{
        width: 100%;
    }
    

    在这里插入图片描述

  6. 将DOM树和CSSOM树合并起来,构建渲染树render tree

    渲染树

    渲染树会从DOM树的根节点开始遍历,对于不需要显示的节点会忽略,如注释节点、隐藏的节点(display: none)、html/head/style/title节点

    文本节点不会单独存在于渲染树中,而是被包含在元素节点中

    每个节点都是页面的一个渲染对象,每个节点包含元素的样式信息、内容(如文本或图片等)、关系信息、盒模型信息、可见性信息、事件处理信息

  7. 根据渲染树进行布局计算,计算可见元素的几何信息(在页面上的位置大小),构建布局树

  8. 分层,为特定的节点生成专用的图层(将二维的平面变成立体的效果,有点类似与z-index的效果)

    分层的目的是减少改变网页的工作量,可以理解为本来需要对整个页面进行重新渲染,变成只需要对某个图层重新渲染

  9. 将每一个图层的绘制拆分成很多个小的绘制指令组成绘制列表,交给渲染进程中的合成线程(内部有多个线程)操作

  10. 为了提高效率,合成线程会将图层分块,由合成线程内部的线程池(由合成线程内部的诸多线程组成)对图块进行处理,优先处理视口附近的图块

  11. 合成线程对图块的处理就是栅格化处理,是将图块转化为位图,这个过程有GPU进程的参与,生成的位图保存在GPU内存中

    位图是由像素组成的图像,每一个像素都有自己的颜色

    还记得最开始提到的什么是渲染吗?渲染就是把HTML字符串转换成像素信息,所以最终要生成位图

  12. 所有图块被栅格化处理后,合成线程生成DrawQuard的命令,将该命令发送给GPU进程,由GPU进程与显卡进行沟通,将页面内容绘制到屏幕上

    为什么合成线程不直接与显卡进行沟通?

    因为合成线程是在渲染进程中的,而渲染进程是运行在沙箱模式下,不能直接与硬件进行联系

总结

在这里插入图片描述

不过上述步骤并不是以严格顺序执行的,渲染引擎会以最快的速度展示内容,也就是说,浏览器一边解析HTML,一边构建渲染树,构建一部分就会把当前已有的元素渲染出来,如果这个时候样式还没有加载完成,渲染出来的就是浏览器的默认样式了

重绘repaint

修改节点样式,但不会影响页面的布局,因此不用重新生成布局树,只影响了绘制列表及之后的流程

如修改样式颜色,将h1标签的color: red改为color: black

重排/回流reflow

修改节点样式,使得页面的布局信息发生改变,影响了布局树及之后的流程,为了提高浏览器的性能,需要减少重排

重排必定会引发重绘,但重绘不一定会引发重排

为什么修改了节点样式但不会影响渲染树呢?

因为渲染树本身的结构是没有发生变化的(如:可能只是将节点的样式信息由top: 50%改为top: 40%),所以不会影响渲染树

但该渲染对象在页面中的位置或大小却发生了变化,因此需要重新进行布局计算,影响了布局树

display和visibility对渲染树的影响:渲染树不包含display: none的元素节点,但包含visibility: hidden的元素节点,因此修改display的可见性会导致页面重排(因为在渲染树上增加了一个节点),但修改visibility的可见性只会导致页面重绘(只是改变了节点的可见性信息,不影响布局)

渲染过程中的分层步骤可以避免重排吗?

分层的目的就是为了当页面某一部分发生变化时,只需要重新渲染受到影响的图层即可,减少重排的发生,提高页面的性能

但并不能完全避免重排,如果当页面某一部分发生变化时,影响到其他图层的布局,就可能导致整个页面的重新渲染,引发重排

优化方案
浏览器的优化机制

维护一个队列,把所有会引起重绘与回流的操作放入这个队列中,等待队列中的操作达到一定数量或过了一定时间,浏览器就会批处理这些操作,将多次的回流重绘变成一次回流重绘

但也存在一些交互事件处理不及时导致页面卡顿,所以我们应该优化页面结构和样式

程序员的优化方式

正确书写CSS顺序

span{
    width: 200px;
    height: 200px;
    background-color: red;
    display: block;
}

浏览器在解析CSS代码时是从上到下一行一行解析的,所以在没有解析到display: block时浏览器是用行级元素的计算模式阅读这段代码的,当解析到display: block时就会隐式地将计算模式切换为块级的计算模式,然后重新从头解析这段CSS代码,重新渲染

不要一条一条地修改节点的样式,预先定义好新类名的样式,直接修改节点的className

将节点离线后再修改,如先将节点的display改为none,然后修改,最后再显示出来,这样可以将多次回流变成一次回流

对于经常引发回流重绘的节点(如动画),将其position设为absolutefixed

减少使用table布局,因为一个小改动可能会造成整个表格的重新布局

动画的实现速度像素单位越小页面回流的次数就会越多

限制回流的影响范围,样式尽量加在自身上,不要继承父节点的样式

使用DocumentFragment操作DOM节点

DocumentFragment是一个没有父级节点的最小文档对象,它可以用于存储已经排好版或者尚未确定格式的HTML片段

DocumentFragment不是真实DOM树的一部分,它的变化不会引起DOM树重新渲染的操作,也就不会引起浏览器重排和重绘的操作,从而带来性能上的提升

var list = document.querySelector('#list');
// 1.创建新的DocumentFragment对象
var fragment = document.createDocumentFragment();
for (var i = 0; i < 100; i++) {
	var li = document.createElement('li');
	var text = document.createTextNode('节点' + i);
	li.append(text);
    // 2.将新增的元素添加至DocumentFragment对象中
    fragment.append(li);
}
// 3.处理DocumentFragment对象
list.append(fragment);

JS异步

为了减少JS解析引擎长时间占用渲染进程的主线程,所以引入异步

异步是通过将一些特定的任务(认为其耗时长)交给渲染进程其他特定的线程去处理,同时JS解析引擎继续向下解析(单线程)

这些特定的任务是已经规定好的,如定时器任务,事件任务以及网络请求任务,与实际任务是否耗时无关

只要JS解析引擎解析到这些任务,就会将其交给其专门的线程进行处理,如定时器任务交给定时器线程,事件交给事件触发线程,网络请求交给异步HTTP请求线程

这些线程将任务处理完成之后,会将其中的回调函数封装成任务添加进任务队列

渲染进程的主线程中的执行栈一旦为空就会进入一个无限循环中,循环检查任务队列中是否有新的任务,这个循环也就是事件循环

但任务队列并不是完全的按照先进先出的规则执行的,还存在优先级

微任务与宏任务

任务队列中的任务分为宏任务与微任务

一般来说微任务的优先级是高于宏任务的,在事件循环时,我们需要将任务队列中所有的微任务执行完毕再去执行宏任务,流程图如下:

在这里插入图片描述

微任务

Promise,.then(),.catch(),finally()

MutationObserver

Object.observer

宏任务

新程序或子程序直接被执行,如<script>

事件的回调函数

定时器setTimeout(),setInterval()

为什么说一般来说微任务的优先级高于宏任务?

因为新程序的执行属于宏任务,所以最开始是先将宏任务加入执行栈中,当执行栈为空时才去检查任务队列,之后才优先执行微任务

<script> // 宏任务
	console.log("1"); // 同步任务
	setTimeout(()=>console.log("2"), 0); // 宏任务	
	Promise.resolve().then(()=>console.log("3")); // 微任务
	console.log("4"); // 同步任务
	Promise.resolve().then(()=>console.log("5")); // 微任务
	setTimeout(()=>console.log("6"), 0); // 宏任务
</script>
<!-- 输出顺序 143526 -->
影响页面渲染的两个方法
stop()

stop()可以停止解析HTML代码,即停止构建DOM树,并进入下一个流程

<body>
    <button>按钮</button>
    <script>
        stop();
    </script>
    <h1>hhh</h1>
</body>
<!-- 该页面只会渲染出按钮,不会渲染出hhh,因为当JS解析引擎解析到stop()时停止当前对DOM树的构建,进入下一个流程构建渲染树,然后继续后面的流程 -->

在这里插入图片描述

alert()

alert()会阻塞JS解析引擎的执行

<body>
    <button>按钮</button>
    <script>
        alert("1");
    </script>
    <h1>hhh</h1>
    <script>
        alert("2");
    </script>
</body>
<!-- 该页面会先弹出第一个警告框,点击确定后警告框关闭渲染出按钮,此时还没有渲染出hhh,然后弹出第二个警告框,点击确定后警告框关闭渲染出hhh -->

上面这段代码的渲染过程如下:

  1. 解析HTML代码并构建DOM树,当JS引擎解析到alert("1")时,浏览器会弹出第一个警告框,此时DOM树的body中有button一个节点
  2. 当弹出警告框的期间,浏览器会开始渲染页面(实际上浏览器不会等到整个DOM树构建完毕之后在进行下一个步骤,而是一边构建一边渲染)当我们关闭警告框,就将按钮渲染到页面
  3. 此时,渲染引擎继续解析HTML,DOM树中又多了一个h1节点,然后其余步骤如上
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值