浏览器工作原理与实践

导读

本文主要是从极客时间专栏《浏览器工作原理与实践》,对其每一章的学习进行系统性总结–总版。

宏观视角下的浏览器(6讲)

01.Chrome架构:仅仅打开一个页面,为什么有四个进程


在谷歌浏览器随便打开一个页面,点击“选项”菜单,选择“更多工具”,点击“任务管理器”,这将打开Chrome的任务管理器的窗口,你会发现一个页面上有四个以上的进程。为什么会有四个进程呢?
本章通过分析浏览器的进化史而展开探讨这个问题。
开始之前,我们必须得了解一下这个进程线程的概念。

线程 VS 进程

线程这个东西它是不能单独存在的,它是由进程来启动和管理的。

一个进程就是一个程序的运行实例

线程是依附于进程的,而进程中使用多线程并行处理能提升运算效率

总结,线程与进程之间的关系有以下四个特点:

  1. 进程中任一线程执行出错,都会导致整个进程崩溃。
  2. 线程之间共享进程的数据。
  3. 当一个进程关闭之后,操作系统会回收进程所占用的内存。
  4. 进程之间的内容都互相隔离。(如果进程之间需要进行数据的通信,这时候需要使用用于进程间通讯(IPC)机制了)。
单进程浏览器时代

早在07年之前,市面上的浏览器都是单进程的。单进程,顾名思义是指:浏览器的所有功能模块都运行在同一个进程里。
如此多的功能模块运行在一个进程里,导致单进程浏览器不稳定不流畅不安全
具体的表现就不说了,总之体验非常差,于是进入了“多线程浏览器”时代。

多进程浏览器时代
  • 多进程浏览器由于进程相互隔离,所以当一个页面或者插件崩溃的时候,影响的仅仅是当前的页面进程或者插件过程。这就完美解决了页面或者插件崩溃而导致的整个浏览器崩溃问题。JS的渲染若出现问题,影响的同样是当前的渲染页面,没有响应的仅对当前页面。而且在多进程浏览器时代,当关闭一个页面的时候,整个渲染进程会被关闭,该进程占用的内存都会被系统回收,这样也就轻松解决了浏览器页面的内存泄露问题。
  • 此外有关安全方面的问题:使用多线程架构可以使用安全沙箱

可以把沙箱看成是操作系统给进程上了一把锁,沙箱里面的程序可以运行,但是不能在硬盘上写入任何数据,也不能在敏感位置读取任何数据(例如文档和桌面),Chrome把插件进程和渲染进程锁在沙箱里面,这样即使在渲染进程或者插件进程里面执行了恶意程序,恶意程序也无法突破沙箱去获取系统权限。

目前多进程架构

最新的Chrome进程架构:Chrome浏览器包括:1个浏览器(Brower)主进程,1个GPU进程、一个网络(NetWork)进程、多个渲染进程和多个插件进程
虽然多进程模型提升了浏览器的稳定性、流畅性、安全性,但是同样也会不可避免带来一些问题:

  • 更高的资源占用 :因为每个进程都会包含公共基础结构的副本(如JavaScript运行环境),这意味着浏览器会消耗更多的资源。
  • 更复杂的体系架构:浏览器各模块之间耦合性高、扩展性差等问题,会导致现在的架构已经很难适应新的需求。
未来面向服务的架构(SOP)

为了解决这些问题,2016年,Chrome团队使用“面向服务的架构“(SOP)思想设计了新的Chrome架构,这也是现阶段Chrome团队的一个主要任务。

02|TCP协议:如何保证页面文件能被完整的送达浏览器


在衡量Web页面性能的时候有一个重要的指标叫”FP(First Paint)”,是指从页面加载到首次开始绘制的时长。其中影响FP的一个重要因素就是网络加载速度

要优化网络加载速度,需要对网络有充分的了解,这一节重点介绍在Web世界中的TCP/IP是如何工作的

在网络中,一个文件通常会被拆分为很多数据包来进行传输,而数据包在传输过程中有很大概览丢失或者出错,那么如何保证页面文件能被完整地送达浏览器?

一个数据包的“旅程”
  1. IP:把数据包送达目的主机
    计算机的地址称为IP地址,访问任何网站实际上只是你的计算机向另外一台计算机请求信息。
    当从主机A向主机B发送数据(即发送数据包),传输前,数据包会被附加上主机A和主机B的IP地址信息,这些信息会被封到一个叫做IP头的数据结构里,在这个IP头中包含IP数据包开头的信息(IP版本、源IP地址、目标地址、生存时间等信息),于是数据包从主机A发送到主机B。

  2. UDP:把数据包送达应用程序
    IP通过IP地址信息把数据包发送给指定的电脑,而UDP通过端口号把数据包发给正确的程序。
    UDP发送数据,有各种因素会导致数据包出错,虽然UDP可以校验数据是否正确,但是UDP不提供重发机制,只是丢弃当前的包,且UDP在发送之后无法知道能否到达目的地。
    UDP不能保证数据可靠性,但是传输速度却非常快,因此UDP应用在一些关注速度但不那么严格要求数据完整性的领域,例如:在线视频、互动游戏等。

  3. TCP:把数据完整的送达应用程序
    TCP(Transmission Control Protocol,传输控制协议):他是一种面向连接的、可靠的、基于字节流的传输层通信协议。相对于UDP而言:

  • 对于数据包丢失情况,提供重传机制。
  • TCP引入数据包排序机制,用来保证把乱序的数据包组合成一个完整的文件。
完整的TCP连接过程

我们现在已经知道TCP单个数据包的传输流程和UDP流程差不多,不同在于,通过TCP头信息可以保证一块大的数据传输的完整性。
一个完整的TCP连接过程,其生命周期包括了“建立连接“、”数据传输“、”断开连接“三个阶段。

  • 建立连接阶段:这个阶段通过“三次握手”来建立客户端和服务器之间的连接。
  • 数据传输阶段:在该阶段,接收端需要对每个数据包进行确认操作。接收端应该在接收数据后要发送确认数据包给发送端,若发送端没有接收到这个确认,则判断数据包丢失,并触发发送端的重发机制。
  • 断开连接阶段:数据传输完毕,终止连接,通过最后一个阶段“四次挥手”来保证双方都能断开连接。

03|HTTP请求流程:为什么很多站点第二次打开速度会很快?


首先的首先我们知道:HTTP协议建立在TCP连接基础之上的。HTTP是一种允许浏览器向服务器获取资源的协议,是Web的基础。HTTP是浏览器使用最广的协议

简单说说HTTP和TCP的关系:浏览器使用HTTP协议作为应用层协议,用来封装请求的文本信息,并使用TCP/IP作为传输层协议将它发到网路上,所以HTTP工作前,需要通过TCP与服务器建立连接,也就是说:HTTP的内容是通过TCP的传输数据阶段来实现的

浏览器端发起HTTP请求流程

如果在浏览器地址栏里输入:https://www.liugezhou.online 这个网址后,浏览器这个庞然大物,它的背后都做些什么呢?

  • 构建请求

    首先浏览器构建请求行信息,构建好之后,浏览器准备发起网路请求。

  • 查找缓存

    在准备发起网路请求阶段,浏览器偷偷的在它的缓存中查询是否有要请求的资源。
    若有:拦截请求,返回资源副本,直接结束请求。
    若缓存查找失败:继续下一步。

  • 准备IP地址和端口号

    这个IP地址和端口号的获取,肯定是通过域名与其映射,即“域名系统”,也就是我们熟知的DNS。
    于是,浏览器第一步会请求DNS返回域名对应的IP,如果没有特别指明端口号,则默认为80。
    (浏览器提供了DNS数据缓存服务,若缓存过也就不会去请求,直接解析。从而减少一次网络请求)

  • 等待TCP队列

    拿到IP地址与端口号后,还需要在TCP队列中排队才能建立TCP连接。
    这是因为:Chrome有个机制,同一个域名同时最多只能建立6个TCP连接,若此刻同时有10个请求发生。则四个会进入TCP队列进行排队。
    当然,若当前请求数量少于6个,则会直接进入下一步。

  • 建立TCP连接

    建立TCP连接,上一节我们已经知道,一个完整的TCP连接过程包括“建立连接”、“数据传输”、“断开连接“三个阶段。

  • 发送HTTP请求

    HTTP请求是在TCP连接的数据传输阶段工作的,这个时候浏览器向服务器发送请求行,它包括请求方法、请求URI、HTTP版本协议。,HTTP中的数据在这个通信过程中传输。

服务器端处理HTTP请求流程

这里可以在命令行中输入curl -i https://www.google.com来查看返回请求数据。 (-i 返回响应行、响应头和响应体信息。 -I 不返回响应体。)
返回网站的HTTP协议、Connection、Location、Cache-Control等信息。

通常情况,一旦服务器向客户端返回了请求数据,它就要关闭TCP连接,但是如果浏览器或者服务器设置了Connection:keep-alive,那么TCP连接在发送后将仍保持打开状态。保持TCP连接可以省去下次请求时需要建立连接的时间,提升资源加载速度

问题解答
  1. 为什么很多站点第二次打开速度会很快?

    主要原因肯定是第一次加载页面的过程中,缓存了一些数据(从上面的过程分析,我们知道DNS缓存页面资源缓存这两块数据是会被浏览器缓存起来的).
    网站把很多资源都缓存到了本地,浏览器缓存直接使用本地副本来回应请求,而不会产生真实的网络请求,从而节省了时间。

  2. 登录状态是如何保持的

    简单地说,如果服务器端发送的响应头内有 Set-Cookie 的字段,那么浏览器就会将该字段的内容保持到本地。当下次客户端再往该服务器发送请求时,客户端会自动在请求头中加入 Cookie 值后再发送出去。服务器端发现客户端发送过来的 Cookie 后,会去检查究竟是从哪一个客户端发来的连接请求,然后对比服务器上的记录,最后得到该用户的状态信息。

04|导航流程:从输入URl到页面展示,这中间发生了什么


流程开始前,回顾下浏览器进程、网络进程、渲染进程的各自主要职责:

  • 浏览器进程:主要负责用户交互子进程管理文件储存等功能。
  • 网络进程:面向渲染进程或浏览器进程提供网络资源下载
  • 渲染进程:将HTML、CSS、JS、图片等资源解析为可以显示和交互的页面。
过程大致描述
  • 首先,用户从浏览器进程中输入请求信息。
  • 然后,网络进程发起URL请求。
  • 服务器响应URL请求后,浏览器进程开始准备渲染进程。
  • 渲染进程准备好以后,需要先向渲染进程提交页面数据,这称之为文档提交阶段。
  • 渲染进程接收到文档信息之后,便开始解析页面和加载子资源,完成页面的渲染。
    这其中,用户发出URL请求到页面开始解析的过程,就叫做导航。
从输入URL到页面展示–过程细节
  • 响应数据类型处理:根据服务端返回的Content-Type字段来决定如何显示响应体的内容。
  • 同一站点下的多个页面会运行在一个渲染进程中。
“从输入 URL 到页面展示,这中间发生了什么?”(留言总结)
  1. 用户输入url并回车·
  2. 浏览器进程检查url,组装协议,构成完整的url
  3. 浏览器进程通过进程间通信(IPC)把url请求发送给网络进程
  4. 网络进程接收到url请求后检查本地缓存是否缓存了该请求资源,如果有则将该资源返回给浏览器进程
  5. 如果没有,网络进程向web服务器发起http请求(网络请求),请求流程如下:
    5.1 进行DNS解析,获取服务器ip地址,端口
    5.2 利用ip地址和服务器建立tcp连接
    5.3 构建请求头信息
    5.4 发送请求头信息
    5.5 服务器响应后,网络进程接收响应头和响应信息,并解析响应内容
  6. 网络进程解析响应流程;
    6.1 检查状态码,如果是301/302,则需要重定向,从Location自动中读取地址,重新进行第4步
    6.2 200响应处理:
    检查响应类型Content-Type,如果是字节流类型,则将该请求提交给下载管理器,该导航流程结束,不再进行后续的渲染,如果是html则通知浏览器进程准备渲染进程准备进行渲染。
  7. 准备渲染进程
    7.1 浏览器进程检查当前url是否和之前打开的渲染进程根域名是否相同,如果相同,则复用原来的进程,如果不同,则开启新的渲染进程
  8. 传输数据、更新状态
    8.1 渲染进程准备好后,浏览器向渲染进程发起“提交文档”的消息,渲染进程接收到消息和网络进程建立传输数据的“管道”
    8.2 渲染进程接收完数据后,向浏览器发送“确认提交”
    8.3 浏览器进程接收到确认消息后更新浏览器界面状态:安全、地址栏url、前进后退的历史状态、更新web页面。

05|渲染流程(上):HTML、CSS和JavaScript,是如何变成页面的


按照渲染的时间顺序,渲染流水线可分为以下几个构建阶段:
构建DOM树样式计算布局阶段分层绘制光栅化合成
本节主要讨论前三个阶段。

构建DOM树

因为浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树。
DOM树和HTML内容几乎一样,但和HTML不同的是:DOM是保存在内存中的树结构。

样式计算
  1. 把CSS转换为浏览器能够理解的结构。
  2. 转换样式表中的属性值,使其标准化。
    「例如rem -> px, red -> rgb(255,0,0),bold -> 700」
  3. 计算出DOM树中每个节点的具体样式。
    「CSS继承:每个DOM节点都包含有父节点的样式」
    「CSS层叠:它在 CSS 处于核心地位,定义了如何合并来自多个源的属性值的算法」
    「样式来源:如果一个元素不提供任何样式,默认使用的是UserAgent样式—浏览器提供的一组默认样式」。
布局阶段

内容: 布局阶段是根据DOM树和样式计算出元素的几何位置。

  1. 创建布局树「构建一颗只包含可见元素的布局树」
  2. 布局计算

「在执行布局操作的时候,会把布局运算的结果重新写回布局树中,所以布局树既是输入内容也是输出内容,这是布局阶段一个不合理的地方,因为在布局阶段并没有清晰地将输入内容和输出内容区分开来。针对这个问题,Chrome 团队正在重构布局代码,下一代布局系统叫 LayoutNG,试图更清晰地分离输入和输出,从而让新设计的布局算法更加简单。」

06|渲染流程(下):HTML、CSS和JavaScript,是如何变成页面的


分层

为了生成一些复杂效果(3D变换、页面滚动、z轴排序等),渲染引擎还需要为特定的节点生成专用的图层,生成一颗对应的图层树.

  • 并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层.
  • 素有了层叠上下文的属性或者需要被剪裁,满足这任意一点,就会被提升成为单独一层。
绘制

一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表。「可在浏览器开发者工具的Layers中查看。」

栅格化操作

所谓栅格化,是指将图块转换为位图。
栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中.

合成和显示

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。经过浏览器就会显示出页面。

一个完整的渲染流程大致可总结为如下:

  • 渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构。
  • 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。
  • 创建布局树,并计算元素的布局信息。对布局树进行分层,并生成分层树。
  • 为每个图层生成绘制列表,并将其提交到合成线程。
  • 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
  • 合成线程发送绘制图块命令 DrawQuad 给浏览器进程。浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。
相关概念
重排(更新了元素的几何属性)

使用CSS或者JS使元素的几何位置发生了改变,例如改变元素的宽度、高度等,这会使得浏览器触发重新布局、解析之后的一系列子阶段,这个过程就是重排。无疑,重排需要更新完整的渲染流水线,所以开销是最大的。

重绘(更新元素的绘制属性)

比如改变了元素的背景色,这会触发浏览器进行重绘之后的操作。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。

直接合成阶段

那如果你更改一个既不要布局也不要绘制的属性,会发生什么变化呢?渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做合成.
例如CSS的transform实现的动画效果,可以避开重排和重绘阶段,相对于重绘和重排,合成能大大提升绘制效率。

浏览器中的JavaScript执行机制 (5讲)

07|变量提升:JavaScript代码是按顺序执行的吗?


本节主要讲解执行上下文相关的内容。

通过一些代码的执行顺序与经验我们知道:

  • 在执行过程中,若使用了未声明的变量,那么 JavaScript 执行会报错。
  • 在一个变量定义之前使用它,不会出错,但是该变量的值会为 undefined,而不是定义时的值。
  • 在一个函数定义之前使用它,不会出错,且函数能正确执行。
变量提升

所谓的变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined.
之所以会发生变量提升,是因为一段JavaScript代码在执行之前,需要被JavaScript引擎编译,编译完成之后,才会进入执行阶段。也就是说在编译阶段,变量和函数的声明提升到了开头。

08 |调用栈:为什么JavaScript代码会出现栈溢出?


一般有三种情况,当一段代码执行的时候JS引擎对其进行编译并创建执行上下文:

  1. 当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份.
  2. 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。
  3. 当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行上下文。
小结
  • 每调用一个函数,JavaScript 引擎会为其创建执行上下文,并把该执行上下文压入调用栈,然后 JavaScript 引擎开始执行函数代码。
  • 如果在一个函数 A 中调用了另外一个函数 B,那么 JavaScript 引擎会为 B 函数创建执行上下文,并将 B 函数的执行上下文压入栈顶。
  • 当前函数执行完毕后,JavaScript 引擎会将该函数的执行上下文弹出栈。
  • 当分配的调用栈空间被占满时,会引发“堆栈溢出”问题。

09 | 块级作用域:var缺陷以及为什么要引入let和const


作用域

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。
ES6出现之前,JS的作用域只有两种:全局作用域函数作用域。 ES6出现,引入了块级作用域

在同一段代码中,ES6 是如何做到既要支持变量提升的特性,又要支持块级作用域的呢?

当一段代码里面既有var声明的变量也有let声明的变量的时候:

  • 函数内部通过var声明的变量,在编译阶段全都被存放到变量环境里面.
  • 通过let声明的变量,在编译阶段会被存放到词法环境中。
  • 在函数作用域内部,通过let声明的变量并没有被存放到词法环境中。
    也就是说:通过理解词法环境的结构和工作机制,块级作用域是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现的,通过两者的结合,JavaScript引擎也就同时支持了变量 提升和块级作用域了。

10 | 作用域和闭包:代码中出现相同的变量,JavaScript引擎是如何选择的


作用域链

理解作用域链是理解闭包的基础,而闭包在JavaScript中无处不在,同时作用域和作用域链还是作用语言的基础,所以我们先来学习一下作用域链
理解了调用栈、执行上下文、词法环境、变量环境等概念,那么你理解起来作用域链也会很容易,看下面一段代码:


function bar() {
   console.log(myName)
}
function foo() {
   var myName = "局部变量"
   bar()
}
var myName = "全局变量"
foo()

通过上面的代码,我们知道最终打印出来的结果是:”全局变量“。
这是因为,当一段代码使用了一个变量后,JavaScript引擎会首先在“当前的执行上下文”中去查找该变量。若没有找到,由于每个执行上下文都包含一个外部引用指向外部执行上下文,所以bar函数中的变量会去全局上下文中区域查找。我们把这个查找的链条就称为作用域链。

词法作用域

foo 函数调用的 bar 函数,那为什么 bar 函数的外部引用是全局执行上下文,而不是 foo 函数的执行上下文?了解这个问题我们继续来学习词法作用域:
词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
然后,根据词法作用域,foo 和 bar 的上级作用域都是全局作用域,所以如果 foo 或者 bar 函数使用了一个它们没有定义的变量,那么它们会到全局作用域去查找。也就是说,词法作用域是代码阶段就决定好的,和函数是怎么调用的没有关系。

闭包

在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。
在使用闭包的时候,要尽量注意一个原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。

11 | this:从JavaScript执行上下文的视角讲清楚this


首先我们要知道,在对象内部的方法中使用对象内部的属性是一个非常普遍的需求,但是JavaScript作用域机制并不支持这一点,基于这个需求,JavaScript搞出了一套this机制。

在前几节中,我们提到执行上下文中包含了:变量环境词法环境外部环境、还有一个没有提及的this,this是和执行上下文绑定的,每个执行上下文都有一个this。
在08节我们总结了执行上下文主要分三种:全局执行上下文、函数执行上下文和eval执行上下文。
对应的this也只有这三种:全局执行上下文中的this、函数执行上下中的this和eval中的this(不做讨论)。

  • 全局执行上下文中的this:全局执行上下文中的this指向window对象。
  • 函数执行上下文中的this:
    1. 默认情况下调用一个函数,其执行上下文中的 this 也是指向 window 对象的.
    2. 通过函数的call方法设置其this指向其他对象(还可以使用bind和apply方法来设置函数执行上下文中的this)。
    3. 通过对象调用方法设置。(使用对象来调用其内部的一个方法,该方法的 this 是指向对象本身的。在全局环境中调用一个函数,函数内部的this指向的是全局变量window)。
    4. 通过构造函数中设置。
this的设计缺陷以及应对方案
  1. 嵌套函数的this不会从外层函数中继承。==> 1⃣️、将this保存一个self变量,利用变量作用域机制传递给嵌套函数。2⃣️、将乔套函数改为箭头函数。
  2. 普通函数中的this默认指向全局对象window。==>可以通过设置JavaScript的“严格模式”来解决。

V8工作原理(3讲)


12 | 栈空间和堆空间:数据是如何存储的?

我们把这种在使用之前就需要确认其变量数据类型的称为静态语言
相反地,我们把在运行过程中需要检查数据类型的语言称为动态语言

通常把偷偷转换的操作成为隐式类型转换:支持因此类型转换的语言称为弱类型语言,不支持隐式类型转换的语言称为强类语言

原始数据类型的值都是直接保存在“栈”中,引用数据类型的值是存放在“堆”中的。

13 | 垃圾回收:垃圾数据是如何自动回收的?


通常情况下:垃圾数据回收分为手动回收自动回收两种策略。

JavaScript、Java、Python 等语言,产生的垃圾数据是由垃圾回收期来释放的,并不需要手动通过代码来释放。

调用栈中的数据是如何回收的

在调用栈中,有一个记录当前执行状态的指针(称为ESP),当一个函数执行结束之后,JavaScript引擎会通过向下移动ESP来销毁该函数保存在栈中的执行上下文。

堆中的数据是如何回收的

堆中的数据是如何回收的—-回收堆中的垃圾数据,需要用到JavaScript中的垃圾回收期

前置知识点:代际假说的两个特点:(代际假说时垃圾回收领域一个重要的术语)

  • 第一个是大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问。
  • 第二个是不死的对象,会活的更久。

有了代际假说基础,我们便可以探讨V8是如何实现垃圾回收的了:
在V8中会把堆分为新生代老生代两个区域,新生代存放的是生存时间极短的对象,老生代中存放的是生存时间久的对象

  • 副垃圾回收器,主要负责新生代的垃圾回收。
  • 主垃圾回收期,主要负责老生代的垃圾回收。

接下来我们开始分析垃圾回收期的工作流程
V8把堆分成了两个区域,并分别使用不同的垃圾回收期,但不论什么类型回收期,他们使用的是一套共同的执行流程。

副垃圾回收器

新生代中用Scavenge算法来处理:即把新生代空间对半划分为两个区域:一半是对象区域,一半是空闲区域
对象区域与回收区域会经过反复的角色翻转操作。
JavaScipt引擎采用了对象晋升策略,也就是经过两次垃圾回收仍然存活的对象,会被移动到老生区中。

主垃圾回收器

主要负责老生区中的垃圾回收,老生区中的对象有两个特点:一个是对象占用空间大,一个是对象存活时间长。
采用标记-清除(Mark-Sweep)的算法进行垃圾回收。
由于碎片过多而导致大对象无法分配到足够的连续内存,于是又产生来另外一种算法–标记-整理(Mark-Compact)。

全停顿:为了降低老生代的垃圾回收而造成的卡顿,V8将标记过程分为一个个的子标记过程,同时让垃圾回收标记和JavaScript应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为增量标记(Incremental Marking)算法。

14 | 编译器和解释器:V8是如何执行一段JavaScript代码的?


了解V8的编译流程能让你对语言以及相关工具有更充分的认识。
深入理解V8的工作原理,这里我们需要搞清楚一些概念和原理:编译器(Compiler)解释器(Interpreter)抽象语法树(AST)字节码(Bytecode)即时编译器(JIT)等概念。

编译器和解释器

按语言的执行流程,可以把语言划分为编译型语言和解释型语言。
编译型语言在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了。
而由解释型语言编写的程序,在每次运行时都需要通过解释器对程序进行动态解释和执行。

V8时如何执行一段JavaScript代码的

V8在执行过程中既有解释器Ignition,也有编译器TurboFan.下面分解其执行流程:

  1. 生成抽象语法树(AST)和执行上下文。
    AST的结构和代码的结构非常相似。因此可以把AST看成代码的结构化表示,编译器和解释器后续的工作都需要依赖于AST,而不是源代码。
    抽象语法树(AST)的应用:Babel、ESLint。

生成AST需要经过两个阶段:第一阶段是分词(tokenize),又称为词法分析。第二个阶段是解析(parse),又称为语法分析。
这就是 AST 的生成过程,先分词,再解析。
有了AST后,接下来V8就会生成该代码的执行上下文。

  1. 生成字节码
    第一步的AST和执行上下文搞定后,下一步就是解释器登场,根据AST生成字节码,并解释执行字节码。
    说到字节码,其实一开始的时候V8是没有字节码的,而是直接将AST转换为机器码。但是随着Chrome在手机上普及,内存占用问题暴露出来了(这是因为V8需要消耗大量的内存在存放转换后的机器码)。因此为了解决内存占用问题,引入来字节码。

    字节码就是介于AST和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。

  2. 执行代码
    通常,如果有一段第一次执行的字节码,解释器 Ignition会逐条解释执行,若发现有热点代码,那么编译器TurboFan就会把该段热点的字节码编译为高效的机器码,再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了。
    我们把这种技术称为即时编译(JIT)

JavaScript的性能优化

在过去几年中,JavaScript的性能得到了大幅提升,这得益于V8团队对解释器和编译器的不断改进和优化。
应该将优化的中心聚焦在单次脚本的执行时间和脚本的网络下载上,主要关注以下三点内容:

  • 提升单次脚本执行速度 。
  • 避免大的内联脚本 。
  • 减少JavScript文件容量。

浏览器中的页面循环系统(5讲)

15 | 消息队列和事件循环:页面上怎么“活”起来的?


浏览器页面是由消息队列和事件循环系统来驱动的。

如果把一个渲染进程比作一个国家,在线程世界里,我们将主线程比作一个总统。总统公务如此繁忙,当然需要一些得力大臣来帮助统筹调度等,我们这里要说的得力大臣就是要学习的消息队列事件循环系统.

在线程运行中处理新任务

要想在线程运行过程中,能接受并执行新的任务,就需要采用事件循环机制。

处理其它线程发送过来的任务

处理其它线程发送过来的任务通用模式是消息队列。
消息队列说一种数据结构,可以存放要执行的任务。它符合队列“先进先出”的特点。

处理其它进程发送过来的任务

渲染进程专门有一个IO线程用来接受其他进程传进来的消息。接受到消息后将这些消息组装成任务发送给主线程,后续步骤同上述。

消息队列中的任务类型

输入事件(鼠标滚动、点击、移动)、微任务、文件读写、WebSocket、JavaScript定时器等等。
消息队列还包含了很多与页面相关的事件:JavaScript执行、解析DOM、样式计算、布局计算、CSS动画等。

16 | WebAPI:setTimeout是如何实现的?


setTimeout。一个定时器,用来指定某个函数在多少毫秒之后执行。

首先,为了支持定时器实现,浏览器增加了延时队列。
其次,由于消息队列排队和一些系统级别的限制,通过setTimeout设置的回调任务并非总是可以实时地执行,这样就不能满足一些实时性要求较高的需求了。
最后,在定时器中使用过程中,还存在一些陷阱需要去留意。

17 | WebAPI:XMLHttpRequest是怎么实现的?


回调函数 VS 系统调用栈

18 | 宏任务和微任务:不是所有任务都是一个待遇


#####

19 | Promise:使用Promise,告别回调函数


Promise已经成为现代前端的“水”和“电”,很是关键,学好Promise势在必行!

Promise解决的是异步编码风格的问题。

Promise:消灭嵌套调用和多次错误处理

浏览器中的页面(8讲)

21 | Chrome开发者工具:利用网络面板做性能分析


这节内容是介绍了我们平时开发用到的Chrome浏览器的面板。下面记录一下从这篇文章加深印象的知识点:

控制器:红色圆点表示“开始/暂停抓包”。
Disabled cache:禁止从Cache中加载资源。
下载信息概要:重点关注【DOMContentLoaded】和【Load】两个事件。

  • DOMContentLoaded:这个事件发生后,说明页面已经构建好DOM,意味着构建DOM需要的HTML文件、JavaScript、CSSCSS文件都已经下载完成。
  • Load:说明浏览器已经加载了所有的资源(样式、图片等)。
    单个资源的时间线面板:
  • Queuing:导致排队有三个原因,一为资源优先级别;二是浏览器会为每个域名最多维护6个TCP连接;第三个是网络进程为数据分配磁盘空间。
  • Stalled:排队完成,进入连接状态,进入连接之前,还有一些原因会导致连接过程被推迟,体现在此。
  • Request send:网络进程准备请求数据,并将它发送至网络。这个时间通常不超过1毫秒。
  • Waiting(TTFB):数据发送出去,等待接受服务器的第一个字节的数据,通常称为“第一字节时间“。反映服务器响应时间的重要指标。

22 | DOM树 | JavaScript是如何影响DOM树构建的


什么是DOM?

从网络传给渲染引擎的HTML文件字节流是无法直接被渲染引擎理解的。
所以要将其转化为渲染引擎能够理解的内部结构。
这个结构就是DOM。
DOM三个层面的作用:

  • 从页面视角看,DOM是生成页面的基础数据结构。
  • 从JS视角来看,DOM提供了给JS脚本操作的接口,JS通过这套接口可以对DOM进行访问,从而改变文档的内容、结构、样式。
  • 从安全视角来看,DOM是一道安全防护线。一些不安全的内容在DOM解析阶段被拒之门外。
    总结:DOM是表述HTML的内部文件,它将Web页面和JavaScript脚本连接起来,并过滤一些不安全的内容。
    DOM树如何生成
    通过HTML解析器:它存在与渲染引擎内部,负责将HTML字节流转换为DOM结构。
    注意:HTML解析器不是等整个文档加载完毕之后再去解析的,而是网络进程加载了多少数据,HTML解析器便解析多少数据。
  • 第一个阶段:通过分词器将字节流转换为Token。(分为Tag Token和文本Token),Tag Token又分为StartTag和EndTag。
  • 第二三阶段同步:需要将Token解析为DOM节点,并将DOM节点添加到DOM树中。

JavaScript的下载过程会阻塞DOM的解析。Chrome对此做了优化,其中最主要的一个优化是预解析操作(预解析线程会提前下载这些文件)。

23 | 渲染流水线:CSS如何影响首次加载时的白屏时间


渲染流水线视角下的CSS

合成布局树需要CSSOM和DOM。

同HTML一样,浏览器渲染引擎是不理解CSS的,需要将其解析成渲染引擎能够理解的结构:CSSOM。
CSSOM有两个作用:第一个是提供给JavaScript操作样式表的能力,第二个是为布局树的合成提供基础的样式信息。

等DOM和CSSOM都构建好之后,渲染引擎就会构造布局树.
CSS的白屏出现的通常瓶颈主要体现在:下载CSS文件、下载JavaScript文件和执行JavaSCript。

缩短白屏时间的策略:

  • 通过内敛JavaScript、内敛CSS来移除这两种类型的文件下载,这样获取到HTML文件后就可以直接开始渲染流程。
  • 在不适合内敛的场景下,减少文件体积,比如通过Webpack等工具移除一些不必要的注释,并压缩JavaScript文件。
  • 可以将一些不需要在解析HTML阶段使用的JavaScrit,标记上async活defer。
  • 对于特大的CSS文件,可以通过媒体查询属性,将其拆分为不同用途的多CSS文件。

24 | 分层和合成机制:为什么CSS动画比JavaScript高效?


关于任意一桢的生成方式,有重排、重绘和合成三种方式。

本节主要学习渲染引擎的分层和合成机制,因为分层和合成机制代表了浏览器最为先进的合成技术。
Chrome中的合成技术,用三个词来概括的话:分层、分块、合成。

浏览器安全(5讲)

35 | CSRF攻击、陌生链接不要点


黑客经常采用的三种攻击方式是:

  • 自动发起Get请求。
  • 自动发起Post请求。
  • 引诱用户点击链接。
    和XSS攻击不同的是,CSRF攻击不需要将恶意代码注入用户的页面,仅仅是利用服务器的漏洞和用户的登录状态来展开攻击。

要发起CSRF攻击需要具备三个条件:

  • 目标站点存在服务器漏洞。
  • 用户登录过目标站点。
  • 黑客需要通过第三方站点发起攻击。
    黑客通过CSRF攻击,最关键的一点是找到服务器的漏洞,所以说对于CSRF攻击的主要防护手段是提升服务器的安全性。
liugezhou wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
Enjoy Yourself EveryDay!