先梳理下主干流程:
- 从浏览器接收url到开启网络请求线程
- 开启网络线程到发出一个完整的http请求
- 从服务器接收到请求到对应后台接收到请求
- 后台和前台的http交互
- 缓存问题,http的缓存
- 浏览器接收到http数据包后的解析流程
- CSS的可视化格式模型
- JS引擎解析过程
1. 从浏览器接收url到开启网络请求线程
多进程的浏览器
浏览器是多进程的,有一个主控进程,以及每一个tab页面都会新开一个进程(某些情况下多个tab会合并进程)。
多线程的浏览器内核
每一个tab页面可以看做是浏览器内核进程,然后这个进程是多线程的,它有几大类子线程:
可以看到,里面的JS引擎是内核进程中的一个线程,这也是为什么常说JS引擎是单线程的。
解析URL
输入URL后,会进行解析(URL的本质就是统一资源定位符)
网络请求都是单独的线程
每次网络请求时都需要开辟单独的线程进行,譬如如果URL解析到http协议,就会新建一个网络线程去处理资源下载。
2.开启网络线程到发出一个完整的http请求
这一部分主要内容包括:dns查询、tcp/ip请求构建、五层因特网协议栈 等。
如果输入的是域名,需要进行dns解析成ip,大致流程:
- 找 dns 缓存(只有一分钟)
- 若1找不到,找操作系统自身的 dns 缓存
- 若2找不到,读取本地的host
- 如果本地没有,浏览器发起一个dns的系统调用(1. 宽带运营商服务器查找本身缓存 2. 运营商服务器发起一个迭代dns解析请求)
tcp/ip请求
http的本质就是tcp/ip请求。
需要了解 三次握手规则建立连接 及 断开连接时的 四次挥手。
三次握手的步骤:
- 客户端:hello,你是server么?
- 服务端:hello,我是server,你是client么?
- 客户端:yes,我是client
建立连接成功后,接下来就正式传输数据。
然后,待断开连接时,需要进行四次挥手
四次挥手的步骤:
- 主机A:我已经关闭了向你那边的主动通道了,只能被动接收了
- 主机B:收到通道关闭的信息
- 主机B:那我也告诉你,我这边向你的主动通道也关闭了
- 主机A:最后收到数据,之后双方无法通信
为何需要四次挥手呢?
TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP是全双工模式,这就意味着,当主机1发出FIN
报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2,它的数据已经全部发送完毕了;但是,这个时候主机1还是可以接受来自主机2的数据;当主机2返回ACK
报文段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的;当主机2也发送了FIN
报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。
tcp/ip的并发限制
浏览器对同一域名下并发的tcp连接是有限的(2-10个不等)
而且http1.0中往往一个资源下载就需要对应一个tcp/ip请求
get和post的区别
get和post虽然本质都是tcp/ip,但两者除了在http层面外,在tcp/ip层面也有区别。
get会产生一个tcp数据包,post两个。
具体就是:
- get请求时,浏览器会把headers和data一起发送出去,服务器响应200(返回数据)
- post请求时,浏览器先发送headers,服务器响应100 continue,浏览器再发送data,服务器响应200(返回数据)
五层因特网协议栈
从客户端发出http请求到服务器接收,中间会经过一系列的流程。
从应用层的发送http请求,到传输层通过三次握手建立tcp/ip连接,再到网络层的ip寻址,再到数据链路层的封装成帧,最后到物理层的利用物理介质传输。
五层因特尔协议栈其实就是:
- 应用层(dns,http)dns解析成ip并发送http请求
- 传输层(tcp,udp)建立tcp连接(三次握手)
- 网络层(ip,ARP)ip寻址
- 数据链路层(PPP)封装成帧
- 物理层(利用物理介质传输比特流)物理传输(然后传输的时候通过双绞线,电磁波等各种介质)
3.从服务器接收到请求到对应后台接收到请求
负载均衡
用户发起的请求都指向调度服务器(反向代理服务器,譬如安装了nginx控制负载均衡),然后调度服务器根据实际的调度算法,分配不同的请求给对应集群中的服务器执行,然后调度器等待实际服务器的HTTP响应,并将它反馈给用户。
4.后台和前台的http交互
前后端交互时,http报文作为信息的载体。
http报文结构
报文一般包括了:通用头部、请求/响应头部、请求/响应体
通用头部
- Request Url:请求的web服务器地址
- Request Method:请求方式(Get、POST、OPTIONS、PUT、HEAD、DELETE、CONNECT、TRACE)
- Status Code:请求的返回状态码
- Remote Address:请求的远程服务器地址(会转成IP)
5.http的缓存
缓存可以简单的划分成两种类型:强缓存(200 from cache)与 协商缓存(304)。
区别简述如下:
- 强缓存(200 from cache)时,浏览器如果判断本地缓存未过期,就直接使用,无需发起http请求
- 协商缓存(304)时,浏览器会向服务器发起http请求,然后服务器告诉浏览器文件未改变,让浏览器使用本地缓存
对于协商缓存,使用 ctrl + F5 强制刷新可以使得缓存无效。但是对于强缓存,在未过期时,必须更新资源路径才能发起新的请求。
缓存头部简述
通过不同的http头部控制强缓存和协商缓存
属于强缓存控制的:
-
(http1.1)Cache-Control/Max-Age
-
(http1.0)Pragma/Expires
属于协商缓存控制的:
-
(http1.1)If-None-Match/E-tag
-
(http1.0)If-Modified-Since/Last-Modified
HTML页面中也有一个meta标签可以控制缓存方案——Pragma
<META HTTP-EQUIV="Pragma" CONTENT="no-cache">
6.浏览器接收到http数据包后的解析流程
- 解析HTML,构建DOM树
- 解析CSS,生成CSS规则树
- 合并DOM树和CSS规则树,生成render树
- 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
- 绘制render树(paint),绘制页面像素信息
- 浏览器会将各层的信息发给GPU,GPU会将各层合成(composite),显示在屏幕上
解析HTML,构建DOM树
Bytes → characters → tokens → nodes → DOM
- Conversion转换:浏览器将获得的HTML内容(Bytes)基于他的编码转换为单个字符
- Tokenizing分词:浏览器按照HTML规范标准将这些字符转换为不同的标记token。每个token都有自己独特的含义以及规则集
- Lexing词法分析:分词的结果是得到一堆的token,此时把他们转换为对象,这些对象分别定义他们的属性和规则
- DOM构建:因为HTML标记定义的就是不同标签之间的关系,这个关系就像是一个树形结构一样。
解析CSS,生成CSS规则树
Bytes → characters → tokens → nodes → CSSOM
构建渲染树
当DOM树和CSSOM都有了后,就要开始构建渲染树了。一般来说,渲染树和DOM树相对应的,但不是严格意义上的一一对应。因为有一些不可见的DOM元素不会插入到渲染树中,如head这种不可见的标签或者 display: none 等。
渲染
有了render树,接下来就是开始渲染,基本流程如下:
途中重要的四个步骤:
- 计算CSS样式
- 构建渲染树
- 布局,主要定位坐标和大小,是否换行,各种position overflow z-index属性
- 绘制,将图像绘制出来
图中的线与箭头代表通过js动态修改了DOM或CSS,导致了重新布局(Layout)或渲染(Repaint)。
这里Layout和Repaint的概念是有区别的:
- Layout,也称为Reflow,即回流。一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树
- Repaint,即重绘。意味着元素发生的改变只是影响了元素的一些外观之类的时候(如背景色、文字颜色等),此时只需要应用新样式绘制这个元素就可以了
什么会引起回流?
- 页面渲染初始化
- DOM结构改变,比如删除了某个节点
- render树变化,比如减少了padding
- 窗口resize
- 最复杂的一种:获取某些属性,引发回流
很多浏览器会对回流做优化,会等到数量足够时做一次批处理回流,但是除了render树的直接变化,当获取一些属性时,浏览器为了获得正确的值也会触发回流,这样使得浏览器优化无效,包括:
- offset(Top/Left/Width/Height)
- scroll(Top/Left/Width/Height)
- cilent(Top/Left/Width/Height)
- width,height
- 调用了getComputedStyle()或者IE的currentStyle
回流一定伴随着重绘,重绘却可以单独出现。所以有一些优化方案:
- 减少逐项更改样式,最好一次性更改style,或者将样式定义为class并一次性更新
- 避免循环操作dom,创建一个documentFragment或div,在它上面应用所有DOM操作,最后再把它添加到window.document
- 避免多次读取offset等属性。无法避免则将它们缓存到变量
- 将复杂的元素绝对定位或固定定位,使得它脱离文档流,否则回流代价会很高
注意:改变字体大小会引发回流
资源外链的下载
这里将遇到的静态资源分为一下几大类(未列举所有):
-
CSS样式资源
-
JS脚本资源
-
img图片类资源
当遇到上述的外链时,会单独开启一个下载线程去下载资源(http1.1中是每一个资源的下载都要开启一个http请求,对应一个tcp/ip链接)。
遇到CSS样式资源
CSS资源的处理有几个特定:
- CSS下载时异步,不会阻塞浏览器构建DOM树
- 但是会阻塞渲染,也就是在构建render时,会等到css下载解析完毕后才进行(这点与浏览器优化有关,防止css规则不断改变,避免了重复的构建)
- 有例外,
media query
声明的CSS是不会阻塞渲染的
遇到JS脚本资源
JS脚本资源的处理有几个特点:
-
阻塞浏览器的解析,也就是说发现一个外链脚本时,需等待脚本下载完成并执行后才会继续解析HTML
-
浏览器的优化,一般现代浏览器有优化,在脚本阻塞时,也会继续下载其它资源(当然有并发上限),但是虽然脚本可以并行下载,解析过程仍然是阻塞的,也就是说必须这个脚本执行完毕后才会接下来的解析,并行下载只是一种优化而已
-
defer与async,普通的脚本是会阻塞浏览器解析的,但是可以加上defer或async属性,这样脚本就变成异步了,可以等到解析完毕后再执行
注意,defer和async是有区别的: defer是延迟执行,而async是异步执行。
-
async
是异步执行,异步下载完毕后就会执行,不确保执行顺序,一定在onload
前,但不确定在DOMContentLoaded
事件的前或后 -
defer
是延迟执行,在浏览器看起来的效果像是将脚本放在了body
后面一样(虽然按规范应该是在DOMContentLoaded
事件前,但实际上不同浏览器的优化效果不一样,也有可能在它后面)
遇到img图片类资源
遇到图片等资源时,直接就是异步下载,不会阻塞解析,下载完毕后直接用图片替换原有src的地方。
loaded和domcontentloaded
-
DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片(譬如如果有async加载的脚本就不一定完成)
-
load 事件触发时,页面上所有的DOM,样式表,脚本,图片都已经加载完成了
7.CSS的可视化格式模型
-
CSS中规定每一个元素都有自己的盒子模型(相当于规定了这个元素如何显示)
-
然后可视化格式模型则是把这些盒子按照规则摆放到页面上,也就是如何布局
-
换句话说,盒子模型规定了怎么在页面里摆放盒子,盒子的相互作用等等
CSS的可视化格式模型就是规定了浏览器在页面中如何处理文档树。
关键字:
-
包含块(Containing Block)
-
控制框(Controlling Box)
-
BFC(Block Formatting Context)
-
IFC(Inline Formatting Context)
-
定位体系
-
浮动
-
...
另外,CSS有三种定位机制: 普通流
, 浮动
, 绝对定位
,如无特别提及,下文中都是针对普通流中的。
包含块(Containing Block)
一个元素的box的定位和尺寸,会与某一矩形框有关,这个框就称之为包含块。元素会为它的子孙元素创建包含块,但是,并不是说元素的包含块就是它的父元素,元素的包含块与它的祖先元素的样式等有关系。
譬如:
-
根元素是最顶端的元素,它没有父节点,它的包含块就是初始包含块
-
static和relative的包含块由它最近的块级、单元格或者行内块祖先元素的内容框(content)创建
-
fixed的包含块是当前可视窗口
-
absolute的包含块由它最近的position 属性为
absolute
、relative
或者fixed
的祖先元素创建-
如果其祖先元素是行内元素,则包含块取决于其祖先元素的
direction
特性 -
如果祖先元素不是行内元素,那么包含块的区域应该是祖先元素的内边距边界
-
控制框(Controlling Box)
块级元素和块框以及行内元素和行框的相关概念。
块框:
-
块级元素会生成一个块框(
BlockBox
),块框会占据一整行,用来包含子box和生成的内容 -
块框同时也是一个块包含框(
ContainingBox
),里面要么只包含块框,要么只包含行内框(不能混杂),如果块框内部有块级元素也有行内元素,那么行内元素会被匿名块框包围
行内框
-
一个行内元素生成一个行内框
-
行内元素能排在一行,允许左右有其它元素
display属性的影响
display
的几个属性也可以影响不同框的生成:
-
block
,元素生成一个块框 -
inline
,元素产生一个或多个的行内框 -
inline-block
,元素产生一个行内级块框,行内块框的内部会被当作块块来格式化,而此元素本身会被当作行内级框来格式化(这也是为什么会产生BFC
) -
none
,不生成框,不再格式化结构中,当然了,另一个visibility:hidden
则会产生一个不可见的框
总结:
-
如果一个框里,有一个块级元素,那么这个框里的内容都会被当作块框来进行格式化,因为只要出现了块级元素,就会将里面的内容分块几块,每一块独占一行(出现行内可以用匿名块框解决)
-
如果一个框里,没有任何块级元素,那么这个框里的内容会被当成行内框来格式化,因为里面的内容是按照顺序成行的排列
BFC(Block Formatting Context)
BFC规则:
在块格式化上下文中,每一个元素左外边与包含块的左边相接触(对于从右到左的格式化,右外边接触右边),即使存在浮动也是如此(所以浮动元素正常会直接贴近它的包含块的左边,与普通元素重合),除非这个元素也创建了一个新的BFC。
总结几点BFC特点:
-
内部
box
在垂直方向,一个接一个的放置 -
box的垂直方向由
margin
决定,属于同一个BFC的两个box间的margin会重叠 -
BFC区域不会与
floatbox
重叠(可用于排版) -
BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此
-
计算BFC的高度时,浮动元素也参与计算(不会浮动坍塌)
如何触发BFC?
-
根元素
-
float
属性不为none
-
position
为absolute
或fixed
-
display
为inline-block
,flex
,inline-flex
,table
,table-cell
,table-caption
-
overflow
不为visible
这里提下, display:table
,它本身不产生BFC,但是它会产生匿名框(包含 display:table-cell
的框),而这个匿名框产生BFC。更多请自行网上搜索。
IFC(Inline Formatting Context)
IFC即行内框产生的格式上下文。
IFC规则
在行内格式化上下文中,框一个接一个地水平排列,起点是包含块的顶部。水平方向上的 margin,border 和 padding 在框之间得到保留,框在垂直方向上可以以不同的方式对齐:它们的顶部或底部对齐,或根据其中文字的基线对齐。
行框
包含那些框的长方形区域,会形成一行,叫做行框。行框的宽度由它的包含块和其中的浮动元素决定,高度的确定由行高度计算规则决定。
行框的规则:
-
如果几个行内框在水平方向无法放入一个行框内,它们可以分配在两个或多个垂直堆叠的行框中(即行内框的分割)
-
行框在堆叠时没有垂直方向上的分割且永不重叠
-
行框的高度总是足够容纳所包含的所有框。不过,它可能高于它包含的最高的框(例如,框对齐会引起基线对齐)
-
行框的左边接触到其包含块的左边,右边接触到其包含块的右边
8.JS引擎解析过程
JS的解释阶段
JS是解释型语言,所以它无需提前编译,而是由解释器实时运行
引擎对JS的处理过程可以简述如下:
- 读取代码,进行词法分析,然后将代码分解成词元
- 对词元进行语法分析,然后将代码整理成语法树
- 使用翻译器,将代码转为字节码
- 使用字节码解释器,将字节码转为机器码
最终计算机执行的就是机器码。为了提高运行速度,现代浏览器一般采用即时编译( JIT-JustInTimecompiler
)。即字节码只在运行时编译,用到哪一行就编译哪一行,并且把编译结果缓存( inlinecache
),这样整个程序的运行速度能得到显著提升。而且,不同浏览器策略可能还不同,有的浏览器就省略了字节码的翻译步骤,直接转为机器码(如chrome的v8)。
总结起来可以认为是: 核心的 JIT
编译器将源码编译成机器码运行
JS的预处理阶段
上述将的是解释器的整体过程,这里再提下在正式执行JS前,还会有一个预处理阶段(譬如变量提升,分号补全等)。
预处理阶段会做一些事情,确保JS可以正确执行,这里仅提部分:
分号补全
JS执行是需要分号的,但为什么以下语句却可以正常运行呢?
console.log('a')
console.log('b')
原因就是JS解释器有一个Semicolon Insertion规则,它会按照一定规则,在适当的位置补充分号。
譬如列举几条自动加分号的规则:
-
当有换行符(包括含有换行符的多行注释),并且下一个
token
没法跟前面的语法匹配时,会自动补分号。 -
当有
}
时,如果缺少分号,会补分号。 -
程序源代码结束时,如果缺少分号,会补分号。
于是,上述的代码就变成了:
console.log('a');
console.log('b');
所以可以正常运行。
当然了,这里有一个经典的例子:
function b () {
return
{
a: 'a'
};
}
由于分号补全机制,所以它变成了:
function b () {
return;
{
a: 'a'
};
}
所以运行后是 undefined
变量提升
一般包括函数提升和变量提升。
a = 1;
b();
function b () {
console.log('b');
}
var a;
经过变量提升后,就变成:
function b () {
console.log('b');
}
var a;
a = 1;
b();