前言:浏览器(参考文献:https://blog.csdn.net/ch834301/article/details/114826592)
1.进程和线程
进程和线程是操作系统的基本概念。
进程:
进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。我们这里将进程比喻为工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。
注:一个计算机下的所有应用程序算是一个进程,每个进程之间相互独立,那么浏览器对于操作系统的CPU来说也是一个进程,其占据CPU的一些内存。那么浏览器这个软件(进程)又是一个多进程,每个进程可能又分为好多线程,这些线程之间公用他们自己的进程的内存。
线程:
在早期的操作系统中并没有线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程,线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元。这里把线程比喻一个车间的工人,即一个车间可以允许由多个工人协同完成一个任务。即一个进程分为多个线程来执行不同的任务,这些线程可以同时运行分别执行不同的任务,也可以不同时执行。
进程和线程的区别和关系:
- 进程是操作系统分配资源的最小单位,线程是程序执行的最小单位。
- 一个进行由一个或多个线程组成,线程是进程中代码的不同执行路线
- 进程之间是独立的,但是同一进程下的各线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号)。
- 调度和切换:线程上下文切换比进程上下文切换要
快得多
- 总结:(一个进程相当于一个程序)进程相当于一个一个进程将任务分配给多个线程,这些线程分别执行不同的任务,他们协助完成这个任务。这些线程可以同时运行,也可以不同时运行。
多进程和多线程:
多进程:多进程指的是在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态。多进程带来的好处是明显的,比如你可以听歌的同时,打开编辑器敲代码,编辑器和听歌软件的进程之间丝毫不会相互干扰。【多个进程应该指的是多个app(进程)的同一时间运行;说明同时有多个CPU,因为一个CPU,在某一时间内只允许运行一个进程】
多线程:是指程序(一个进程)中包含多个执行流,即在一个程序(一个进程)中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序(一个进程)创建多个并行执行的线程来完成各自的任务。
打个比方:
- 假如进程是一个工厂,工厂有它的独立的资源
- 工厂之间相互独立
- 线程是工厂中的工人,多个工人协作完成任务
- 工厂内有一个或多个工人
- 工人之间共享空间
再完善完善概念:
- 工厂的资源 -> 系统分配的内存(独立的一块内存)
- 工厂之间的相互独立 -> 进程之间相互独立
- 多个工人协作完成任务 -> 多个线程在进程中协作完成任务
- 工厂内有一个或多个工人 -> 一个进程由一个或多个线程组成
- 工人之间共享空间 -> 同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)
以上内容为进程和线程的概念
我们首先了解一下浏览器。浏览器是一个多进程框架。从浏览器输入URL到页面渲染的整个过程都是由浏览器框架中的各个进程之间的配合完成。
浏览器进程:主进程,它负责用户界面(地址栏、菜单等等)、子进程的管理(例如,进程间通信和数据传递)、存储等等。即管理子进程、提供服务功能
网络进程:它负责网络资源的请求,例如 HTTP
请求、WebSocket
模块
渲染进程:它负责将接收到的 HTML
文档、css文档和 JavaScript
等转化为用户界面。将HTML、CSS、JS渲染成界面,js引擎v8和排版引擎Blink就在上面,他会为每一个tab页面创建一个渲染进程。【在浏览器中打开一个网页相当于新起了一个进程(进程内有自己的多线程).如果浏览器是单进程,那么某个Tab页或者第三方插件崩溃了,就影响了整个浏览器,体验有多差,而且多进程还有其它的诸多优势,当然,多进程内存等资源消耗也会更大】。
GPU
(图形处理器)进程:本来是负责处理3Dcss的,后来慢慢的UI界面也交给GPU来绘制。用于硬件加速图形绘制
插件进程:它负责对插件的管理,负责插件的运行的,因为插件很容易崩溃,把它放到独立的进程里不要让它影响别人
分析渲染进程: 渲染进程(浏览器内核/渲染引擎):chrom浏览器,会给每个Tab页面创建一个进程(渲染进程),也就是说每个页面都是一个独立的程序。每个页面都有自己的window对象。打印a页面和b页面的window对象就不一样。那么其js引擎线程当然也属于各自的进程。所有的一切都属于自己进程。我们这里讲的所有的都是某个tab页面即某个进程。【其他浏览器不一定,有的每个页面都是一个线程,从而一个页面崩了,其他的页面也受影响】
浏览器内核(渲染引擎):通常所谓的浏览器内核也就是浏览器所采用的渲染引擎,渲染引擎决定了浏览器如何显示网页的内容以及页面的格式信息。不同的浏览器内核对网页编写语法的解释也有不同,因此同一网页在不同的内核的浏览器里的渲染(显示)效果也可能不同,这也是网页编写者需要在不同内核的浏览器中测试网页显示效果的原因。最开始渲染引擎和js引擎并没有区分的很明确,后来JS引擎越来越独立,内核就倾向于只指渲染引擎。即渲染引擎(浏览器内核主要把控GUI渲染吧即浏览器如何显示网页的内容以及页面的格式信息。因此,我们一般需要在有的css属性中添加-webkit- -mos-等等,代表不同的内核)
在浏览器内核(渲染引擎)控制下各线程相互配合以保持同步,一个渲染进程通常由以下常驻线程组成
a. GUI 渲染线程
1、 GUI渲染线程负责当浏览器收到响应的html后,该线程开始解析HTML文档构建DOM树,解析CSS文件构建CSSOM,合并构成渲染树,并计算布局样式,绘制在页面上。
2、当界面样式被修改的时候可能会触发reflow和repaint,该线程就会重新计算,重新绘制,是前端开发需要着重优化的点
3、注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
b.JavaScript引擎线程(js线程)【所有的异步【事件、定时、http请求等】都不属于JavaScript引擎线程,只是执行的时候需要在JavaScript引擎线程的执行栈中执行】
注:heap:内存堆,应该是属于进程的空间吧。这也就是说,我们定义了一个对象比如var b={name:'张三'},那么对象{name:'张三'}就被存放在内存堆中,当其不被引用的时候即没有变量指向它的地址,那么,这个对象就等着被回收。当然如果这个进程关闭,其当然也就被销毁了。一定要记住,所有的例如b={name:"张三"},只是某个作用域中的变量b指向内存堆中的这个对象,当这个变量的变量对象被销毁,那么这个变量当然也就被销毁了。而对象仍然存在浏览器内存堆中。
事件循环、webAPI、任务队列都认为是浏览器的一些东西吧。因为它们不属于js线程也不属于定时器、异步、等线程。
Javascript 有一个 main thread 主线程 和 call-stack 调用栈(执行栈即Stack执行栈),所有的任务都会被放到调用栈等待主线程执行
-
JS 调用栈(Stack即执行栈)
JS 调用栈是一种后进先出的数据结构。当函数被调用时,会被添加到栈中的顶部,执行完成之后就从栈顶部移出该函数,直到栈内被清空。
-
同步任务、异步任务
JavaScript 单线程中的任务分为同步任务和异步任务。同步任务会在调用栈中按照顺序排队等待主线程执行。
异步任务则会在异步有了结果后【对应的线程将注册的回调函数从WEB API中拉出来,安排进入任务队列】将注册的回调函数添加到任务队列(消息队列)中等待主线程空闲的时候,也就是栈内被清空的时候,被读取到栈中等待主线程执行。任务队列是先进先出的数据结构。
-
Event Loop
调用栈中的同步任务都执行完毕,栈内被清空了,就代表主线程空闲了,这个时候就会去任务队列中按照顺序读取一个任务放入到栈中执行。每次栈内被清空,都会去读取任务队列有没有任务,有就读取执行,一直循环读取-执行的操作,就形成了事件循环
javascript引擎线程的具体描述
1、也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)即<script>标签中的代码的加载、解析、运行。
2、JS引擎线程负责解析Javascript脚本,运行代码。<script>标签中的代码分为同步代码和异步代码,同步代码属于JavaScript引擎线程自身的代码。所有的异步即需要等待在任务队列中的代码都有自己对应的线程,只不过这些异步都需要在JavaScript引擎线程的执行栈中执行,当然,在执行的时候,JavaScript引擎线程启动,GUI线程挂载。
3、JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
4、同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
javascript线程和GUI线程的关系:GUI渲染线程与JS引擎线程互斥的.至于为什么互斥,这里不深究了,总之:记住,GUI渲染线程和JavaScript引擎线程是互斥的,具体表现为:当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到引擎线程空闲时,才会接着执行。
注: DOMCountentLoaded:(https://www.cnblogs.com/caizhenbo/p/6679478.html)
DOMContentLoaded顾名思义,就是即dom树构建完成即HTML解析完成触发。差不多也是页面渲染出来的时间。那什么是dom内容加载完毕呢?我们从打开一个网页说起。当输入一个URL,页面的展示首先是空白的,然后过一会,页面会展示出内容,但是页面的有些资源比如说图片资源还无法看到,此时页面是可以正常的交互,过一段时间后,图片才完成显示在页面。从页面空白到展示出页面内容,会触发DOMContentLoaded事件。而这段时间就是HTML文档被加载和解析完成。
这里需要注意一点,在现在浏览器中,为了减缓渲染被阻塞的情况,现代的浏览器都使用了猜测预加载。当解析被阻塞的时候,浏览器会有一个轻量级的HTML(或CSS)扫描器(scanner)继续在文档中扫描,查找那些将来可能能够用到的资源文件的url,在渲染器使用它们之前将其下载下来。
在这里我们可以明确DOMContentLoaded所计算的时间,当文档中没有脚本时,浏览器解析完HTML文档便能触发 DOMContentLoaded 事件;如果文档中包含脚本,则脚本会阻塞文档的解析,而脚本需要等位于脚本前面的css加载完才能执行。也就是说只有当所有<script>标签的内容全部解析完成了,没有其他需要解析的元素了,HTML才解析完成也就是dom树才构建完成。在任何情况下,DOMContentLoaded 的触发不需要等待图片等其他资源加载完成。【因为img src等没有专门的线程处理,因此dom构建和其没有关系。】即HTML解析完成了也就是DOM树创建完成才触发DOMContentLoaded事件。这里一定要注意,<script>中的同步代码才是JavaScript引擎线程的代码,所有的异步都不属于这个线程的代码,因此HTML的解析只包括这些同步代码的执行,不包括异步代码的执行,只不过这些异步在JavaScript引擎线程的执行栈中执行,在执行这些异步代码的时候,GUI渲染线程是挂起的。
接下来,我们来说说load,页面上所有的资源(图片,音频,视频等)被加载以后才会触发load事件,简单来说,页面的load事件会在DOMContentLoaded被触发之后才触发。
我们在 jQuery 中经常使用的 $(document).ready(function() { // ...代码... }); 其实监听的就是 DOMContentLoaded 事件,而 $(document).load(function() { // ...代码... }); 监听的是 load 事件。在用jquery的时候,我们一般都会将函数调用写在ready方法内,就是页面被解析后,我们就可以访问整个页面的所有dom元素,可以缩短页面的可交互时间,提高整个页面的体验。
因此如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。更具体的表现看后面的渲染进程部分
c. 定时触发器线程(setTimeout等异步,顾名思义,负责执行异步定时器一类的函数的线程,如: setTimeout,setInterval)
1、传说中的setInterval与setTimeout所在线程
2、浏览器的定时器并不是由JavaScript引擎计数的,因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响计时的准确,因此通过单独的线程来计时并触发定时器。
-
3、主线程依次执行代码时,遇到定时器,会将定时器交给该线程处理,当计数完毕后,定时触发线程会将计数完毕后的事件加入到任务队列的尾部,等待JS引擎线程执行。
d. 事件触发线程(事件)
归属于渲染(浏览器内核)进程,不受JS引擎线程控制。主要用于控制事件(例如鼠标,键盘等事件),当事件被触发时候,事件触发线程就会把该事件的处理函数添加进任务队列中,等待JS引擎线程空闲后执行。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。
即:主线程依次执行代码时,遇到事件处理程序如:onclick='myclick'等,交给实现触发线程去处理。当用于点击或者触发了某个事件,浏览器给包含该坐标点的所有元素创建并分发事件,当实际目标元素接收到本次事件后,事件触发程序就将该元素的事件处理程序添加到任务队列中等待被执行。事件继续传播,同理事件触发程序将对元素的事件处理程序添加到任务队列中等待被执行。
e. 异步http请求线程(顾名思义,负责执行异步请求一类的函数的线程,如: Promise,axios,ajax等)
主线程依次执行代码时,遇到异步请求,会将函数交给该线程处理,当监听到状态码变更,如果有回调函数,异步http请求线程会将回调函数加入到任务队列的尾部,等待JS引擎线程执行
注意:浏览器对通一域名请求的并发连接数是有限制的,Chrome和Firefox限制数为6个,ie8则为10个。
总结:b-e这四个线程参与了JS的执行,但是永远只有JS引擎线程在执行JS脚本程序,其他三个线程只负责将满足触发条件的处理函数推进任务队列,等待JS引擎线程执行。
当用户在浏览器窗口输入一个URL后:大体流程是这样的:
浏览器进程:
1.浏览器会对我们输入的URL进行解析,主要将其分为以下几个部分:协议、网络地址、资源路径。其中,网络地址指的是该连接网络上的哪台电脑上(从哪台电脑(服务器)上获取资源即服务器地址),可以是IP也可以是域名,也可以包括端口号。协议指的是从计算机上(服务器)获取资源的方式,常见的是HTTP,HTTPS和FTP,不同协议有不同的通讯内容格式。资源路径指的是从服务器获取资源的具体路径。
这里浏览器对输入的url解析为如下内容:
url:https://www.jianshu.com/p/879fada10661
协议:https
网络地址:www.jianshu.com
资源路径:/p/879fada10661;再例如:/index.html 、 /js/index.js .... 客户端指定请求的路径名称,服务器端根据这个信息把具体的文件[一定要记住,这里是文件不是文件夹。一个url对应一个文件。也就是说一次请求只能请求到一个文件中的数据]中的源代码读取到然后给客户端返回即可。当我们没有指定的情况,大部分情况默认请求的都是项目根目录下的index.html,也就是默认会增加 /index.html (当然这个默认的值是服务器可以修改配置的)
【浏览器的主要功能:将用户选择的web资源呈现出来。而这,它需要从服务器请求资源(比如我的一台电脑),并将其显示在浏览器窗口中。资源的格式通常是html,也包括PDF,image等其他格式。用户用URL(Uniform Resource Identifier统一资源标识符)来指定所请求资源的位置(比如我的电脑的地址,只不过这个地址不是Ip。url相当于人的名字,IP相当于电话号码。那么我们要打电话就需要通过人名找到对应的手机号;因此,通过DNS解析来找到URL对应的IP),通过DNS(域名系统(英文:DomainNameSystem,缩写:DNS))查询,将网址转换为IP地址。【用户输入URL到浏览器从服务器请求资源的过程,这就跟文件共享差不多一个概念。我要从另外一个电脑复制文件,找到该同事的电脑的IP,然后进行连接,最后就可以随便复制文件了】整个浏览器的工作流程如下:
1.输入URL
2.浏览器查找域名的IP地址(DNS解析完成,即找到所要找的资源的ip地址)
3.浏览器给web服务器发送一个http请求
4.网站服务的永久重定向响应
5.浏览器跟踪重定向地址,现在,浏览器知道了要访问的正确地址,所以它会发送另一个获取请求
6.服务器“处理”请求,服务器接收到获取请求,然后处理并返回一个响应
7.服务器发回一个html响应
8.浏览器开始显示html
9.浏览器发送请求,以获取嵌入在html中的对象。在浏览器显示HTML时,它会注意到需要获取其他地址内容的标签。这时,浏览器会发送一个获取请求来重新获得这些文件。这些文件就包括CSS/JS/图片等资源,这些资源的地址都要经历一个和HTML读取类似的过程。所以浏览器会在DNS中查找这些域名,发送请求,重定向等等…
例如:你发现快过年了,于是想给你的女朋友买一件毛衣,你打开了 www.taobao.com,这时你的浏览器首先查询DNS服务器,将 www.taobao.com转换成IP地址。但是,你首先会发现,在不同的地区或者不同的网络下,转换后的IP地址很可能是不一样的,这首先涉及负载均衡的第一步,通过DNS解析域名时,将你的访问分配的不同的入口,同时尽可能的保证你所访问的入口时所有入口中较快的一个。 你通过这个入口成功的访问了www.taobao.com实际的入口IP地址,……经过一系列的复杂的逻辑运算和数据处理,用于给你看的淘宝首页HTML内容便生成了,浏览器下一步会加载页面中用到的CSS /JS/图片等样式、脚本和资源文件。】
2.URL解析到页面展示的详解(网络进程)
2.1用户在浏览器中输入URL(https://www.jianshu.com/p/879fada10661),浏览器进程会将完整的url通过进程间通信,即 IPC
,发送给网络进程(浏览器进程 URL >网络进程) 进程间通信
2.2网络进程接收到 URL
后,并不是马上对指定 URL
进行请求。而是因为要知道用户想要用http访问一个网络地址是“www.jianshu.com”的网站,那么如何找到这个地址呢?就像你打的回家,你跟司机说去阮老师家,他哪儿知道阮老师家是哪里呢?你得告诉他地址呀。网站服务器的地址就是IP地址。所有浏览器首先要确认的是域名所对应的ip是啥?因此我们需要进行 DNS
解析域名得到对应的 IP
,然后通过 ARP
解析 IP
得到对应的 MAC
(Media Access Control Address
)地址。
1.DNS的定义:
域名系统(DomainNameSystem)是互联网的一项服务。它作为将域名和ip地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。
2.域名的定义:
域名:(英语:Domain Name)是由一串用点分割的名字组成的internet上某一台计算机或计算机组的名称,用于在数据传输表示计算机的电子方位(有时也指地理位置)。
3.IP地址
IP地址是Internet主机的作为路由寻址用的数字体标识,人不容易记忆。因而产生了域名这一种字符型标识。
域名是我们取代记忆复杂的 IP
的一种解决方案,而 IP
地址才是目标在网络中所被分配的节点。MAC
地址是对应目标网卡所在的固定地址。(url(域名) dns解析 >ip)
dns解析的过程:
1》浏览器会先看看是否存在本地缓存,如果有就直接返回资源给浏览器进程,无则下一步 DNS-> IP -> TCP
2》浏览器会首先查看本地硬盘的 hosts 文件,看看其中有没有和这个域名对应的规则,如果有的话就直接如果有,将ip直接返回给网络进行,完成域名解析。
3》如果在本地的 hosts 文件没有能够找到对应的 ip 地址,浏览器会发出一个 DNS请求到本地DNS服务器 。本地DNS服务器一般都是你的网络接入服务器商提供,比如中国电信,中国移动。查询你输入的网址的DNS请求到达本地DNS服务器之后,本地DNS服务器会首先查询它的缓存记录,如果缓存中有此条记录,就将包含url的ip地址响应报文发给网络进程,此过程是递归的方式进行查询。如果没有,本地DNS服务器还要向DNS根服务器发起请求进行进行查询。
4》根DNS服务器有该对应关系,则将对应关系响应给本地DNS服务器,本地DNS服务器再将含有url的ip地址的响应报文发送给网络进行;如果根DNS服务器没有记录具体的域名和IP地址的对应关系,就是告诉本地DNS服务器,你可以到域服务器上去继续查询,并给出域服务器的地址。这种过程是迭代的过程
5》本地DNS服务器继续向域服务器发出请求,在这个例子中,请求的对象是.com域服务器。.com域服务器收到请求之后,也不会直接返回域名和IP地址的对应关系,而是告诉本地DNS服务器,你的域名的解析服务器的地址。
6》最后,本地DNS服务器向域名的解析服务器发出请求,这时就能收到一个域名和IP地址对应关系,本地DNS服务器将含有https://www.jianshu.com/p/879fada10661的IP地址的响应报文发送给客户端返回给用户电脑,还要把这个对应关系保存在缓存中,以备下次别的用户查询时,可以直接返回结果,加快网络访问。
————————————————————————————————————截止这里,我们的服务器的ip终于找到了——————————————————————
3、浏览器获取端口号
知道ip地址(服务器的地址)后,还需要端口号。例如:
好了,阮老师家的地址知道了,正常来讲是可以出发了。可是对于网络有些不一样,你还需要指定端口号。端口号之于计算机就像窗口号之于银行,一家银行有多个窗口,每个窗口都有个号码,不同窗口可以负责不同的服务。端口只是一个逻辑概念,和计算机硬件没有关系。现在可以这么说,阮老师家好几扇们,办不同的业务走不同的门,你得告诉师傅你走那扇门,你要不说,就默认你是个普通客人,丢大门得了。http协议默认端口号是80。
浏览器会以一个随机端口(1024<端口<65535)向服务器的WEB程序(常用的有httpd,nginx等)80端口发起TCP的连接请求。(向服务器的WEB程序80端口发起请求)
4、TCP建立连接「再三确认是我要连接的服务器」【浏览器是指客户端的浏览器即用户在自己的电脑上的浏览器,浏览器作为客户端的代表和服务器进行交互。】
IP和端口都有了,在http消息发送前,浏览器发起建立客户端与服务器的TCP连接接(三次握手)的请求(即客户端通过浏览器向服务器发起一个TCP连接的请求)。
- 客户端发送标有
SYN
的数据包,表示我将要发送请求。 - 服务端发送标有
SYN/ACK
的数据包,表示我已经收到通知,告知客户端发送请求。 - 客户端发送标有
ACK
的数据包,表示我要开始发送请求,准备接收。
这种建立连接的方法可以防止产生错误的连接
三次握手完成,TCP客户端和服务器端成功地建立连接,可以开始传输数据了。(相当于两台电脑连上了,可以实现文件共享了)
5.发送HTTP请求(还是网络进程)即浏览器
浏览器(客户端)发起一个http请求。一个典型的 http request header 一般需要包括请求的方法,例如 GET 或者 POST 等,不常用的还有 PUT 和 DELETE 、HEAD、OPTION以及 TRACE 方法,一般的浏览器只能发起 GET 或者 POST 请求。
客户端向服务器发起http请求的时候,会有一些请求信息,请求信息包含三个部分:
| 请求方法URI协议/版本
| 请求头(Request Header)
| 请求正文:
下面是一个完整的HTTP请求例子:
GET/sample.jsp HTTP/1.1
Accept:image/gif.image/jpeg,*/* Accept-Language:zh-cn Connection:Keep-Alive Host:localhost User-Agent:Mozila/4.0(compatible;MSIE5.01;Window NT5.0) Accept-Encoding:gzip,deflate
username=jinqiao&password=1234
(1)请求的第一行是“方法URL协议/版本”:GET/sample.jsp HTTP/1.1
(2)请求头(Request Header)
请求头包含许多有关的客户端环境和请求正文的有用信息。例如,请求头可以声明浏览器所用的语言,请求正文的长度等。
Accept:image/gif.image/jpeg.*/* Accept-Language:zh-cn Connection:Keep-Alive Host:localhost User-Agent:Mozila/4.0(compatible:MSIE5.01:Windows NT5.0) Accept-Encoding:gzip,deflate.
(3)请求正文
请求头和请求正文之间是一个空行,这个行非常重要,它表示请求头已经结束,接下来的是请求正文。请求正文中可以包含客户提交的查询字符串信息:
username=jinqiao&password=1234
![](https://img2020.cnblogs.com/blog/1076231/202111/1076231-20211101000421731-1242226562.png)
HTTP响应与HTTP请求相似,HTTP响应也由3个部分构成,分别是:
l 状态行
l 响应头(Response Header)
l 响应正文
HTTP/1.1 200 OK Date: Sat, 31 Dec 2005 23:59:59 GMT Content-Type: text/html;charset=ISO-8859-1 Content-Length: 122
<html> <head> <title>http</title> </head> <body> <!-- body goes here --> </body> </html>
状态行:
状态行由协议版本、数字形式的状态代码、及相应的状态描述,各元素之间以空格分隔。
格式: HTTP-Version Status-Code Reason-Phrase CRLF
例如: HTTP/1.1 200 OK \r\n
-- 协议版本:是用http1.0还是其他版本
-- 状态描述:状态描述给出了关于状态代码的简短的文字描述。比如状态代码为200时的描述为 ok
-- 状态代码:状态代码由三位数字组成,第一个数字定义了响应的类别,且有五种可能取值。如下
服务器将响应行、响应头、响应体,并发给网络进程。网络进程接受了响应信息之后,就开始解析响应头的内容。![](https://img2020.cnblogs.com/blog/1076231/202105/1076231-20210531161102055-2092882087.png)
在响应结果中都会有个一个HTTP状态码,比如我们熟知的200、301、404、500等。通过这个状态码我们可以知道服务器端的处理是否正常,并能了解具体的错误。
状态码由3位数字和原因短语组成。根据首位数字,状态码可以分为五类:
状态码 的解析:2xx,请求成功;
4XX,客户端错误状态码(用户在客户端通过客户端的浏览器输入一个URL,有错的话,一般就是就是url有错即发送请求失败。) 5XX,服务器错误状态码(服务器处理请求出错,这种情况,我们客户端是无法处理的)
响应头:
响应头部:由关键字/值对组成,每行一对,关键字和值用英文冒号":"分隔,典型的响应头有:
响应正文
包含着我们需要的一些具体信息,比如cookie,html,image,后端返回的请求数据等等。这里需要注意,响应正文和响应头之间有一行空格,表示响应头的信息到空格为止,下图是fiddler抓到的请求正文,红色框中的:响应正文:
7.断开连接(为了避免服务器与客户端双方的资源占用和损耗,当双方没有请求或响应传递时,任意一方都可以发起关闭请求。
)
当浏览器收到数据后,收发数据的过程就结束了(网络进程响应之前吧),。
建立一个连接需要三次握手,而终止一个连接要经过四次挥手,这是由TCP的半关闭(half-close)造成的。具体过程如下图所示。
![](https://img2020.cnblogs.com/blog/1076231/202105/1076231-20210531174958458-2142346601.png)
即
而这整个过程的客户端则是网络进程。并且,在数据传输的过程还可能会发生的重定向的情况,即当网络进程接收到状态码为 3xx 的响应报文,则会根据响应报文首部字段中的 Location 字段的值进行重新向,即会重新发起请求
8.数据处理
当网络进程接收到的响应报文状态码,进行相应的操作。例如状态码为 200 OK
时,会解析响应报文中的 Content-Type
首部字段,例如我们这个过程 Content-Type
会出现 application/javascript
、text/css
、text/html
,即对应 Javascript
文件、CSS
文件、HTML
文件。
9.开始渲染【网络进程 html,javascript,css等数据 渲染进程】
传递给
整个渲染的过程其实就是将URL对应的各种资源,通过浏览器渲染引擎的解析,输出可视化的图像。
在创建完渲染进程后,网络进程会将接收到的数据传递给渲染进程。而在渲染进程接收到HTML数据后开始渲染。
而页面渲染的过程可以分为 9 个步骤:
- 解析
HTML
生成DOM
树 - 解析
CSS
生成CSSOM
- 加载或执行
JavaScript
- 生成渲染树(
Render Tree
) - 布局
- 分层
- 生成绘制列表
- 光栅化
可概括为:
-
GUI渲染线程解析HTML生成DOM树 。在此过程中遇到<link>则加载样式表,然后解析,遇到<style>则直接解析,则同时解析CSS生成CSSOM树。遇到<script>则,GUI渲染线程暂且挂起,JavaScript引擎线程运行,即解析JavaScript文档并执行。
-
GUI渲染线程构建Render树 - 接下来不管是内联式,外联式还是嵌入式引入的CSS样式会被解析生成CSSOM树,根据DOM树与CSSOM树生成另外一棵用于渲染的树-渲染树(Render tree),
-
布局Render树 - 然后对渲染树的每个节点进行布局处理,确定其在屏幕上的显示位置
-
绘制Render树 - 最后遍历渲染树并用UI后端层将每一个节点绘制出来
a.解析HTML -> 构建 DOM 树【GUI线程】
解析HTML生成DOM树,网络进程将拿到HTML文件发给渲染进程(网路进程仍还在接收HTML数据)。(因为我们请求的url资源第一次都是都是某个html页面。比如https://www.songma.com/news/txtlist_i62138v.html,这个url,我们请求的资源就是一个HTML文件。一个url只能请求对应的一个文件。) 。渲染引擎首先解析HTML文档并生成DOM树。
需要注意的是这个 DOM
树不同于 Chrome-devtool
中 Element
选项卡的 DOM
树,它是存在内存中的,用于提供 JavaScript
对 DOM
的操作。
注:DOM树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。DOM树的根节点就是document对象。
DOM树的生成过程中可能会被CSS和JS的加载执行阻塞,具体可以参见下一章。当HTML文档解析过程完毕后,浏览器继续进行标记为defer模式的脚本加载【】,然后就是整个解析过程的实际结束触发DOMContentLoaded事件,并在async文档文档执行完之后触发load事件。
b.构建 CSSOM 【GUI线程】
当HTML解析到包含<link>标签的时候:浏览器再发起一个url请求,进行url解析、DNS解析、发送请求等前面的一系列的操作,最后客户端的浏览器网络进程将接收到css文件通过进程之间的通信发给渲染进程即在GUL线程中解析CSS文件并生成CSSOM树。当然,在这个过程中HTML文件仍然在解析并构建DOM树。
当HTML解析到<style>标签的时候->直接解析css->构成CSSOM树。同时,HTML文件仍然在解析并构建DOM树
生成DOM树的同时会生成样式结构体CSSOM(CSS Object Model)Tree接下来不管是内联式,外联式还是嵌入式引入的CSS样式会被解析生成CSSOM树。构建 CSSOM
的过程,即通过解析 CSS
文件、style
标签、行内 style
等,生成 CSSOM
。而这个过程会做这几件事:
- 规范
CSS
,即将color: blue
转化成color: rgb()
形式,可以理解成类似ES6
转ES5
的过程 - 计算元素样式,例如
CSS
样式会继承父级的样式,如font-size
、color
之类的。
CSS Object Model
是一组允许用 JavaScript
操纵 CSS
的 API
。
c.加载 JavaScript【JavaScript引擎线程】
当HTML解析器找到 <script>
标签后,将会暂停HTML解析,并且必须加载、解析和执行 JavaScript的代码。为什么?因为JavaScript 可以使用诸如 document.write()
更改整个DOM结构!所以开发人员在写代码的时候可以在 <script>
标签上加 async
或者 defer
属性。然后浏览器将会异步加载并运行JavaScript,不会阻止解析。
当HTML解析到包含<script>标签的时候,如果是从外部引入的资源,则:浏览器再发起一个url请求,进行url解析、DNS解析、发送请求等前面的一系列的操作,最后客户端的浏览器网络进程将接收到的JavaScript文件通过进程之间的通信发给渲染进程,JavaScript引擎线程开始解析并执行JavaScript文件中的代码。从遇到<script>标签到执行完JavaScript代码的过程中,GUL线程挂起(HTML解析停止,为什么CSS解析不停止呢?),等JavaScript线程中的代码执行完成了,再抽空将GUL线程运行即HTML解析继续->构建dom树。
注意:如果CSS文件还没有解析完成,则JavaScript引擎线程则需要等到css解析完成才能运行。因此JavaScript线程开启时,必须保证如果GUL线程中有CSS解析,那么这个已经完成解析才能开启。
当HTML解析到包含<script>标签的时候,如果不是外部引入的资源,则直接编译执行;
关于加载执行js程序:1.浏览器在解析HTML文档时,将根据文档流从上到下逐行解析和显示。Javascript代码也是HTML文档的组成部分,因此Javascript脚本默认的执行顺序也是根据<script>
标签位置来确定的。<script src="1.js"> <script src="2">即使2因为预加载比1先预加载完成,但是2还是等1.js加载执行完之后才执行。
2.默认JavaScript文件加载完就立马在执行栈中执行。(defer改变这个默认规则,即加载完成还是等所有HTML其他内容都已解析完成,最后才执行这些script.但是注意,只有所有的<Script>也都执行完了,dom树才算是构建完成,这时候才会触发DOMContentLoaded事件。)【asycn异步加载,但是加载完成也是立马就执行。】
3.在执行js代码的时候,都是在执行栈中由js主线程执行。在此过程中,遇到事件处理程序,交由事件线程处理;【当某元素接收到事件之后,事件线程会将该元素对应的事件处理程序加到任务队列中等待被执行】
遇到定时器,交由定时器线程处理;【定时线程计时,到时间了就将该定时中的回调扔进任务队列中等待被执行】
遇到http请求,比ajax或者axios、promise等则,交由异步http请求线程处理;【当状态等发生变化,就将对应的回调函数扔进任务队列中等待被执行】
- 提示:对于导入的js文件,也将按照
<script>
标签在文档中出现的顺序来执行,而执行过程是HTML文档解析的一部分,不会单独解析或者延期执行。
一般情况下,在文档<head>标签中包含js脚本,或者导入的js文件。这意味着必须等到全部js代码都被执行完以后才能继续解析后面的HTML部分。 那么,如果加载的js文件很大,HTML文档解析就容易出现延迟,用户首次加载,等待的时间就太长了,体验效果非常不好。 为了避免这个问题,浏览器自身也做了一些优化,比如如果CSSOM构建完成,可以部分HTML解析构建的dom树可以和CSSOM创建一个没有完成的render树,进行布局绘制首屏页面即first paint,然后同时继续解析HTML直到解析完成,构建完整的dom树,然后和CSSOM再创建完成的render树,然后布局、绘制。即first paint的核心就是css构建完成、有部分dom元素就可以,但是要注意一点,如果如果js还没有执行,first paint出来页面,用户是不能操作的。开发web应用程序时,我们就可以根据这一特点,进行优化页面就是改变js的位置,因为js也是HTML解析的一部分内容,而且first paint不需要js,因此,尽量把js放在HTML的后面。:
【1】建议把导入js文件操作放在<body>后面,让浏览器先将网页内容解析并呈现出来后,再去加载js文件,以便加快网页响应速度。【也就是说HTML解析一般的元素,只要cssOM构建完成、只要有DOM元素,在遇到script 的时候,就先进行进行first paint ,然后再进行<script>的相关动作,注意这时候HTML还没有解析完成即dom树还没有彻底构建完成哦,即DOMContentloaded还没有触发哦】
【2】延迟执行js文件:<scirpt>
标签有一个布尔型属性defer,设置该属性,能够将js文件延迟到页面解析完毕后再运行。【这里defer之后,用于开启新的线程下载脚本文件,并使脚本在HTML文档解析完成即dom树创建完成后执行。去请求到网络进程请求到js文件发给渲染进程的这个过程中,HTML是可以继续解析的(即新的线程和GUI渲染线程可以同时运行),并且等到dom树构建完成了,才去开启javascript引擎线程去执行js。那么,相比之前,有个时间是节约了,就是异步加载的时间;还有首屏渲染也不会被阻塞。之前从遇到<script>标签开始HTML解析就停止了。】
【3】异步加载js文件:默认情况下,网页都是同步加载外部js文件的,如果js文件比较大,会影响后面解析HTML代码的速度。上面介绍的方法是最后加载js文件。而现在我们可以使用<script>标签的async属性,让浏览器异步加载js文件,即在加载js文件时,浏览器仍然继续解析HTML文档。这样能节省时间,提升响应速度。
提示:async是HTML5新增的布尔型属性,通过设置async属性,就不用考虑<script>标签的放置位置,用户可以根据习惯吧很多大型js库文件放在<head>标签内。【HTML5新增属性,用于异步下载脚本文件,这个过程中HTML是可以继续解析的(下载js文件和解析HTML同时进行),但是js文件下载完毕立即停止GUL线程并开启js引擎线程去执行代码。而且,哪个<script>中的文件先下载完就执行哪个,不会按照<script>在HTML文档中的顺序执行。这样虽然相比之前节约了加载文件的时间,但是1.<script>中的文件执行顺序改变;2.还是可能会阻塞html解析;】
通常情况下,在构建 DOM
树或 CSSOM
的同时,如果也要加载 JavaScript
,则会造成前者的构建的暂停。当然,我们可以通过 defer
或 sync
来实现异步加载 JavaScript
。虽然 defer
和 sync
都可以实现异步加载 JavaScript
,但是前者是在加载后,等待 CSSOM
和 DOM
树构建完后才执行 JavaScript
,而后者是在异步加载完马上执行,即使用 sync
的方式仍然会造成阻塞。
defer和async[简介]:https://blog.csdn.net/liuhe688/article/details/51247484
defer
和async
是script
标签的两个属性,用于在不阻塞页面文档解析的前提下,控制脚本的下载和执行
在介绍他们之前,我们有必要先了解一下页面的加载和渲染过程:
1. 浏览器通过HTTP协议请求服务器,获取HMTL文档并开始从上到下解析,构建DOM;
2. 在构建DOM过程中,如果遇到外联的样式声明和脚本声明,则暂停文档解析,创建新的网络连接,并开始下载样式文件和脚本文件;
3. 样式文件下载完成后,构建CSSDOM;脚本文件下载完成后,解释并执行,然后继续解析文档构建DOM
4. 完成文档解析后,将DOM和CSSDOM进行关联和映射,最后将视图渲染到浏览器窗口
在这个过程中,脚本文件的下载和执行是与文档解析同步进行,也就是说,它会阻塞文档的解析,如果控制得不好,在用户体验上就会造成一定程度的影响。(一般都是空白页面等待的时间太长了,很久很久才能加载出来页面即页面渲染完成)
所以我们需要清楚的了解和使用defer和async来控制外部脚本的执行。
在开发中我们可以在script
中声明两个不太常见的属性:defer
和async
,下面分别解释了他们的用法: defer
:用于开启新的线程下载脚本文件,并使脚本在文档解析完成后执行。 async
:HTML5新增属性,用于异步下载脚本文件,下载完毕立即解释执行代码。
defer:
显而易见,1.js被延后致至文档解析中其他元素全部解析完成,只剩余包含defer的<script>标签了,这之后defer标签中代码才执行,它的执行顺序比body中的<script>还要靠后。与默认的同步解析不同,defer下载外部脚本的不是阻塞的,浏览器会另外开启一个线程,进行网络连接下载,这个过程中,文档解析及构建DOM仍可以继续进行,不会出现因下载脚本而出现的页面空白。
关于defer我们需要注意下面几点:
1. defer只适用于外联脚本,如果script标签没有指定src属性,只是内联脚本,不要使用defer
2. 如果有多个声明了defer的脚本,则会按顺序下载和执行
3. defer脚本会在DOMContentLoaded和load事件之前执行。DOMCountentLoaded:就是HTML解析完成触发的事件。即包含defer的<script>标签中的代码只有等所有的其余HTML中的标签都已经解析完成了,才去一次执行,(如果是外部资源)至于什么时候加载这取决于<script>标签的位置,如果放置在body的最后面,那么请求回来资源立马执行,如果放在head中,则先另起线程加载,然后等最后再一次执行。等HTML中的所有资源都加载并解析或者执行(不包括图片等,这里的资源指的是所有标签以及link、script中的资源),HTML才算解析完成即dom树创建完成。
async
我们发现,3个脚本的执行是没有顺序的,我们也无法预测每个脚本的下载和执行的时间和顺序。async和defer一样,不会阻塞当前文档的解析,它会异步地下载脚本,但和defer不同的是,async会在脚本下载完成后立即执行,如果项目中脚本之间存在依赖关系,不推荐使用async。
关于async,也需要注意以下几点:
1. 只适用于外联脚本,这一点和defer一致
2. 如果有多个声明了async的脚本,其下载和执行也是异步的,不能确保彼此的先后顺序
3. async会在load事件之前执行,但并不能确保与DOMContentLoaded的执行先后顺序。【script加上async还是HTML的标签吧,HTML的解析不是等所有标签都加载解析或者执行完成吗?】
最后我们来回答这个问题:我们为什么一再强调将css放在头部,将js文件放在尾部(这里的js指没有加defer或者async) ?
按照我们之前的分析用户在输入一个url之后,经过解析URL、网络进程中从服务器请求url中所指向的资源,网络进程再将请求到的资源通过进程之间的通信发给渲染进程,GUI线程运行,进行解析HTML文件,当解析到<link>或者<style>等css的时候,如果是<link>则在经过解析url、网络进程获取数据等一些列的操作获取到css文件然后进行解析,当然这个过程中,HTML仍然在继续解析。当解析到<script>标签的时候HTML解析就停止,如果是外部的资源还需要进行前面的一系列的请求文件,文件请求会之后在JavaScript引擎线程中解析执行,但是JavaScript的加载还需等CSSOM解析完成,因此,当CSSOM解析完成,JavaScript加载执行。
那么这样,对于<script>放在头部,先加载并执行js,然后继续解析HTML生成dom,从而js阻塞了HTML解析,因为渲染的过程是HTML解析成dom树即HTML解析完成dom树构建完成,css解析完成生成CSSOM树,二者生成render树,然后render树布局,然后绘制,这样页面才能显示出来。
对于<script>放在尾部,解析HTML生成dom树,遇到css解析css生成CSSOM树,那么当最后遇到<script>标签的时候,开始请求执行等。那么这个时候,因为<script>是HTML的标签,因此HTML解析还没有完成,因此dom树也还没有彻底创建完成,等到所有的<script>中的代码执行完毕,HTML继续解析,这时候因为后面没有标签,因此HTML解析结束,也就是dom创建完成,这时候cssom创建完成了,二者生成render树,然后布局、绘制页面显示。
综上分析,那么对于<Script>放在头部和尾部没什么区别,因为都是花一样的时间页面才能渲染出来。那么,为什么说把js放在尾部可以加快网络响应速度呢?经常会有人在回答页面的优化中提到将js放到body标签底部,原因是因为浏览器生成Dom树的时候是一行一行读HTML代码的,script标签放在最后面就不会影响前面的页面的渲染。那么问题来了,既然Dom树完全生成好后页面才能渲染出来,浏览器又必须读完全部HTML才能生成完整的Dom树,script标签不放在body底部是不是也一样,因为dom树的生成需要整个文档解析完毕【当然包括<script>标签中的文件也都解析执行完成】。其实现代浏览器为了更好的用户体验,渲染引擎将尝试尽快在屏幕上显示的内容。它不会等到所有HTML解析之后才开始构建和布局渲染树。部分的HTML内容将被解析并显示。也就是说浏览器能够渲染不完整的dom树和cssom,尽快的减少白屏的时间【可能首屏根据浏览器窗口的大小,先渲染排在这个窗口的render树,其余再解析生成完整的dom树,然后生成完整render树,最后全部渲染出来,这时候,用户才能够在浏览器中操作】。假如我们将js放在header,js将阻塞解析dom,dom的内容会影响到First Paint,导致First Paint延后。所以说我们会将js放在后面,以减少First Paint的时间,但是不会减少DOMContentLoaded被触发的时间即dom构建完成。
html解析、css解析、加载执行<script>标签的关系:【<link><style><script>都是html的元素,因此HTML的解析,就会解析到这些标签,从而css和JavaScript文件才能被加载解析,这是本质】
1.<script>标签阻断HTML解析;现代浏览器总是并行加载资源,例如,当 HTML 解析器(HTML Parser)被脚本阻塞时,解析器虽然会停止构建 DOM,但仍会识别该脚本后面的资源,并进行预加载即这里需要注意一点,在现在浏览器中,为了减缓渲染被阻塞的情况,现代的浏览器都使用了预加载。当解析被阻塞的时候,浏览器会有一个轻量级的HTML(或CSS)扫描器(scanner)继续在文档中扫描,查找那些将来可能能够用到的资源文件的url,在渲染器使用它们之前将其下载下来。(提前去发送请求等,加载那些可能用到的资源,请求到之后缓存在某个地方,将来执行到这里的时候,直接使用)
2.<script>标签中的代码加载执行受CSS解析的影响,即GUI线程中的CSS解析完成,JavaScript引擎线程才开启去加载执行;
3.HTML解析和CSS解析可以同时进行,都属于GUI线程。
CSS解析(GUI线程)→→影响→→→→↓
↓影 >页面渲染
↓响 ↑影响
即<script>标签(js线程)影响>html解析(GUI线程) 1.dom树和cssom都构建完成之后才开始渲染成render树,然后render树在布局、最后绘制出来,即页面渲染完成。
2.GUI线程和js线程永远都是互斥的,一个在运行的时候,另一个挂载。这里一定要注意,当html遇到script的时候,虽然GUI线程仍然在运行,是因为在解析CSS文件。而这时候HTML解析仍然停止的。等CSS解析完成,GUI立马挂载,js线程立马运行。
所以,script 标签的位置很重要。实际使用时,可以遵循下面两个原则:
-
CSS 优先:引入顺序上,CSS 资源先于 JavaScript 资源。
-
JavaScript 应尽量少影响 DOM 的构建。(前面所述的3种方法)
切记:为什么js线程和gui线程互斥,以及这3个的影响规则,不需要深究,人家就是这么规定的。你只需要明白在这种规则下,如何更好地渲染页面就可以了
注:具体再分析一下:
css:
这样的 link 标签(无论是否 inline)会被视为阻塞渲染的资源,浏览器会优先处理这些 CSS 资源,直至 CSSOM 构建完毕。
渲染树(Render-Tree)的关键渲染路径中,要求同时具有 DOM 和 CSSOM,之后才会构建渲染树。即,HTML 和 CSS 都是阻塞渲染的资源。HTML 显然是必需的,因为包括我们希望显示的文本在内的内容,都在 DOM 中存放,那么可以从 CSS 上想办法。
最容易想到的当然是精简 CSS 并尽快提供它
关于CSS加载的阻塞情况:
1. css加载不会阻塞DOM树的解析
2. css加载会阻塞DOM树的渲染
3. css加载会阻塞后面js语句的执行
没有js的理想情况下,html与css会并行解析,分别生成DOM与CSSOM,然后合并成Render Tree,进入Rendering Pipeline;但如果有js,css加载会阻塞后面js语句的执行,而(同步)js脚本执行会阻塞其后的DOM解析(所以通常会把css放在头部,js放在body尾)
js
这里的 script 标签会阻塞 HTML 解析,无论是不是 inline-script。上面的 P 标签会从上到下解析,这个过程会被两段 JavaScript 分别打断一次(加载、执行)。
解析过程中无论遇到的JavaScript是内联还是外链,只要浏览器遇到 script 标记,唤醒 JavaScript解析器,就会进行暂停 (blocked )浏览器解析HTML,并等到 CSSOM 构建完毕,才去执行js脚本。因为脚本中可能会操作DOM元素,而如果在加载执行脚本的时候DOM元素并没有被解析,脚本就会因为DOM元素没有生成取不到响应元素,所以实际工程中,我们常常将资源放到文档底部。
d.布局(布局Render Tree) 【GUI线程】[layout]
通过以上的过程,现在我们已经有了DOM树和DOM树中每个元素的样式,但是这样还是无法显示页面,因为还不知道DOM节点的集合位置。
所以接下来需要计算出DOM树中「可见元素」的几何位置,这个计算过程即为布局。
Chrome在布局阶段有两个任务:创建布局树和布局计算。
1.创建布局树即生成渲染树(Render Tree)
在有了 DOM
树和 CSSOM
之后,需要将两者结合生成渲染树 Render Tree
,并且这个过程会去除掉那些 display: none
的节点。此时,渲染树就具备元素和元素的样式信息。 即:CSS根据DOM节点,会生成类似于DOM结构的一个布局树,仅包含了页面上可见内容的信息,如果有 display: none
等,则该元素不属于布局树。如果有p::before {content:"123"}
等伪类的存在,就算它不在DOM中,也会包含在布局树中
注:生成DOM树的同时会生成样式结构体CSSOM(CSS Object Model)Tree,再根据CSSOM和DOM树构造渲染树Render Tree,渲染树包含带有颜色,尺寸等显示属性的矩形,这些矩形的顺序与显示顺序基本一致。从MVC的角度来说,可以将Render树看成是V,DOM树与CSSOM树看成是M,C则是具体的调度者,比HTMLDocumentParser等。
可以这么说,没有DOM树就没有Render树,但是它们之间不是简单的一对一的关系。Render树是用于显示,那不可见的元素当然不会在这棵树中出现了,譬如 <head>。除此之外,display等于none的也不会被显示在这棵树里头,但是visibility等于hidden的元素是会显示在这棵树里头的
【截止这里,虽然render树中每个节点都具有元素和样式,但是,这些节点没有大小等信息,只是按照要显示的顺序用元素+css样式这么排列着,这里可以将css样式理解为我们现在用webstrom打开的一个状态一样,没有任何意义】
2.布局计算
当renderer构造出来并添加到Render树上之后,它并没有位置跟大小信息,为它确定这些信息的过程,接下来是布局(layout)。即在有了上面的完整布局树之后,就要开始计算布局树节点的「坐标位置」了,这个过程十分复杂
根据 Render Tree
渲染树,对树中每个节点进行计算,确定每个节点在页面中的宽度、高度和位置。 至此,每个节点都拥有自己的样式和布局信息,下面就可以利用这些信息去展示页面了。
需要注意的是,第一次确定节点的大小和位置的过程称为布局,而第二次才被称为回流
注: 布局阶段输出的结果称为box盒模型(width,height,margin,padding,border,left,top,…),盒模型精确表示了每一个元素的位置和大小,并且所有相对度量单位此时都转化为了绝对单位。
e.分层(layer tree)
页面中有很多复杂的效果,比如3D变换、页面滚动、使用z-indexing做z轴排序等,为了方便的实现这些效果,渲染引擎需要为特定的节点生成专用的图层(Layer),并生成一棵对应的图层树,正是这些图层叠加在一起构成了最终的页面图像。
在Chrome中的开发者工具中选择“Layers”便签可以可视化的查看页面分层情况。
通常情况下,不是布局树的每个节点都包含一个图层,如果一个节点没有对应的图层,那么这个节点就从属于父节点的图层。比如span标签没有专属图层,那么它们就从属于它们的父节点。所以,最终每一个节点都会直接或间接的从属于某一个层。
比如position、float等这时候创建新的图层,这些元素才从原来的位置中脱离出来,然后进行对齐布局等,那么原来的布局需要进行重新布局。这样,页面元素的位置都已经各就各位了。注:这时候其实,render树不会改变。render树只是按照dom流中的元素以及CSSOM进行合成的以及最后可能在布局的时候给添加了位置大小等。对应的图层树中,才会元素重新布局排列。
值得一提的是,对于内容溢出存在滚轮的情况也会进行分层
一般满足以下两个条件的元素会被提升为单独的一层。
(1)拥有层叠上下文属性的元素
页面是一个二维平面,但是层叠上下文能让HTML元素具有三维的概念,它们会按照自身属性的优先级分布在垂直于这个二维平面的z轴上。
明确定位属性的元素、定义透明属性的元素、使用css滤镜的元素等等都可以拥有层叠上下文属性。
(2)需要剪裁(Clip)的地方
示例:把div中的文字限定在一个大小为200px*200px的区域内,由于文字内容比较多,所以文字的显示区域必定超过这个区域,此时就会产生「剪裁」,
f.绘制图层数【GUI线程】[paint]
在完成图层树的构建后,渲染引擎会对图层树中的每个图层进行绘制。
以上步骤是一个渐进的过程,为了提高用户体验,渲染引擎试图尽可能快的把结果显示给最终用户。它不会等到所有HTML都被解析完才创建并布局渲染树。它会在从网络层获取文档内容的同时把已经接收到的局部内容先展示出来。
g. 生成绘制列表[composite layers]【GUI线程】【一般DCL即触发DocumentContentLoaded即HTML解析完成即</body>被解析完成后,就会进行这一步。到这里,就要进入绘出页面的阶段了。即页面已成定性,首屏马上就会被绘制完成】
将上面的paint阶段的东西,composite 对于存在图层的页面部分,需要进行有序的绘制,而对于这个过程,渲染引擎会将一个个图层的绘制拆分成绘制指令,并按照图层绘制顺序形成一个绘制列表。
H.光栅化[rasterize paint]【光栅进程】【紧接着上一步的composite layers,这里进行光栅化】
有了绘制列表后,渲染引擎中的合成线程会根据当前视口的大小将图层进行分块处理,然后合成线程会对视口附近的图块生成位图,即光栅化。而渲染进程也维护了一个栅格化的线程池,专门用于将图块转为位图。
栅格化的过程通常会使用 GPU
加速,例如使用 wil-change
、opacity
,就会通过 GPU
加速显示
i.显示[GPU][经过这一步之后,才能将HTML渲染在页面中,即首屏绘制完成]【浏览器进程】
当所有的图块都经过栅格化处理后,渲染引擎中的合成线程会生成绘制图块的指令,提交给浏览器进程。然后浏览器进程将页面绘制到内存中。最后将内存绘制结果显示在用户界面上。
总结:解析HTML到页面绘制的过程。
在这期间,遇到外载的<script>即<Script src></script>,之前解析的HTML先会先绘制一版即首屏绘制。然后再加载执行这个script中的资源,执行完成之后——parseHTml(解析HTML),如果修改了dom,则还会绘制一版------【外载的,只要遇到就先绘制一版,然后立马停止渲染工作,加载执行,加载执行完本外载<script>中的同步代码,一定是先回调渲染线程,进行渲染,等本次渲染完成之后,再检查任务队列,如果有任务则继续执行】
遇到内联的<script>即<script>......</script>,相当于一个普通的HTML标签来解析,只不过需要在JavaScript引擎线程中执行。从<script>开始执行到完成,都当做是<html>的parse html即解析,一版情况下执行完这个<script>还会继续解析HTML,但是如果这个script的执行时间比较长,那么因为html隔固定时间就会渲染一次页面,因此执行完比较长的script之后(相当于解析HTML时间过长),就会绘制一版。然后继续解析。[一定是本次本次渲染完之后再去检查任务队列]
那么,对于异步,在执行代码的过程中遇到就丢给自己的线程处理,到了某个条件或者某个时间被放在任务队列中等待被执行。那么任务队列中的回调和渲染又是怎么交替进行的呢?一般情况下,渲染完一次之后,都会检查一下任务队列,如果有任务就执行,至于什么时候回到渲染线程渲染,这个还是和任务队列中的回调执行的时间长度有关系,如果时间比较长了,就会先渲染以及再继续执行任务队列中的任务,如果这些任务中的执行时间都比较断,则统一执行完之后再回调渲染线程中进行渲染。
具体的可以查看一下浏览器的performance
因此,综上所述,一般我们将<script>放在底部,这样首屏绘制就会早一些,如果放在head中,必须等到外载的、内联的script都加载完成了再渲染,首屏太慢了。
对于,defer,即放在<head>中的外载<script>加上defer,那么相当于将该外载<script></script>放在</body>的前面即紧挨着</body>。也就是相当于将<Script>放在底面。同样等这些脚本都执行完了,才会触发DCL.
回流与重绘【GUI线程】【无论是回流还是重绘都是直接针对rendertree】
回流(reflow):将可见DOM节点以及它对应的样式结合起来,可是我们还需要计算它们在设备视口(viewport)内的确切位置和大小,这个计算的阶段就是回流。
当浏览器发现某个部分发生了点变化影响了布局,需要倒回去重新布局。reflow 会从 <html>这个 root frame 开始递归往下,依次计算所有的结点几何尺寸和位置。reflow 几乎是无法避免的。现在界面上流行的一些效果,比如树状目录的折叠、展开(实质上是元素的显示与隐藏)等,都将引起浏览器的 reflow。鼠标滑过、点击……只要这些行为引起了页面上某些元素的占位面积、定位方式、边距等属性的变化,都会引起它内部、周围甚至整个页面的重新渲染。通常我们都无法预估浏览器到底会 reflow 哪一部分的代码,它们都彼此相互影响着。
引起回流的操作有哪些?【元素的位置改变或者尺寸的改变都会导致回流】
什么操作会引起重绘、回流
其实任何对render tree中元素的操作都会引起回流或者重绘,比如:
1. 添加、删除元素(回流+重绘)
2. 隐藏元素,display:none(回流+重绘),visibility:hidden(只重绘,不回流)
3. 移动元素,比如改变top,left(jquery的animate方法就是,改变top,left不一定会影响回流),或者移动元素到另外1个父元素中。(重绘+回流)
4. 对style的操作(对不同的属性操作,影响不一样)
5. 还有一种是用户的操作,比如改变浏览器大小,改变浏览器的字体大小等(回流+重绘)
重绘(repaint):改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸没有变。(重新绘制render树,不需要重新布局。)每次Reflow,Repaint后浏览器还需要合并渲染层并输出到屏幕上。所有的这些都会是动画卡顿的原因。Reflow 的成本比 Repaint 的成本高得多的多。一个结点的 Reflow 很有可能导致子结点,甚至父点以及同级结点的 Reflow 。在一些高性能的电脑上也许还没什么,但是如果 Reflow 发生在手机上,那么这个过程是延慢加载和耗电的。可以在csstrigger上查找某个css属性会触发什么事件。
reflow与repaint的时机:
回流必将引起重绘,而重绘不一定会引起回流。
-
display:none 会触发 reflow,而 visibility:hidden 只会触发 repaint,因为没有发生位置变化。
-
有些情况下,比如修改了元素的样式,浏览器并不会立刻 reflow 或 repaint 一次,而是会把这样的操作积攒一批,然后做一次 reflow,这又叫异步 reflow 或增量异步 reflow。
-
有些情况下,比如 resize 窗口,改变了页面默认的字体等。对于这些操作,浏览器会马上进行 reflow。
再比如:
让我们看看下面的代码是如何影响回流和重绘的:
var s = document.body.style;
s.padding = "2px"; // 回流+重绘
s.border = "1px solid red"; // 再一次 回流+重绘
s.color = "blue"; // 再一次重绘
s.backgroundColor = "#ccc"; // 再一次 重绘
s.fontSize = "14px"; // 再一次 回流+重绘
// 添加node,再一次 回流+重绘
document.body.appendChild(document.createTextNode('abc!'));
请注意我上面用了多少个再一次。
说到这里大家都知道回流比重绘的代价要更高,回流的花销跟render tree有多少节点需要重新构建有关系,假设你直接操作body,比如在body最前面插入1个元素,会导致整个render tree回流,这样代价当然会比较高,但如果是指body后面插入1个元素,则不会影响前面元素的回流。
聪明的浏览器
从上个实例代码中可以看到几行简单的JS代码就引起了6次左右的回流、重绘。而且我们也知道回流的花销也不小,如果每句JS操作都去回流重绘的话,浏览器可能就会受不了。所以很多浏览器都会优化这些操作,浏览器会维护1个队列,把所有会引起回流、重绘的操作放入这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会把flush队列,进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。
虽然有了浏览器的优化,但有时候我们写的一些代码可能会强制浏览器提前flush队列,这样浏览器的优化可能就起不到作用了。当你请求向浏览器请求一些style信息的时候,就会让浏览器flush队列,比如:
1. offsetTop, offsetLeft, offsetWidth, offsetHeight
2. scrollTop/Left/Width/Height
3. clientTop/Left/Width/Height
4. width,height
5. 请求了getComputedStyle(), 或者 ie的 currentStyle
当你请求上面的一些属性的时候,浏览器为了给你最精确的值,需要flush队列,因为队列中可能会有影响到这些值的操作。
如何减少回流、重绘
减少回流、重绘其实就是需要减少对render tree的操作,并减少对一些style信息的请求,尽量利用好浏览器的优化策略。具体方法有:
关键渲染路径与阻塞渲染
在浏览器拿到HTML、CSS、JS等外部资源到渲染出页面的过程,有一个重要的概念关键渲染路径(Critical Rendering Path)。
例如为了保障首屏内容的最快速显示,通常会提到一个渐进式页面渲染,但是为了渐进式页面渲染,就需要做资源的拆分,那么以什么粒度拆分、要不要拆分,不同页面、不同场景策略不同。具体方案的确定既要考虑体验问题,也要考虑工程问题。了解原理可以让我们更好的优化关键渲染路径,从而获得更好的用户体验。
现代浏览器总是并行加载资源,例如,当 HTML 解析器(HTML Parser)被脚本阻塞时,解析器虽然会停止构建 DOM,但仍会识别该脚本后面的资源,并进行预加载。
同时:
1.CSS 被视为渲染阻塞资源 (包括js):这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕,才会进行下一阶段。CSSOM阻塞html解析和dom的构建,因为需要和dom树一起构建渲染树,所以,CSSOM会阻塞渲染。又由于JavaScript可以操作CSSOM属性,因此,CSSOM构建会阻塞JavaScript的执行。即: css加载不会阻塞DOM树的解析
css加载会阻塞DOM树的渲染
css加载会阻塞后面js语句的执行
没有js的理想情况下,html与css会并行解析,分别生成DOM与CSSOM,然后合并成Render Tree,进入Rendering Pipeline;但如果有js,css加载会阻塞后面js语句的执行,而(同步)js脚本执行会阻塞其后的DOM解析(所以通常会把css放在头部,js放在body尾)
2.JavaScript 被认为是解释器阻塞资源:HTML解析会被JS阻塞,因为它不仅可以读取和修改 DOM 属性,还可以读取和修改 CSSOM 属性 。
JavaScript 的情况比 CSS 要更复杂一些。如果没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的HTML元素之前,也就是说不等待后续载入的HTML元素,读到就加载并执行。
解析过程中无论遇到的JavaScript是内联还是外链,只要浏览器遇到 script 标记,唤醒 JavaScript解析器,就会进行暂停 (blocked )浏览器解析HTML,并等到 CSSOM 构建完毕,才去执行js脚本。这也会导致一个问题: 因为脚本中可能会操作DOM元素,而如果在加载执行脚本的时候DOM元素并没有被解析,脚本就会因为DOM元素没有生成取不到响应元素,所以实际工程中,我们常常将资源放到文档底部。
对于1,即存在css阻塞资源,浏览器会延迟 JavaScript 的执行和 DOM 构建,即CSSOM 构建时,JavaScript 执行将暂停,直至 CSSOM 就绪
对于2,当浏览器遇到一个 script 标记时,因为HTML解析被阻塞,因此DOM 构建将暂停,直至脚本完成执行,这是因为JavaScript 可以查询和修改 DOM 与 CSSOM。
通过1,2 结论 :script 标签的位置很重要。
实际使用时,可以遵循下面两个原则:
1.CSS 优先:引入顺序上,CSS 资源先于 JavaScript 资源。
2.JavaScript 应尽量少影响 DOM 的构建。
进入渲染进程后,可以总结如下:【解析可以理解为HTML文档解析和JavaScript加载+执行。此消彼长的关系,相互排斥的关系】
1.解析HTML,同时构建dom树。因为HTML中包含link,script标签,因此当解析到link的时候,【渲染线程】
2.下载css资源并开始构建CSSOM树,这时HTML继续解析并继续构建dom树。【渲染线程】
3.当解析html遇到script标签(同步的情况下),HTML停止解析HTML元素(这时候当然也停止构建dom树了),加载并执行JavaScript。【当然,如果cssom树还没有构建完成,JavaScript还需要等cssom构建完成才去执行;由于JavaScript有可能需要操作dom树,而dom树没有构建完成,所以会导致很多问题。】【javascript引擎线程】
因此,针对以上的问题,就用到了上述的一些解决办法。
4.根据dom树和cssom树构建为渲染树,然后根据渲染树进行布局,最后绘制在页面中。【渲染线程】
注释:在浏览器显示HTML时,它会注意到需要获取其他地址内容的标签。这时,浏览器会发送一个获取请求来重新获得这些文件。
下面是几个我们访问facebook.com时需要重获取的几个URL:
图片
https://upload.chinaz.com/2013/0228/1362014211396.gif
https://upload.chinaz.com//
…
CSS 式样表
http://static.ak.fbcdn.net/rsrc.php/z448Z/hash/2plh8s4n.css
http://static.ak.fbcdn.net/rsrc.php/zANE1/hash/cvtutcee.css
…
JavaScript 文件
http://static.ak.fbcdn.net/rsrc.php/zEMOA/hash/c8yzb6ub.js
http://static.ak.fbcdn.net/rsrc.php/z6R9L/hash/cq2lgbs8.js
…
这些地址都要经历一个和HTML读取类似的过程。所以浏览器会在DNS中查找这些域名,发送请求,重定向等等...
还可参考文档:https://blog.csdn.net/dianxin113/article/details/104351670
最后来一个加载渲染的过程:
- 用户输入网址(假设是第一次访问),浏览器向服务器发出请求,服务器返回html文件;
- 浏览器开始载入html代码,发现<head>标签内有一个<link>标签引用外部CSS文件;
- 浏览器又发出CSS文件的请求,服务器返回这个CSS文件;
- 浏览器继续载入html中<body>部分的代码,并且CSS文件已经拿到手了;
- 浏览器在代码中发现一个<img>标签引用了一张图片,向服务器发出请求。此时浏览器不会等到图片下载完,而是继续加载后面的代码;
- 服务器返回图片文件,由于图片占用了一定面积,影响了后面段落的排布,因此浏览器需要回过头来重新渲染这部分代码;
- 浏览器发现了一个包含一行Javascript代码的<script>标签,直接运行该脚本;
- Javascript脚本执行了这条语句,它命令浏览器隐藏掉代码中的某个<div> (style.display=”none”)。少了一个元素,浏览器不得不重新渲染这部分代码;
- </html>表示暂时加载完成;
- 此时用户点了一下界面中的“换肤”按钮,Javascript让浏览器换了一下<link>标签的CSS路径;
- 浏览器向服务器请求了新的CSS文件,重新加载页面。然后执行渲染过程
前端优化:
- 减少HTTP请求次数和请求内容的大小:
- CSS/JS进行合并压缩,样式或者JS都压缩成为一个文件,然后再导入
- 对于简单的项目(尤其是移动端)我们最好把CSS和JS都采用内嵌方式引入
- 雪碧图(图片精灵、sprite) 把一些小图合并到一张大图上,然后通过背景定位来使用
- 图片延迟加载(在第一次打开页面的时候不加载真实图片,当页面加载完成后在开始加载真实的图片)
- ...
6.什么是栈和堆
堆:是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。
栈:是个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是 thread safe的。操作系统在切换线程的时候会自动的切换栈,就是切换 SS/ESP寄存器。栈空间不需要在高级语言里面显式的分配和释放。