内容整理来自极客时间李兵老师所讲专栏《浏览器工作原理与实践》
极客时间
-
浏览器中的进程与线程
进程:一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。
线程:
1. 线程是不能单独存在的,它是由进程来启动和管理的。
2. 线程是依附于进程的。
进程与线程之间的关系:
1. 进程中的任意一线程执行出错,都会导致整个进程的崩溃。
2. 线程之间共享进程中的数据。
3. 当一个进程关闭之后,操作系统会回收进程所占用的内存。
4. 进程之间的内容相互隔离。
-
多进程浏览器 chrome
仅仅打开了 1 个页面,为什么有 4 个进程?因为打开 1 个页面至少需要 1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程,共 4 个;如果打开的页面有运行插件的话,还需要再加上 1 个插件进程。
-
TCP协议:如何保证页面文件能被完整送达浏览器?
- 数据包要在互联网上进行传输,就要符合网际协议(Internet Protocol,简称 IP)标准。计算机的地址就称为 IP 地址,访问任何网站实际上只是你的计算机向另外一台计算机请求信息。
- IP 是非常底层的协议,因此,需要基于 IP 之上开发能和应用打交道的协议,最常见的是“用户数据包协议(User Datagram Protocol)”,简称 UDP。UDP 中一个最重要的信息是端口号,端口号其实就是一个数字,每个想访问网络的程序都需要绑定一个端口号。通过端口号 UDP 就能把指定的数据包发送给指定的程序了,所以 IP 通过 IP 地址信息把数据包发送给指定的电脑,而 UDP 通过端口号把数据包分发给正确的程序。
- 使用UDP协议来传输文件会存在两个问题:
a. 数据包在传输过程中容易丢失;
b. 大文件会被拆分成很多小的数据包来传输,这些小的数据包会经过不同的路由,并在不同的时间到达接收端,而 UDP 协议并不知道如何组装这些数据包,从而把这些数据包还原成完整的文件。 - TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP特点如下:
a. 对于数据包丢失的情况,TCP 提供重传机制;
b. TCP 引入了数据包排序机制,用来保证把乱序的数据包组合成一个完整的文件。
- 完整的TCP链接过程
- 数据包要在互联网上进行传输,就要符合网际协议(Internet Protocol,简称 IP)标准。计算机的地址就称为 IP 地址,访问任何网站实际上只是你的计算机向另外一台计算机请求信息。
-
从输入URL到页面展示,这中间发生了什么?
- 判断用户输入的是关键词还是url地址
1.1 关键词:则调用浏览器默认搜索引擎来进行搜索(加上内容合同url)
1.2 url地址:则加上协议(如https)合成完整的url - 按下回车,浏览器进程通过IPC(进程间通信)把url传给网络进程(当网络进程接收到url才发起真正的网络请求)
2.1 此时浏览器导航栏显示loading状态,但是页面还是呈现前一个页面,这是因为新页面的响应数据还没有获得 - 网络进程接收到url后,先查找有没有本地缓存
3.1 有缓存,则拦截请求,直接返回200的缓存的资源。
3.2 无缓存,则进入真正的网络请求过程。 - 网络进程请求DNS返回域名所对应的IP和端口号
4.1 首先获取域名的IP,系统会自动从hosts文件中寻找域名对应的IP地址,若找到则和服务器建立TCP链接。
4.2 若没找到,则进行DNS解析找到IP地址。
4.3 若之前DNS数据缓存服务缓存过当前域名信息,就会直接返回缓存信息,否则,发起请求获取域名解析出来的IP和端口号,如果没有端口号,http默认80端口,https默认443端口,若还是https请求,则还需要建立TLS链接。 - Chrome有个机制,同一域名同时最多只能建立6个TCP链接,若同一个域名下同时有10个请求发生,那么其他4个排队等待,若小于6个,则直接建立。
- TCP三次握手建立连接,http请求加上TCP头部-包括端口号,目的程序款口号和用域校验数据完整性的序号,向下传输
- 网络层在数据包上加上IP头部-包括源IP地址和目的IP地址,继续向下传输到底层
- 底层通过物理网络传输给目的服务器主机
- 目的服务器主机传输层收到数据包,解析出IP头部,识别数据部分,将解开的数据包向上传输到应用层
- 应用层http解析请求头和请求体
10.1 若需要进行重定向,则http直接返回数据状态码为301,302,同时在请求头的Location字段中附上重定向地址,浏览器根据code和location进行重定向操作。
10.2 若不需要重定向,首先服务器会根据请求头中的if-None-Match的值来判断请求的资源是否被更新了,若没有更新则返回304状态码,相当于告诉浏览器之前的缓存还可以继续使用,就不返回新数据了。
10.3 否则,则返回新数据,200状态码,并且如果想要浏览器缓存数据的话,就在响应头中加入字段:Cache-Control:Max-age=2000(缓存时间2秒) - 响应头中的Content-Type属性告诉了浏览器服务器返回的数据是什么类型,若返回text/html则为网页类型,则浏览器准备渲染进程来渲染页面了,若为下载类型比如application/octet-stream,则是下载类型,请求会交给浏览器的下载进程
- 数据传输完成,TCP四次挥手断开连接,若浏览器或者服务器在http头部加上Connection:Keep-Alive属性的话,则TCP就保持一直连接,保持TCP连接可以节约下次建立连接的时间。
- 浏览器进程获取到通知,根据当前页面B是否从页面A打开的并且和页面A是否是同一个站点(根域名和协议医院就被认为是同一个站点),若满足上述条件,则复用之前网页的进程,否则,重新创建一个单独的渲染进程
- 浏览器发出“提交文档”的消息给渲染进程,渲染进程收到消息后,会和网络进程建立传输数据的“管道”,文档数据传输完成后,渲染进程会返回“确认提交”的消息给浏览器进程。
- 浏览器收到“确认提交”消息后,会更新浏览器的页面状态,包括了安全状态,地址栏的URL,前进后退的历史状态,并更新web页面,此时web页面是上个页面内容或者空白页面。
- 此时渲染进程这个沙盒会对文档进行页面解析和字子资源加载,HTML会通过HTML解析器转成DOM tree,css按照css规则和css解析器转换成CSSOM tree 两个tree结合,形成render tree,并且通过Layout布局方案计算出每个元素具体的宽高颜色位置,结合起来,开始绘制,最后显示新页面。
- 判断用户输入的是关键词还是url地址
-
HTML,CSS,JS如何变成页面的?
渲染进程中的流程:
-》(DOM)构建DOM树 (浏览器无法识别 则会将html文件转换成dom树结构体)
-》(StyleSheets)样式计算 (同理 浏览器无法识别css样式 则会转换为styleSheets结构)
1. 把css转换为浏览器能够理解的结构 其中css来源如下:
a. 通过link标签链接外部地址
b. 标签体内容
c. 行内样式
2. 转换样式表中的属性值,使其标准化
a. 比如em变成px
b. 比如颜色blue变成rgb
c. 比如bold变成700
3. 计算出DOM树中每个节点的具体样式
a. 涉及css的继承规则(每个子元素都会继承其父元素样式)
b. 涉及css的层叠规则(合并来自多个源的属性值)
-》(Layout)布局阶段
1. 创建布局树(由dom树和ComputedStyle计算样式 再生成布局树)
a. Dom树中包含了不可见元素,比如head标签,display:none属性设置,所以还会额外的构建一颗可见元素的布局树
b. 遍历dom树中所有可见节点,并加到布局树中,忽略其他不可见元素
2. 布局计算
-》(Layer)分层
1. 渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)
2. 浏览器的页面实际上被划分成了很多图层,这些图层叠加后合同了最终的页面
3. 并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层
4. 何时创建为图层:
a. 拥有层叠上下文属性的元素会被提升为单独的一层(如:明确定位属性的元素;定义透明属性的元素;使用css滤镜的元素)
b. 需要裁剪(clip)的地方会被创建图层(如文字超过div高度;出现滚动条)
-》(Drawing)绘制
1. 浏览器会把图层拆分成很多小的绘制指令
2. 形成一个绘制列表
以上均由渲染进程中的主线程来完成,接下来“主线程”将“commit”提交至“合成线程”来实际完成绘制
-》(Tile)分块
1. 屏幕上页面的可见区域就叫视口(ViewPort)
2. 由于用户只看的见这部分内容,所以浏览器的合成线程会将图层划分为图块(tile),这些图块大小通常是256x256或者512x512
-》(Raster)光栅化
1. 合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图
2. 渲染进程维护了一个栅格化的线程池
3. 通常栅格化都是GPU来加速生成的,该操作叫做快速栅格化,生成的位图保存在GPU内存中
-》(Display)合成显示
1. 在所有图块都被光栅化后,合成线程发出“DrawQuad“命令提交给”浏览器进程“
2. 浏览器进程有个viz组件,根据DrawQuad命令,将其内容绘制到内存中,最后将内存显示在屏幕上
总结上面的所有流程:
- 渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构。
- 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。
- 创建布局树,并计算元素的布局信息。
- 对布局树进行分层,并生成分层树。
- 为每个图层生成绘制列表,并将其提交到合成线程。
- 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
- 合成线程发送绘制图块命令 DrawQuad 给浏览器进程。浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。
如何理解“重绘“、”重排“、”合成“?- (重排)更新了元素的几何属性
如果你通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的。 - (重绘)更新元素绘制属性
如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。 - (合成)
比如使用了transform来实现动画效果,会避开重绘和重排,合成是非主线程来完成。
- (重排)更新了元素的几何属性
-
变量提升:javascript代码是按顺序执行的吗?
- Js代码执行流程:
变量和函数声明在代码里得位置是不会改变得,而且是在编译阶段被js引擎放入内存中;意思就是一段js代码在执行前需要被js引擎编译,编译完成后,才会进入执行阶段。
- 编译阶段:
主要就是做2件事,第一:变量提升部分代码;第二:执行部分的代码。
编译过后,会产生两部分内容:执行上下文(变量环境&词法环境等)和可执行代码。 - 一段代码如果定义了两个相同名字的函数,那么最终生效的是最后一个函数。
showName()
var showName = function(){console.log(2)}
showName()
function showName(){console.log(1)}
showName()
3.1 首先是编译阶段的 变量提升 则
Var showName = undefined
function showName(){console.log(1)}
3.2 然后是可执行代码阶段
ShowName()
showName = function(){console.log(2)}
showName()
showName()
3.3
即第一个showName函数执行 打印1
接下来将showName变量赋值成函数打印2
即后面showName函数都将打印2
- Js代码执行流程:
-
为什么js代码执行会出现“栈溢出“?
- 调用栈就是用来管理函数调用的关系的一种数据结构。
- 首先会对全局创建全局执行上下文,如果执行到函数的时候,并会创建该函数的执行上下文和可执行代码。
- 在执行上下文创建好后,JavaScript 引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈。
- 当函数返回的时候,当前函数的执行上下文就会从栈顶弹出,调用栈是 JavaScript 引擎追踪函数执行的一个机制。
- 如何查看当前js引擎栈有多深:
function computeMaxCallStackSize () {
try {
return 1 + computeMaxCallStackSize()
} catch (e) {
// Call stack overflow
return 1
}
}
computeMaxCallStackSize()
- 如何避免调用栈溢出:
• 循环代替递归
• 尾递归优化(safari浏览器支持,其他欠缺)
• 事件驱动 的特性,使用 “setTimeout”、 “nextTick” (利用事件循环机制) - 总结:
7.1 每调用一个函数,JavaScript 引擎会为其创建执行上下文,并把该执行上下文压入调用栈,然后 JavaScript 引擎开始执行函数代码。
7.2 如果在一个函数 A 中调用了另外一个函数 B,那么 JavaScript 引擎会为 B 函数创建执行上下文,并将 B 函数的执行上下文压入栈顶。
7.3 当前函数执行完毕后,JavaScript 引擎会将该函数的执行上下文弹出栈。当分配的调用栈空间被占满时,会引发“堆栈溢出”问题。
-
作用域链和闭包
- 作用域链
在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用成为outer,通过作用域查找变量的链条称为作用域链,作用域链是通过词法作用域来确定的,而词法作用域反映了代码的结构。
- 词法作用域
a. 词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
b. 词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。 - 闭包
a. 在js中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。
b. 如果该闭包会一直使用,那么它可以作为全局变量而存在,但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。
- 作用域链