浏览器——学习笔记

01 | Chrome架构:仅仅打开了1个页面,为什么有4个进程?

1.线程 VS 进程

进程:进程是CPU进行资源分配的基本单位
线程:线程是CPU调度的最小单位,它不能单独存在,是由进程启动和管理的

2.单线程 VS 多线程

单线程处理:也就是分四步按照顺序分别执行这四个任务。
多线程处理,只需分“两步走”:第一步,使用三个线程同时执行前三个任务;第二步,再执行第四个显示任务。【大大提升性能】

3.进程和线程的四大关系

1.进程中的任意一线程执行出错,都会导致整个进程的崩溃
2.线程之间共享进程中的数据
3.当一个进程关闭之后,操作系统会回收进程所占用的内存
4.进程之间的内容相互隔离。

4.单进程浏览器时代

单进程浏览器是指浏览器的所有功能模块都是运行在同一个进程里,这些模块包含了网络、插件、JavaScript 运行环境、渲染引擎和页面等。(在 2007 年之前,市面上浏览器都是单进程的)

缺点:不稳定、不流畅、不安全

5.多进程浏览器时代

最新的 Chrome 浏览器包括:1 个浏览器(Browser)主进程、1 个 GPU 进程、1 个网络(NetWork)进程、多个渲染进程和多个插件进程。

1.进程功能:

  1. 浏览器主进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
  2. GPU 进程:其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
  3. 网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
  4. 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
  5. 插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。

2.打开一个新页面采用的渲染进程策略:

  • 通常情况下,打开新的页面都会使用单独的渲染进程
  • 如果从A页面打开B页面,且A和B都属于同一个站点(协议、根域名相同)的话,那么B页面复用A页面的渲染进程;如果是其他情况,浏览器进程则会为B创建一个新的渲染进程

3.渲染进程中的五个线程

一、主线程

  • 主线程主要用于处理js代码(解析、执行)。只要消息队列不为空,就会一直从中取任务执行。由于主线程和GUI线程的互斥,所以当一个js任务执行过长时,会阻塞页面的渲染,造成页面的卡顿。

二、GUI渲染线程

  • GUI渲染线程负责解析HTML、CSS、合成CSSOM树、布局树、绘制、分层、栅格化、合成,所以重绘、重排、合成都在改线程中执行。
  • GUI线程和JS引擎线程是冲突的,当GUI线程执行时,js引擎线程会被挂起,当js引擎线程执行任务时,有需要GUI线程执行的任务,会被保存到一个队列中,等待js引擎执行完执行。

三、事件触发线程

  • 当js代码在解析时,遇到事件比如鼠标事件时,会将这些任务添加到事件触发线程中,等事件触发时,会将任务从事件触发线程中取出,放到消息队列的队尾等待执行。

四、定时器触发线程

  • 用于存放setTimeout、setInterval等任务,在解析遇到这些任务时,js引擎会将这些任务放到定时器触发线程中,并开始计数,时间到了之后,将任务放到消息队列中等待执行。

五、http请求线程

  • 用于检测XMLHttpRequest请求,当请求状态改变时,将设置的回调函数添加到消息队列中等待执行。

6.Chrome 打开一个页面至少需要启动多少进程?

在这里插入图片描述

打开 1 个页面至少需要 1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程,共 4 个;如果打开的页面有运行插件的话,还需要再加上 1 个插件进程。

和 Windows 任务管理器一样,Chrome 任务管理器也是用来展示运行中 Chrome 使用的进程信息的。(可以点击 Chrome 浏览器右上角的“选项”菜单,选择“更多工具”子菜单,点击“任务管理器”,这将打开 Chrome 的任务管理器的窗口)

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

1.IP:把数据包送达目的主机

计算机的地址就称为 IP 地址,访问任何网站实际上只是你的计算机向另外一台计算机请求信息。

数据包从A 发送到主机B:

  • 数据包 + IP头 组成新的 IP数据包(网络层),并交给底层
  • 底层通过物理网络将数据包传输给主机B
  • 在主机B拆开数据包的IP头信息(网络层),并将拆开的数据部分交给上层

IP头:IP 头是 IP 数据包开头的信息,包含 IP 版本、源 IP 地址、目标 IP 地址、生存时间等信息。

2.UDP:把数据包送达应用程序(不可靠性,但是传输速度却非常快,通常在线视频、互动游戏中使用)

UDP 中一个最重要的信息是端口号,端口号其实就是一个数字,每个想访问网络的程序都需要绑定一个端口号。通过端口号 UDP 就能把指定的数据包发送给指定的程序了。
IP 通过 IP 地址信息把数据包发送给指定的电脑,而 UDP 通过端口号把数据包分发给正确的程序。

数据包从A 发送到主机B的应用程序:
在网络层和上层之间增加了传输层:数据包 + UDP头 组成新的 UDP数据包,并交给网络层
在这里插入图片描述

UDP传输存在的问题:

  • 数据包在传输过程中容易丢失, 并不提供重发机制,所以 UDP 在发送之后也无法知道是否能达到目的地。
  • 大文件会被拆分成很多小的数据包来传输,这些小的数据包会经过不同的路由,并在不同的时间到达接收端,而 UDP 协议并不知道如何组装这些数据包,从而把这些数据包还原成完整的文件。

3.TCP:把数据完整地送达应用程序(可靠的)

1.TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。相对于 UDP,TCP 有下面两个特点:

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

2.TCP 保证了数据完整地传输,它的连接可分为三个阶段:建立连接(三次握手)、传输数据和断开连接(四次挥手)

3.TCP 为了保证数据传输的可靠性,牺牲了数据包的传输速度,因为“三次握手”和“数据包校验机制”等把传输过程中的数据包的数量提高了一倍。

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

1. 浏览器中的 HTTP 请求从发起到结束的八个阶段

构建请求、查找缓存、准备 IP 和端口、等待 TCP 队列、建立 TCP 连接、发起 HTTP 请求、服务器处理请求、服务器返回请求和断开连接。
在这里插入图片描述

2. 为什么很多站点第二次打开速度会很快?

主要原因是第一次加载页面过程中,DNS 缓存页面资源缓存这两块数据是会被浏览器缓存的。浏览器缓存直接使用本地副本来回应请求,而不会产生真实的网络请求,从而节省了时间。同时,DNS 数据也被浏览器缓存了,这又省去了 DNS 查询环节。

1.DNS 缓存: 比较简单,它主要就是在浏览器本地把对应的 IP 和域名关联起来
2.页面资源缓存

a. 如果有缓存,且未过期

  • 浏览器是通过响应头中的 Cache-Control 字段来设置是否缓存该资源。如果有,且未过期,会直接返回缓存中的资源给浏览器

b. 如果缓存过期了,浏览器则会继续发起网络请求,并且在 HTTP 请求头中带上(If-None-Match)。服务器收到请求头后,会根据 If-None-Match 的值来判断请求的资源是否有更新。

  • 如果没有更新,就返回 304 状态码,相当于服务器告诉浏览器:“这个缓存可以继续使用,这次就不重复发送数据给你了。”
  • 如果资源有更新,服务器就直接返回最新资源给浏览器。

3. 登录状态是如何保持的?

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

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

在这里插入图片描述

1,用户输入url并回车
2,浏览器进程检查url,组装协议,构成完整的url
	2.1 如果是搜索内容,会使用浏览器默认的搜索引擎,加上搜索内容合成url
	2.2 如果输入的内容符合url规则,会加上协议,合成完整的url
3,浏览器进程通过进程间通信(IPC),把url请求发送给网络进程
4,网络进程接收到url请求后,检查本地缓存是否缓存了该请求资源,如果有则将该资源返回给浏览器进程
5,如果没有,网络进程向web服务器发起http请求(网络请求),请求流程如下:
    5.1 进行DNS解析,获取服务器ip地址、端口号
    5.2 利用ip地址和服务器建立tcp连接(3次握手)
    5.3 建立连接后,浏览器构建数据包(包含请求行、请求头、请求正文,并把该域名相关的Cookie等数据附加到请求头),然后向服务器发送请求消息
    5.4 服务器接收到消息后,根据请求信息构建响应数据(包括响应行、响应头、响应正文),然后发送回网络进程
    5.5 网络进程接收到响应数据后,进行解析
6,网络进程解析响应流程,检查状态码
    6.1 如果是301/302,则需要重定向,从Location自动中读取地址,重新进行第4步
    6.2 如果是200,则继续处理请求,检查响应类型Content-Type:
        a. 如果是下载类型,则将该请求提交给浏览器的下载管理器,同时该url请求的导航流程就此结束
        b. 如果是html,则浏览器就要准备渲染页面了。
7,准备渲染进程
    7.1 浏览器进程检查当前url是否和之前打开的渲染进程同一站点(根域名、协议是否相同)如果相同,则复用原来的进程,如果不同,则开启新的渲染进程
8,传输数据、更新状态
    8.1 当浏览器进程接收到网络进程的响应头数据之后,便向渲染进程发起“提交文档”的消息;
    8.2 渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”;
    8.3 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程;		  
    8.4 浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面  
9,渲染阶段:一旦文档被提交,渲染进程便开始页面解析和子资源加载了  
    9.1 渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构。
    9.2 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。
    9.3 创建布局树,并计算元素的布局信息。
    9.4 对布局树进行分层,并生成分层树。
    9.5 为每个图层生成绘制列表,并将其提交到合成线程。
    9.6 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
    9.7合成线程发送绘制图块命令 DrawQuad 给浏览器进程。
    9.8 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。

打开一个新页面采用的渲染进程策略就是:

  • 通常情况下,打开新的页面都会使用单独的渲染进程;
  • 如果从 A 页面打开 B 页面,且 A 和 B 都属于同一站点的话,那么 B 页面复用 A 页面的渲染进程;如果是其他情况,浏览器进程则会为 B 创建一个新的渲染进程。

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

1.为什么要构建DOM树?

这是因为浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树。

2.样式计算

渲染引擎将 CSS 样式转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式

3.布局阶段

1.创建布局树

  • 遍历 DOM 树中的所有可见节点,并把这些节点加到布局树中
  • 而不可见的节点会被布局树忽略掉,如 head 标签下面的全部内容,再比如 body.p.span 这个元素,因为它的属性包含 dispaly:none,所以这个元素也没有被包进布局树。

2.计算布局树

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

1.完整的渲染流程

在这里插入图片描述

  1. 渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构。
  2. 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。
  3. 创建布局树,并计算元素的布局信息。
  4. 对布局树进行分层,并生成分层树。
  5. 为每个图层生成绘制列表,并将其提交到合成线程。
  6. 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
  7. 合成线程发送绘制图块命令 DrawQuad 给浏览器进程。
  8. 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。

2.重绘:更新了元素的几何属性

通过JS或CSS修改元素的几何位置属性,例如改变元素的宽度、高度,班浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排(重排需要更新完整的渲染流水线,开销大)

3.重绘:更新了元素的绘制属性

如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。(相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。)

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

1.变量提升

JavaScript 代码在编译阶段,把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量赋值为 undefined。可以在定义之前使用变量或者函数的原因
但实际上变量和函数的声明在代码里的位置是不会改变的,而是在编译阶段被JavaScript引擎放入内存中

2.JavaScript 代码的执行流程 (先编译,再执行)

1.编译阶段
a. 执行上下文:执行上下文是 JavaScript 执行一段代码时的运行环境。在执行上下文中存在一个变量环境的对象,该对象中保存了变量提升的内容,变量和函数会被存放到变量环境的对象
b. 可执行的代码

2. 执行阶段
JavaScript 引擎开始执行“可执行代码”,会从变量环境中去查找自定义的变量和函数

3.在编译阶段,出现相同的变量或者函数怎么办?

最终存放在变量环境中的是最后定义的那个,后定义的会覆盖掉之前的

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

1.执行上下文 (执行环境)

1.全局执行环境:整个页面的生存周期内,全局执行环境只有一个,window对象
2.函数执行环境:当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行环境,一般情况下,函数执行结束之后,创建的函数执行环境会被销毁。
3.eval:当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行环境。

2.什么是 JavaScript 的调用栈

1.在执行环境创建好后,JavaScript 引擎会将执行环境压入栈中,通常把这种用来管理执行环境的栈称为环境栈,又称调用栈。调用栈是 JavaScript 引擎追踪函数执行的一个机制
2.调用栈是一种用来管理执行环境的数据结构,符合后进先出的规则
3.可以使用 console.trace() 来输出当前的函数调用关系

3. 栈溢出

调用栈是有大小的,当入栈的执行环境超过一定数目,JavaScript 引擎就会报错,我们把这种错误叫做栈溢出。

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

1.作用域

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

2.块级作用域

1.块级作用域就是使用一对大括号包裹的一段代码,比如:函数、判断语句、循环语句,甚至单独的一个{}都可以被看作是一个块级作用域。

2.代码块内部定义的变量,在外部是访问不到的,并且等该代码块中的代码执行完成之后,代码块中定义的变量会被销毁

3.ES6 之前是不支持块级作用域的。没有了块级作用域,再把作用域内部的变量统一提升,直接导致了函数中的变量无论是在哪里声明的,在编译阶段都会被提取到执行上下文的变量环境中,所以这些变量在整个函数体内部的任何地方都是能被访问的,这也就是 JavaScript 中的变量提升。

4.块级作用域就是通过词法环境的栈结构来实现的

3.ES6 是如何解决变量提升带来的缺陷

ES6 引入了 let 和 const 关键字,使用 let 关键字声明的变量是可以被改变的,而使用 const 声明的是不可以被改变的。

1.函数内部通过 var 声明的变量,在编译阶段全都被存放到变量环境里面了。
2.通过 let 、const 声明的变量,在编译阶段会被存放到词法环境中。
3.在函数的作用域块内部,通过 let 声明的变量并没有被存放到词法环境中。

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

1.词法作用域

词法作用域:是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系

2.作用域链

通过作用域查找变量的链条称为作用域链;作用域链是通过词法作用域来确定的,而词法作用域反应了代码的结构

3.闭包

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

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

1. this 的设计缺陷以及应对方案

1. 缺陷:嵌套函数中的 this 不会从外层函数中继承
2. 解决方案:

  • 把 this 保存为一个 self 变量,再利用变量的作用域机制传递给嵌套函数。
  • 继续使用 this,但是要把嵌套函数改为箭头函数,因为箭头函数没有自己的执行上下文,所以它会继承调用函数中的 this。

2. this的指向问题

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

1. js是什么类型的语言(动态、弱类型)

JavaScript 是一种弱类型的、动态的语言。弱类型意味着你不需要告诉 JavaScript 引擎这个或那个变量是什么数据类型,JavaScript 引擎在运行代码的时候自己会计算出来。动态意味着你可以使用同一个变量保存不同类型的数据。

静态语言: 在使用之前就需要确认其变量数据类型的称为静态语言
动态语言: 在运行过程中需要检查数据类型的语言称为动态语言。
弱类型语言: 支持隐式类型转换的语言称为弱类型语言。
强类型语言: 不支持隐式类型转换的语言称为强类型语言

2. 数据类型

1.原始类型:Boolean、String、Number、Undefined、Null、BigInt、Symbol

2.引用类型:Object在这里插入图片描述

3. 栈空间和堆空间

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

1. 为什么要分开存呢?
这是因为 JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率。

所以通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间。

2. js中的复制
原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。

4.再谈闭包

产生闭包的核心:
1.预扫描内部函数
2.把内部函数引用的外部变量保存到堆中

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

1. V8 是如何实现垃圾回收的

在 V8 中会把堆分为新生代老生代两个区域,并分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收。

  • 新生代中存放的是生存时间短的对象,通常只支持 1~8M 的容量,垃圾回收比较频繁(由副垃圾回收器回收)
  • 老生代中存放的是占用空间大、存活时间长的对象(由主垃圾回收器回收)

2.垃圾回收器的工作流程

1. 标记
标记空间中活动对象(还在使用的对象)和非活动对象(可进行垃圾回收的对象)

2. 回收
回收非活动对象所占用的内存

3. 内存整理(可选的)
一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大连续内存的时候,就有可能出现内存不足的情况。所以最后一步需要整理这些内存碎片,但这步其实是可选的,因为有的垃圾回收器不会产生内存碎片,比如接下来我们要介绍的副垃圾回收器。

3.副垃圾回收器(新生区)

新生代中用 Scavenge 算法来处理。所谓 Scavenge 算法,是把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域,如下图所示:
在这里插入图片描述
新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作:

1.垃圾回收
a. 要对对象区域中的垃圾做标记
b. 标记完成之后,就进入垃圾清理阶段,副垃圾回收器会把这些存活的对象复制到空闲区域中,同时它还会把这些对象有序地排列起来,所以这个复制过程,也就相当于完成了内存整理操作,复制后空闲区域就没有内存碎片了。
c. 完成复制后,对象区域与空闲区域进行角色翻转,也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域。这样就完成了垃圾对象的回收操作,同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去。

2.新生区内存小
每次角色翻转时,复制操作需要时间成本,如果新生区空间设置得太大了,那么每次清理的时间就会过久,所以为了执行效率,一般新生区的空间会被设置得比较小

3.对象晋升策略
因为新生区的空间不大,所以很容易被存活的对象装满整个区域。为了解决这个问题,JavaScript 引擎采用了对象晋升策略,也就是经过两次垃圾回收依然还存活的对象,会被移动到老生区中。

4.主垃圾回收器(老生区)

由于老生区的对象比较大,若要在老生区中使用 Scavenge 算法进行垃圾回收,复制这些大的对象将会花费比较多的时间,从而导致回收执行效率不高,同时还会浪费一半的空间。因而,主垃圾回收器是采用标记 - 清除(Mark-Sweep)的算法进行垃圾回收的。

1. 标记
标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。

2.1 标记-清除
可以理解这个过程是清除掉红色标记数据的过程,不过对一块内存多次执行标记 - 清除算法后,会产生大量不连续的内存碎片。而碎片过多会导致大对象无法分配到足够的连续内存,于是又产生了另外一种算法——标记 - 整理(Mark-Compact)
在这里插入图片描述
2.2 标记-整理
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
在这里插入图片描述

5. 全停顿

1.全停顿
全停顿:由于 JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(会造成页面卡顿的现象)。

2.增量标记
使用增量标记算法,可以把一个完整的垃圾回收任务拆分为很多小的任务,这些小的任务执行时间比较短,可以穿插在其他的 JavaScript 任务中间执行,这样当执行上述动画效果时,就不会让用户因为垃圾回收任务而感受到页面的卡顿了。

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

1. 编译器和解释器

1. 编译器
编译型语言在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了。比如 C/C++、GO 等都是编译型语言。

在编译型语言的编译过程中,编译器首先会依次对源代码进行词法分析、语法分析,生成抽象语法树(AST),然后是优化代码,最后再生成处理器能够理解的机器码。如果编译成功,将会生成一个可执行的文件。但如果编译过程发生了语法或者其他的错误,那么编译器就会抛出异常,最后的二进制文件也不会生成成功。

2. 解释器
由解释型语言编写的程序,在每次运行时都需要通过解释器对程序进行动态解释和执行。比如 Python、JavaScript 等都属于解释型语言。

在解释型语言的解释过程中,同样解释器也会对源代码进行词法分析、语法分析,并生成抽象语法树(AST),不过它会再基于抽象语法树生成字节码,最后再根据字节码来执行程序、输出结果。

2.V8 是如何执行一段 JavaScript 代码的?

1.生成抽象语法树(AST)和执行上下文

1.什么是AST
AST:AST 的结构和代码的结构非常相似,其实也可以把 AST 看成代码的结构化的表示,编译器或者解释器后续的工作都需要依赖于 AST,而不是源代码。

2.AST的应用:Babel
Babel 的工作原理就是先将 ES6 源码转换为 AST,然后再将 ES6 语法的 AST 转换为 ES5 语法的 AST,最后利用 ES5 的 AST 生成 JavaScript 源代码。

3.AST的生成过程

  • 第一阶段是分词(tokenize),又称为词法分析,其作用是将一行行的源码拆解成一个个 token。所谓 token,指的是语法上不可能再分的、最小的单个字符或字符串。
  • 第二阶段是解析(parse),又称为语法分析,其作用是将上一步生成的 token 数据,根据语法规则转为 AST。如果源码符合语法规则,这一步就会顺利完成。但如果源码存在语法错误,这一步就会终止,并抛出一个“语法错误”。

2. 生成字节码
解释器 (Ignition) 会根据 AST 生成字节码,并解释执行字节码【字节码就是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。】

3. 执行代码
生成字节码之后,接下来就要进入执行阶段了:

  • 第一次执行的字节码,解释器 Ignition 会逐条解释执行。(解释器 Ignition作用: 生成字节码、解释执行字节码。)
  • 在 Ignition 执行字节码的过程中,如果发现有热点代码(HotSpot),比如一段代码被重复执行多次,这种就称为热点代码,那么后台的编译器 TurboFan 就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。

3.JavaScript 的性能优化

  1. 提升单次脚本的执行速度,避免 JavaScript 的长任务霸占主线程,这样可以使得页面快速响应交互;
  2. 避免大的内联脚本,因为在解析 HTML 的过程中,解析和编译也会占用主线程;
  3. 减少 JavaScript 文件的容量,因为更小的文件会提升下载速度,并且占用更低的内存。

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

1.进化过程:

  1. 如果有一些确定好的任务,可以使用一个单线程来按照顺序处理这些任务,这是第一版线程模型。
  2. 要在线程执行过程中接收并处理新的任务,就需要引入循环语句和事件系统,这是第二版线程模型。
  3. 如果要接收其他线程发送过来的任务,就需要引入消息队列,这是第三版线程模型。
  4. 如果其他进程想要发送任务给页面主线程,那么先通过IPC把任务发送给渲染进程的 IO 线程,IO 线程再把任务发送给页面主线程。
  5. 消息队列机制并不是太灵活,为了适应效率和实时性,引入了微任务(消息队列中的任务称为宏任务

2.线程模型:消息队列 + 事件循环+IPC
在这里插入图片描述

渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息,接收到消息之后,会将这些消息组装成任务发送给渲染主线程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值