0%

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

线程 VS 进程

多线程可以并行处理任务,但是线程是不能单独存在的,它是由进程来启动和管理的。 那什么又是进程呢?一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。

img 从图中看出,线程是依附于进程的,而进程中使用多线程并行能提高运算效率

总结:

  1. 进程中的任一线程执行出错,都会导致整个进程的崩溃
  2. 线程之间共享进程中的数据。

img 3. 当一个进程关闭之后,操作系统会回收进程所占用的内存
\4. 进程之间的内容相互隔离

目前浏览器的多进程架构

img

最新的chrome浏览器包括: 1个浏览器主进程,1个GPU进程,1个网络进程,多个渲染进程和多个插件进程。

分析这几个进程的功能:

  • 浏览器进程:

主要负责界面展示,用户交互,子进程管理,同时提供存储等功能。

  • 渲染进程:

核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。

  • GPU进程

主要是用来实现 3D,CSS等效果

  • 网络进程

主要负责页面的网络资源加载

  • 插件进程

主要是负责插件的进程,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响

多进程架构带来优缺点:

优点: 提高了稳定性、流畅性和安全性

缺点:更高的资源占用,更复杂的体系架构

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

在衡量 Web 页面性能的时候有一个重要的指标叫 “FP(First Paint)” ,是指 从页面加载到首次开始绘制的时长 。这个指标直接影响了用户的跳出率,更快的页面响应意味着更多的 PV、更高的参与度,以及更高的转化率。那什么影响 FP 指标呢?其中一个重要的因素是网络加载速度。

一个数据包的“旅程”

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

img 2. UDP:把数据包送达应用程序 img 增加了UDP传输层
\3. TCP:把数据完整地送达应用程序

UDP的问题:

  • 数据包在传输过程中容易丢失;
  • 大文件会被拆成很多小的数据包来传输,这些小的数据包会经过不同的路由,并在不同的时间到达接收端,而UDP协议并不知道如何组装这些数据包,从而把这些数据包还原成完整的文件。

TCP的特点:

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

img

一个完整的TCP连接的生命周期:

img

总结:

  • 互联网中的数据是通过数据包来传输的,数据包在传输过程中容易丢失或出错。
  • IP 负责把数据包送达目的主机。
  • UDP 负责把数据包送达具体应用。
  • 而 TCP 保证了数据完整地传输,它的连接可分为三个阶段:建立连接、传输数据和断开连接。

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

HTTP 是一种允许浏览器向服务器获取资源的协议,是 Web 的基础

浏览器发起 HTTP 请求的流程

  1. 构建请求
1
GET /index.html HTTP1.1
  1. 查找缓存

浏览器缓存是一种在本地保存资源副本,以供下次请求时直接使用的技术。

  1. 准备IP地址和端口

img

第一步浏览器会请求 DNS 返回域名对应的 IP。当然浏览器还提供了 DNS 数据缓存服务,如果某个域名已经解析过了,那么浏览器会缓存解析的结果,以供下次查询时直接使用,这样也会减少一次网络请求。

  1. 等待 TCP 队列

Chrome 有个机制,同一个域名同时最多只能建立 6 个 TCP 连接,如果在同一个域名下同时有 10 个请求发生,那么其中 4 个请求会进入排队等待状态,直至进行中的请求完成。

  1. 建立 TCP 连接
  2. 发送 HTTP 请求

浏览器是如何发送请求信息给服务器的?

img 首先浏览器会向服务器发送请求行,它包括了请求方法、请求 URI(Uniform Resource Identifier)和 HTTP 版本协议。

服务端处理 HTTP 请求流程

  1. 返回请求
1
curl -i  https://time.geekbang.org/

注意这里加上了-i是为了返回响应行、响应头和响应体的数据

img

i. 首先服务器会返回 响应行,包括协议和状态码。

ii. 然后发送响应头,包括

  • 服务器生成返回数据的时间
  • 返回的数据类型(JSON、HTML、流媒体等类型,),以及服务端要在客户端保存的cookie等信息

iii. 发送响应体,包含了HTML的实际内容

  1. 断开连接

通常情况下,一旦服务器向客户端返回了请求数据,它就要关闭 TCP 连接。不过如果浏览器或者服务器在其头信息中加入了: Connection:Keep-Alive 那么 TCP 连接在发送后将仍然保持打开状态,这样浏览器就可以继续通过同一个 TCP 连接发送请求。保持 TCP 连接可以省去下次请求时需要建立连接的时间,提升资源加载速度。

  1. 重定向

curl -I geekbang.org 注意这里输入的参数是-I,和-i不一样,-I表示只需要获取响应头和响应行数据,而不需要获取响应体的数据,最终返回的数据如下图所示: img 从图中知道,301告诉浏览器重定向,网址是 Location 字段的内容

问题解答:

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

如果第二次页面打开很快,主要是第一次加载页面过程中,缓存了一些耗时的数据。(DNS 缓存和页面资源缓存) 缓存处理的过程:

img

图中知:

  1. 第一次请求时,http response header,浏览器是通过响应头中的Cache-Control字段来设置是否缓存该资源。
  2. 如果缓存过期了,浏览器则会继续发起网络请求,并且在 HTTP 请求头中带上:
1
If-None-Match:"4f80f-13c-3a1xb12a"
  • 没更新 => 304
  • 更新了 => 最新的资源文件

简单说,DNS被缓存,节省查询解析时间 静态资源缓存在了本地,使用了本地副本,节省了时间

  1. 登录状态是如何保持的?
  • 用户打开登录页面,在登录框里填入用户名和密码,点击确定按钮。点击按钮会触发页面脚本生成用户登录信息,然后调用 POST 方法提交用户登录信息给服务器。
  • 服务器接收到浏览器提交的信息之后,查询后台,验证用户登录信息是否正确,如果正确的话,会生成一段表示用户身份的字符串,并把该字符串写到响应头的 Set-Cookie 字段里,如下所示,然后把响应头发送给浏览器。
1
Set-Cookie: UID=3431uad;
  • 浏览器在接收到服务器的响应头后,开始解析响应头,如果遇到响应头里含有 Set-Cookie 字段的情况,浏览器就会把这个字段信息保存到本地。比如把UID=3431uad保持到本地。
  • 当用户再次访问时,浏览器会发起 HTTP 请求,但在发起请求之前,浏览器会读取之前保存的 Cookie 数据,并把数据写进请求头里的 Cookie 字段里(如下所示),然后浏览器再将请求头发送给服务器。
  • 服务器在收到 HTTP 请求头数据之后,就会查找请求头里面的“Cookie”字段信息,当查找到包含UID=3431uad的信息时,服务器查询后台,并判断该用户是已登录状态,然后生成含有该用户信息的页面数据,并把生成的数据发送给浏览器。
  • 浏览器在接收到该含有当前用户的页面数据后,就可以正确展示用户登录的状态信息了。

img

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

附图:

img

从图中可以看到,浏览器中的 HTTP 请求从发起到结束一共经历了如下八个阶段:构建请求、查找缓存、准备 IP 和端口、等待 TCP 队列、建立 TCP 连接、发起 HTTP 请求、服务器处理请求、服务器返回请求和断开连接。

四、 导航流程: 从输入URL到页面显示,这中间发生了什么?

img

浏览器进程、渲染进程和网络进程的主要职责:

  • 浏览器进程主要负责用户交互、子进程管理和文件存储等功能
  • 网络进程是面向渲染进程和浏览器进程等提供网络下载功能。
  • 渲染进程的主要职责是把从网络下载的HTML、Javascript、css、图片等资源解析为可以显示和交互的页面。

简单小结:

  1. 用户输入URL,浏览器会根据用户输入的信息判断是搜索还是网址,如果是搜索内容,就将搜索内容+默认搜索引擎合成新的URL;如果用户输入的内容符合URL规则,浏览器就会根据URL协议,在这段内容上加上协议合成合法的URL
  2. 用户输入完内容,按下回车键,浏览器导航栏显示loading状态,但是页面还是呈现前一个页面,这是因为新页面的响应数据还没有获得
  3. 浏览器进程浏览器构建请求行信息,会通过进程间通信(IPC)将URL请求发送给网络进程

GET /index.html HTTP1.1

  1. 网络进程获取到URL,先去本地缓存中查找是否有缓存文件,如果有,拦截请求,直接200返回;否则,进入网络请求过程
  2. 网络进程请求:第一步进行DNS解析,返回域名对应的IP和端口号,如果之前DNS数据缓存服务缓存过当前域名信息,就会直接返回缓存信息;否则,发起请求获取根据域名解析出来的IP和端口号,如果没有端口号,http默认80,https默认443。如果是https请求,还需要建立TLS连接。
  3. Chrome 有个机制,同一个域名同时最多只能建立 6 个TCP 连接,如果在同一个域名下同时有 10 个请求发生,那么其中 4 个请求会进入排队等待状态,直至进行中的请求完成。如果当前请求数量少于6个,会直接建立TCP连接。
  4. TCP三次握手建立连接,http请求加上TCP头部——包括源端口号、目的程序端口号和用于校验数据完整性的序号,向下传输
  5. 网络层在数据包上加上IP头部——包括源IP地址和目的IP地址,继续向下传输到底层
  6. 底层通过物理网络传输给目的服务器主机
  7. 目的服务器主机网络层接收到数据包,解析出IP头部,识别出数据部分,将解开的数据包向上传输到传输层
  8. 目的服务器主机传输层获取到数据包,解析出TCP头部,识别端口,将解开的数据包向上传输到应用层
  9. 应用层HTTP解析请求头和请求体,如果需要重定向,HTTP直接返回HTTP响应数据的状态code301或者302,同时在请求头的Location字段中附上重定向地址,浏览器会根据code和Location进行重定向操作;如果不是重定向,首先服务器会根据 请求头中的If-None-Match 的值来判断请求的资源是否被更新,如果没有更新,就返回304状态码,相当于告诉浏览器之前的缓存还可以使用,就不返回新数据了;否则,返回新数据,200的状态码,并且如果想要浏览器缓存数据的话,就在相应头中加入字段:

Cache-Control:Max-age=2000 响应数据又顺着应用层——传输层——网络层——底层——网络层——传输层——应用层的顺序返回到网络进程

  1. 数据传输完成,TCP四次挥手断开连接。如果,浏览器或者服务器在HTTP头部加上如下信息,TCP就一直保持连接。保持TCP连接可以省下下次需要建立连接的时间,提高资源加载速度

Connection:Keep-Alive

  1. 网络进程将获取到的数据包进行解析,根据响应头中的Content-type来判断响应数据的类型,如果是字节流类型,就将该请求交给下载管理器,该导航流程结束,不再进行;如果是text/html类型,就通知浏览器进程获取到文档准备渲染
  2. 浏览器进程获取到通知,根据当前页面B是否是从页面A打开的并且和页面A是否是同一个站点(根域名和协议一样就被认为是同一个站点),如果满足上述条件,就复用之前网页的进程,否则,新创建一个单独的渲染进程
  3. 浏览器进程会发出“提交文档”的消息给渲染进程,渲染进程收到消息后,会和网络进程建立传输数据的“管道”,文档数据传输完成后,渲染进程会返回“确认提交”的消息给浏览器进程
  4. 浏览器进程收到“确认提交”的消息后,会更新浏览器的页面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新web页面,此时的web页面是空白页
  5. 渲染进程对文档进行页面解析和子资源加载,HTML 通过HTM 解析器转成DOM Tree(二叉树类似结构的东西),CSS按照CSS 规则和CSS解释器转成CSSOM TREE,两个tree结合,形成render tree(不包含HTML的具体元素和元素要画的具体位置),通过Layout可以计算出每个元素具体的宽高颜色位置,结合起来,开始绘制,最后显示在屏幕中新页面显示出来

笔记:

  1. curl -I + URL的命令是接收服务器返回的响应头的信息
1
2
curl -I http://time.geekbang.org/
复制代码
  1. 同一站点(same-site)

协议/根域名相同 例如:

1
2
3
4
https://time.geekbang.org
https://www.geekbang.org
https://www.geekbang.org:8080
复制代码

他们都属于是同一站点,因为它们的协议都是HTTPS,而且根域名也都是 geekbang.org

process-per-site-instance 策略:

如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点的话,那么新页面会复用父页面的渲染进程

五、 渲染流程:HTML、CSS和 Javascript,是如何变成页面的?

按照渲染的时间顺序,流水线分为如下几个子阶段: 构建Dom树 => 样式计算 => 布局阶段 => 分层 => 绘制 => 分块 => 栅格化 => 合成

1. 构建DOM树

img

2. 样式计算

  1. 把CSS转换为浏览器能够理解的结构
  2. 转换样式表中的属性值,使其标准化
  3. 计算出 DOM 树中每个节点的具体样式(css继承和层叠规则)

3.布局阶段

  1. 创建布局树

img 为了构建布局树,浏览器大体上完成了下面这些工作:

  • 遍历DOM树中的所有的可见节点,并把这些节点添加到布局树中;
  • 而不可见节点会被布局树忽略掉。
  1. 布局计算

4. 分层

img 渲染引擎会为哪些特定的节点创建新的图层呢?

  1. 拥有层叠上下文属性的元素会被提升为单独的一层。
  2. 需要剪裁(clip)的地方也会被创建为图层

5. 图层绘制

img

6. 栅格化(raster)操作

是指将图块转换为位图 img 从图中可以看出,渲染进程把生成图块的指令发送给 GPU,然后在 GPU 中执行生成图块的位图,并保存在 GPU 的内存中。

7. 合成和显示

图块都被光栅化后,合成线程生成一个绘制图块的命令“DrawQuad”,然后将命令提交给浏览器进程。 浏览器进程里的viz组件,用来接受合成线程发过来的DrawQuad命令,然后根据DrawQuad命令,将其页面内容绘制到内存中,最后在将内存显示在屏幕上

渲染流水线大总结

img 结合上图,一个完整的渲染进程大致可总结为如下:

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

拓展:

重排:通过 JavaScript 或者 CSS 修改元素的几何位置属性,重排需要更新完整的渲染流水线,所以开销也是最大的。

重绘:重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。

合成阶段:使用了 CSS 的 transform 来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率

减少重排重绘, 方法很多:

  1. 使用 class 操作样式,而不是频繁操作 style
  2. 避免使用 table 布局
  3. 批量dom 操作,例如 createDocumentFragment,或者使用框架,例如 React
  4. Debounce (window resize,scroll) 事件
  5. 对 dom 属性的读写要分离
  6. will-change: transform 做优化

转载自链接:https://juejin.cn/post/6896238768324509703

是什么

AJAX全称是Async JavaScript and XML,即异步的JavaScript和XML,是一种创建交互式网页应用的网页开发技术,可以在不重新加载整个网页的情况下,与服务器交换数据,并且更新部分网页

实现过程

实现Ajax异步交互需要服务器逻辑进行配合,需要完成以下步骤:

  • 创建Ajax的核心对象XMLHttpRequest对象
  • 通过XMLHttpRequest对象的open()方法与服务器建立连接
  • 构建请求所需的数据内容,并通过XMLHttpRequest对象的send()方法发送给服务器端
  • 通过XMLHttpRequest对象提供的onreadystatechange事件监听服务端你的通信状态
  • 接受并处理服务端向客户端响应的数据结果
  • 将处理结果更新到HTML页面中

创建XMLHttpReauest对象

通过XMLHttpRequest()构造函数用于初始化一个XMLHttpRequest实例对象

1
const xhr=new XMLHttpRequest()

与服务器端建立连接

通过XMLHttpRequest对象的open()方法与服务器建立连接

1
xhr.open(method,url,[async][,user][,password])

method:表示当前请求方式,常见的有GET,POST

url:服务端地址

async:布尔值,表示用于异步执行操作,默认为true

user:可选的用户名用于认证用途,默认为null

password:可选的密码用于认证用途,默认为null

给服务端发送数据

通过XMLHttpRequest对象的send()方法,将客户端页面的数据发送给服务端

1
xhr.send([body])

body:在XHR请求中要发送的数据体,如果不传递数据则为null

如果使用GET请求发送数据,需要注意:

  • 将请求数据添加到open()方法的url地址中
  • 发送请求数据的send()方法中参数设置为null

绑定onreadystatechange事件

onreadystatechange事件用于监听服务器端的通信状态,主要监听的属性为XMLHttpRequest.readyState,关于XMLHttpRequest.readyState属性有五个状态

只要readyState属性值一变化,就会触发一次readyStatechange事件,XMLHttpRequest.reponseText属性用于接收服务器端的响应结果

封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function ajax(options){
//创建一个XMLHttpRequest对象
const xhr=new XHRHttpRequest()
//初始化参数内容
options=options||{}
options.type=(options||'GET').toUpperCase
options.dataType=options.dataType||'json'
const params=options.data

//发送请求
if(options.type==='GET'){
xhr.open('GET',options.url+"?"+params,true)
xhr.send(null)
}else if(options.type==='POST'){
xhr.open('POST',options.url,true)
xhr.send(params)
}
//接收请求
xhr.onreadystatechange=function(){
if(xhr.readyState===4){
let status=xhr.status
if(status>=200&&status<300){
options.success&&options.success(xhr.responseText,xhr,responseXML)
}else{
options.fail&&options.fail(status)
}
}
}



}

使用

1
2
3
4
5
6
7
8
9
10
11
12
ajax({
type:'post',
dataType:'json',
data:{},
url:'https://xxx',
success:function(text,xml){
console.log(text)
},
fail:function(status){
console.log(status)
}
})

一、Vue 3 响应式使用

1 实现单个值的响应式

在普通 JS 代码执行中,并不会有响应式变化,比如在控制台执行下面代码:

1
2
3
4
5
let price = 10, quantity = 2;
const total = price * quantity;
console.log(`total: ${total}`); // total: 20
price = 20;
console.log(`total: ${total}`); // total: 20

从这可以看出,在修改 price 变量的值后, total 的值并没有发生改变。

那么如何修改上面代码,让 total 能够自动更新呢?我们其实可以将修改 total 值的方法保存起来,等到与 total 值相关的变量(如 pricequantity 变量的值)发生变化时,触发该方法,更新 total 即可。我们可以这么实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
let price = 10, quantity = 2, total = 0;
const dep = new Set(); // ①
const effect = () => { total = price * quantity };
const track = () => { dep.add(effect) }; // ②
const trigger = () => { dep.forEach( effect => effect() )}; // ③

track();
console.log(`total: ${total}`); // total: 0
trigger();
console.log(`total: ${total}`); // total: 20
price = 20;
trigger();
console.log(`total: ${total}`); // total: 40

上面代码通过 3 个步骤,实现对 total 数据进行响应式变化:

① 初始化一个 Set 类型的 dep 变量,用来存放需要执行的副作用( effect 函数),这边是修改 total 值的方法;

② 创建 track() 函数,用来将需要执行的副作用保存到 dep 变量中(也称收集副作用);

③ 创建 trigger() 函数,用来执行 dep 变量中的所有副作用;

在每次修改 pricequantity 后,调用 trigger() 函数执行所有副作用后, total 值将自动更新为最新值。

2 实现单个对象的响应式

通常,我们的对象具有多个属性,并且每个属性都需要自己的 dep。我们如何存储这些?比如:

1
let product = { price: 10, quantity: 2 };

从前面介绍我们知道,我们将所有副作用保存在一个 Set 集合中,而该集合不会有重复项,这里我们引入一个 Map 类型集合(即 depsMap ),其 key 为对象的属性(如: price 属性), value 为前面保存副作用的 Set 集合(如: dep 对象),大致结构如下图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let product = { price: 10, quantity: 2 }, total = 0;
const depsMap = new Map(); // ①
const effect = () => { total = product.price * product.quantity };
const track = key => { // ②
let dep = depsMap.get(key);
if(!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(effect);
}

const trigger = key => { // ③
let dep = depsMap.get(key);
if(dep) {
dep.forEach( effect => effect() );
}
};

track('price');
console.log(`total: ${total}`); // total: 0
effect();
console.log(`total: ${total}`); // total: 20
product.price = 20;
trigger('price');
console.log(`total: ${total}`); // total: 40

上面代码通过 3 个步骤,实现对 total 数据进行响应式变化:

① 初始化一个 Map 类型的 depsMap 变量,用来保存每个需要响应式变化的对象属性(key 为对象的属性, value 为前面 Set 集合);

② 创建 track() 函数,用来将需要执行的副作用保存到 depsMap 变量中对应的对象属性下(也称收集副作用);

③ 创建 trigger() 函数,用来执行 dep 变量中指定对象属性的所有副作用;

这样就实现监听对象的响应式变化,在 product 对象中的属性值发生变化, total 值也会跟着更新。

3 实现多个对象的响应式

如果我们有多个响应式数据,比如同时需要观察对象 a 和对象 b 的数据,那么又要如何跟踪每个响应变化的对象?

这里我们引入一个 WeakMap 类型的对象,将需要观察的对象作为 key ,值为前面用来保存对象属性的 Map 变量。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
let product = { price: 10, quantity: 2 }, total = 0;
const targetMap = new WeakMap(); // ① 初始化 targetMap,保存观察对象
const effect = () => { total = product.price * product.quantity };
const track = (target, key) => { // ② 收集依赖
let depsMap = targetMap.get(target);
if(!depsMap){
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if(!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(effect);
}

const trigger = (target, key) => { // ③ 执行指定对象的指定属性的所有副作用
const depsMap = targetMap.get(target);
if(!depsMap) return;
let dep = depsMap.get(key);
if(dep) {
dep.forEach( effect => effect() );
}
};

track(product, 'price');
console.log(`total: ${total}`); // total: 0
effect();
console.log(`total: ${total}`); // total: 20
product.price = 20;
trigger(product, 'price');
console.log(`total: ${total}`); // total: 40

上面代码通过 3 个步骤,实现对 total 数据进行响应式变化:

① 初始化一个 WeakMap 类型的 targetMap 变量,用来要观察每个响应式对象;

② 创建 track() 函数,用来将需要执行的副作用保存到指定对象( target )的依赖中(也称收集副作用);

③ 创建 trigger() 函数,用来执行指定对象( target )中指定属性( key )的所有副作用;

这样就实现监听对象的响应式变化,在 product 对象中的属性值发生变化, total 值也会跟着更新。

大致流程如下图:

二、Proxy 和 Reflect

在上一节内容中,介绍了如何在数据发生变化后,自动更新数据,但存在的问题是,每次需要手动通过触发 track() 函数搜集依赖,通过 trigger() 函数执行所有副作用,达到数据更新目的。

这一节将来解决这个问题,实现这两个函数自动调用。

1. 如何实现自动操作

这里我们引入 JS 对象访问器的概念,解决办法如下:

  • 在读取(GET 操作)数据时,自动执行 track() 函数自动收集依赖;
  • 在修改(SET 操作)数据时,自动执行 trigger() 函数执行所有副作用;

那么如何拦截 GET 和 SET 操作?接下来看看 Vue2 和 Vue3 是如何实现的:

需要注意的是:Vue3 使用的 ProxyReflect API 并不支持 IE。

Object.defineProperty() 函数这边就不多做介绍,可以阅读文档,下文将主要介绍 ProxyReflect API。

2. 如何使用 Reflect

通常我们有三种方法读取一个对象的属性:

  1. 使用 . 操作符:leo.name
  2. 使用 []leo['name']
  3. 使用 Reflect API: Reflect.get(leo, 'name')

这三种方式输出结果相同。

3. 如何使用 Proxy

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。语法如下

1
const p = new Proxy(target, handler)

参数如下:

  • target : 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
  • handler : 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为
1
2
3
4
5
6
7
8
9
10
let product = { price: 10, quantity: 2 };
let proxiedProduct = new Proxy(product, {
get(target, key){
console.log('正在读取的数据:',key);
return target[key];
}
})
console.log(proxiedProduct.price);
// 正在读取的数据: price
// 10

然后结合 Reflect 使用,只需修改 get 函数:

1
2
3
4
  get(target, key, receiver){
console.log('正在读取的数据:',key);
return Reflect.get(target, key, receiver);
}

输出结果还是一样。

接下来增加 set 函数,来拦截对象的修改操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let product = { price: 10, quantity: 2 };
let proxiedProduct = new Proxy(product, {
get(target, key, receiver){
console.log('正在读取的数据:',key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver){
console.log('正在修改的数据:', key, ',值为:', value);
return Reflect.set(target, key, value, receiver);
}
})
proxiedProduct.price = 20;
console.log(proxiedProduct.price);
// 正在修改的数据: price ,值为: 20
// 正在读取的数据: price
// 20

4. 修改 track 和 trigger 函数

通过上面代码,我们已经实现一个简单 reactive() 函数,用来将普通对象转换为响应式对象。但是还缺少自动执行 track() 函数和 trigger() 函数,接下来修改上面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
const targetMap = new WeakMap();
let total = 0;
const effect = () => { total = product.price * product.quantity };
const track = (target, key) => {
let depsMap = targetMap.get(target);
if(!depsMap){
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if(!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(effect);
}

const trigger = (target, key) => {
const depsMap = targetMap.get(target);
if(!depsMap) return;
let dep = depsMap.get(key);
if(dep) {
dep.forEach( effect => effect() );
}
};

const reactive = (target) => {
const handler = {
get(target, key, receiver){
console.log('正在读取的数据:',key);
const result = Reflect.get(target, key, receiver);
track(target, key); // 自动调用 track 方法收集依赖
return result;
},
set(target, key, value, receiver){
console.log('正在修改的数据:', key, ',值为:', value);
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if(oldValue != result){
trigger(target, key); // 自动调用 trigger 方法执行依赖
}
return result;
}
}

return new Proxy(target, handler);
}

let product = reactive({price: 10, quantity: 2});
effect();
console.log(total);
product.price = 20;
console.log(total);
// 正在读取的数据: price
// 正在读取的数据: quantity
// 20
// 正在修改的数据: price ,值为: 20
// 正在读取的数据: price
// 正在读取的数据: quantity
// 40

三、activeEffect 和 ref

在上一节代码中,还存在一个问题: track 函数中的依赖( effect 函数)是外部定义的,当依赖发生变化, track 函数收集依赖时都要手动修改其依赖的方法名。

比如现在的依赖为 foo 函数,就要修改 track 函数的逻辑,可能是这样:

1
2
3
4
5
const foo = () => { /**/ };
const track = (target, key) => { // ②
// ...
dep.add(foo);
}

1. 引入 activeEffect 变量

接下来引入 activeEffect 变量,来保存当前运行的 effect 函数。

1
2
3
4
5
6
let activeEffect = null;
const effect = eff => {
activeEffect = eff; // 1. 将 eff 函数赋值给 activeEffect
activeEffect(); // 2. 执行 activeEffect
activeEffect = null;// 3. 重置 activeEffect
}

然后在 track 函数中将 activeEffect 变量作为依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
const track = (target, key) => {
if (activeEffect) { // 1. 判断当前是否有 activeEffect
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(activeEffect); // 2. 添加 activeEffect 依赖
}
}
1
2
3
effect(() => {
total = product.price * product.quantity
});

这样就可以解决手动修改依赖的问题,这也是 Vue3 解决该问题的方法

2. 引入 ref 方法

熟悉 Vue3 Composition API 的朋友可能会想到 Ref,它接收一个值,并返回一个响应式可变的 Ref 对象,其值可以通过 value 属性获取。

ref:接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value。

官网的使用示例如下:

1
2
3
4
5
const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

我们有 2 种方法实现 ref 函数:

使用 rective 函数

1
const ref = intialValue => reactive({value: intialValue});

使用对象的属性访问器(计算属性)

属性方式去包括:gettersetter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const ref = raw => {
const r = {
get value(){
track(r, 'value');
return raw;
},

set value(newVal){
raw = newVal;
trigger(r, 'value');
}
}
return r;
}

四、完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
const targetMap=new WeakMap()
let activeEffect=null
function effect(eff){
activeEffect=eff
activeEffect()
activeEffect=null

}
function track(target,key){
if(activeEffect){
let depsMap=targetMap.get(target)
if(!depsMap){
targetMap.set(target,(depsMap=new Map()))
}
let dep=depsMap.get(key)
if(!dep){
depsMap.set(key,(dep=new Set()))
}
dep.add(activeEffect)
}

}
function trigger(target,key){
const depsMap=targetMap.get(target)
if(!depsMap)return
const dep=depsMap.get(key)
if(dep){
dep.forEach(effect=>{
effect()
})
}

}
function reactive(target){
const handler={
get(target,key,receiver){
let res=Reflect.get(target,key,receiver)
track(target,key)//if reactive property is Get inside then tarck the effect to rerun on SET,add the effect to the dep
return res
},
set(target,key,value,receiver){
let oldValue=target[key]
let res=Reflect.set(target,key,value,receiver)
if(res&&oldValue!=value)
{
trigger(target,key)//if this reactive property has effects to rerun on SET,trigger them

}
return res
}
}
return new Proxy(target,handler)
}
function ref(raw){
const r={
get value(){
track(r,'value')
return raw
},
set value(newval){
raw=newval
trigger(r,'value')

}
}
return r;
}
let product=reactive({prie:5,quantity:2})
let salePrice=ref(0)
let total=0
effect(()=>{
salePrice.value=product.price*0.9
})
effect(()=>{
total=salePrice.value*product.quantity
})
console.log(`Before updated quantity total=${total} salePrice=${salePrice.value}`)
product.quantity=3
console.log(`After updated quantity total=${total} salePrice=${salePrice.value}`)
product.price=10
console.log(`After updated quantity total=${total} salePrice=${salePrice.value}`)

编译阶段:

  • diff算法优化
  • 静态提升
  • 事件监听缓存
  • SSR优化

diff算法优化

vue3在diff算法中相比vue2增加了静态标记

作用是为了会发生变化的地方添加一个flag标记,下次发生变化的时候直接找该地方进行比较。已经标记静态结点的p标签在diff过程中不会比较,把性能进一步提高

关于静态类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const enum PatchFlags {
TEXT = 1,// 动态的文本节点
CLASS = 1 << 1, // 2 动态的 class
STYLE = 1 << 2, // 4 动态的 style
PROPS = 1 << 3, // 8 动态属性,不包括类名和样式
FULL_PROPS = 1 << 4, // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较
HYDRATE_EVENTS = 1 << 5, // 32 表示带有事件监听器的节点
STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序的 Fragment
KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment
UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment
NEED_PATCH = 1 << 9, // 512
DYNAMIC_SLOTS = 1 << 10, // 动态 solt
HOISTED = -1, // 特殊标志是负整数表示永远不会用作 diff
BAIL = -2 // 一个特殊的标志,指代差异算法
}

静态提升

Vue3中堆不参与更新得元素,会做静态提升,只会被创建一次,在渲染时直接复用,这样就免去了重复的创建节点,大型应用会受益于这个改动,免去重复的创建操作,优化了运行时候的内存占用

1
2
3
<span>你好</span>

<div>{{ message }}</div>

没有做静态提升之前

1
2
3
4
5
6
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode("span", null, "你好"),
_createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}

做了静态提升后

1
2
3
4
5
6
7
8
9
10
const _hoisted_1 = /*#__PURE__*/_createVNode("span", null, "你好", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock(_Fragment, null, [
_hoisted_1,
_createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}

// Check the console for the AST

静态内容_hosted_1被放置在render函数外,每次渲染的时候只要取_hosted_即可,同时_hosted_1被打上PatchFlag,静态标记为-1,特殊标记是负整数表示永远不会用于Diff

事件监听缓存

默认情况下绑定事件行为会被认为是动态绑定,所以每次都会去追踪它的变化

1
2
3
<div>
<button @click = 'onClick'>点我</button>
</div>

没开启事件监听器缓存

1
2
3
4
5
6
export const render = /*#__PURE__*/_withId(function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", { onClick: _ctx.onClick }, "点我", 8 /* PROPS */, ["onClick"])
// PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
]))
})

开启事件监听器缓存

1
2
3
4
5
6
7
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", {
onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
}, "点我")
]))
}

开启缓存后,没有了静态标记,也就是说下次diff算法的时候直接使用

SSR优化

当静态内容大到一定量级,会用createStaticVNode方法在客户端生成一个static node,这些静态node,会被直接innerHtml,就不需要创建对象,任何根据对象渲染

1
2
3
4
5
6
7
8
9
div>
<div>
<span>你好</span>
</div>
... // 很多个静态属性
<div>
<span>{{ message }}</span>
</div>
</div>

编译后

1
2
3
4
5
6
7
8
9
10
11
import { mergeProps as _mergeProps } from "vue"
import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "@vue/server-renderer"

export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
const _cssVars = { style: { color: _ctx.color }}
_push(`<div${
_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))
}><div><span>你好</span>...<div><span>你好</span><div><span>${
_ssrInterpolate(_ctx.message)
}</span></div></div>`)
}

源码体积

相比Vue2,Vue3整体体积变小,除了移除一些不常用API,最重要的是Tree shaking,任何一个函数,如ref,reactive,computed,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小

响应式系统

vue2采用的是defineProperty来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加getter,setter,实现响应式,而vue3采用proxy重写响应式系统,因为proxy可以对整个对象进行监听,所有不需要深度遍历

  • 可以监听动态属性的添加
  • 可以监听到数组索引和数组length属性
  • 可以监听删除属性

先阅读尤大的文章:

https://www.zhihu.com/question/31809713/answer/53544875

比较innerHTML和Virtual DOM的重绘性能消耗

innerHTML:render html string O(template size)+重新创建所有DOM元素O(DOM size)

Vitual DOM :render Vitual DOM +diff O(template size)+必要的DOM更新O

Vitual DOM render+diff显然比渲染html字符慢,但是它依然his纯js层面计算,比DOM操作而言,便宜了很多

diff算法:

一、是什么

diff算法是一种同层的树节点进行比较的高效算法

两个特点:

比较只会在同层级进行,不会跨层级比较

在diff比较过程中,循环会从两边向中间比较

diff 算法的在很多场景下都有应用,在 vue 中,作用于虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较

二、比较方式

diff整体策略为:深度优先,同层比较

  1. 比较只会在同层级进行, 不会跨层级比较
img
  1. 比较的过程中,循环从两边向中间收拢
img

下面举个vue通过diff算法更新的例子:

新旧VNode节点如下图所示:

第一次循环后,发现旧节点D与新节点D相同,直接复用旧节点D作为diff后的第一个真实节点,同时旧节点endIndex移动到C,新节点的 startIndex 移动到了 C

第二次循环后,同样是旧节点的末尾和新节点的开头(都是 C)相同,同理,diff 后创建了 C 的真实节点插入到第一次创建的 B 节点后面。同时旧节点的 endIndex 移动到了 B,新节点的 startIndex 移动到了 E

第三次循环中,发现E没有找到,这时候只能直接创建新的真实节点 E,插入到第二次创建的 C 节点之后。同时新节点的 startIndex 移动到了 A。旧节点的 startIndexendIndex 都保持不动

第四次循环中,发现了新旧节点的开头(都是 A)相同,于是 diff 后创建了 A 的真实节点,插入到前一次创建的 E 节点后面。同时旧节点的 startIndex 移动到了 B,新节点的 startIndex 移动到了 B

第五次循环中,情形同第四次循环一样,因此 diff 后创建了 B 真实节点 插入到前一次创建的 A 节点后面。同时旧节点的 startIndex 移动到了 C,新节点的 startIndex 移动到了 F

新节点的 startIndex 已经大于 endIndex 了,需要创建 newStartIdxnewEndIdx 之间的所有节点,也就是节点F,直接创建 F 节点对应的真实节点放到 B 节点后面

三、原理分析

当数据发生改变时,set方法会调用Dep.notify通知所有订阅者Watcher,订阅者就会调用patch给真实的DOM打补丁,更新相应的视图

Vue2中使用双端diff算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
//双端diff算法
function patchKeyedChildren(n1,n2,container) {
const oldChildren = n1.children
const newChildren = n2.children
//是个索引值
let oldStartIdx = 0
let oldEndIdx = oldChildren.length-1
let newStartIdx = 0
let newEndIdx = newChildren.length-1
//四个索引指向的vnode
let oldStartVnode = oldChildren[oldStartIdx]
let oldEndVnode = oldChildren[oldEndIdx]
let newStartVnode = newChildren[newStartIdx]
let newEndVnode = newChildren[newEndIdx]
//如果头尾部找不到复用的节点,只能拿新的一组子节点中的头部节点去旧的一组子节点中寻找
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
//如果旧结点数组中头部结点或者尾部结点为undefined,说明已经被处理过了,直接跳到下一个位置
if(!oldStartVnode) {
oldStartVnode = oldChildren[++oldStartIdx]
}else if(!oldEndVnode){
oldEndVnode = oldChildren[--oldEndIdx]
}
else if(oldStartVnode.key === newStartVnode.key){
patch(oldStartVnode,newStartVnode,container)
//更新相关索引
oldStartVnode = oldChildren[++oldStartIdx]
newStartVnode = newChildren[++newStartIdx]
}//结点在新的顺序中仍处于尾部,不需要移动,打补丁后更新索引和头尾部结点变量
else if(oldEndVnode.key === newEndVnode.key){
patch(oldEndVnode,newEndVnode,container)
oldEndVnode = oldChildren[--oldEndIdx]
newEndVnode = newChildren[--newEndIdx]
}else if(oldStartVnode.key === newEndVnode.key){
patch(oldStartVnode,newEndVnode,container)
//原本是头部结点,在新的顺序中变为了尾部结点,将旧结点对应的真实DOM移动到旧的一组子节点的尾部结点所对应的真实DOM后面,更新索引
insert(oldStartVnode.el,container,oldEndVnode.el.nextSibling)
oldStartVnode = oldChildren[++oldStartIdx]
newEndVnode = newChildren[--newEndIdx]

}else if(oldEndVnode.key === newStartVnode.key){
patch(oldEndVnode,newStartVnode,container)
//结点p-4原本是最后一个子节点,在新的顺序中它变成了第一个子节点,因此,将索引oldEndIdx指向的虚拟结点对应的真实DOM移动到索引oldStartIdx指向得到虚拟结点所对应的真实DOM前面
insert(oldEndVnode.el,container,oldStartVnode.el)
oldEndVnode = oldChildren[--oldEndIdx]
newStartVnode = newChildren[++newStartIdx]
}else {
//遍历旧的一组子节点,试图寻找与newStartVnode拥有相同key的节点
//idxInOld就是新的一组子节点的头部节点在旧的一组子节点中的索引
const idxInOld = oldChildren.findIndex(
node=>node.key===newStartVnode.key
)
//idxInOld大于0说明·找到了可以复用的结点,并且需要将其对应的真实DOM移动到头部
if(idxInOld > 0){
//idxInOld位置对应的vnode就是需要移动的结点
const vnodeToMove = oldChildren[idxInOld]
patch(vnodeToMove,newStartVnode,container)
//将vnodeToMove移动到头部结点oldStartVnode.el之前
insert(vnodeToMove.el,container,oldStartVnode.el)
//由于位置idxInOld处的结点所对应的真实DOM已经移动到了别处,因此将其设置为undefined
oldChildren[idxInOld] = undefined


}else{
//将newStartVnode作为新节点挂载到头部,使用当前头部结点oldStartVnode.el作为锚点
patch(null,newStartVnode,container,oldStartVnode.el)
}
//更新newStartIdx
newStartVnode = newChildren[++newStartIdx]
}
}
//如果oldStartIdx已经大于oldEndIdx,但是newStartIdx<=newEndIdx,说明新节点中还有元素未被挂载,需要挂载它们
if(oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx){
for(let i = newStartIdx; i<=newEndIdx ; i++){
patch(null,newChildren[i],container,oldStartVnode.el)
}
}
//如果newStartIdx已经大于newEndIdx,而oldStartIdx小于等于newEndIdx,则旧的结点中有结点需要移除
if(newEndIdx < newStartIdx && oldStartIdx<=oldEndIdx) {
for(let i=oldStartIdx;i<oldEndIdx;i++){
unmount(oldChildren[i])
}
}

}
function insert(el,parent,anchor=null){
parent.insertBefore(el,anchor)
}

Vue3快速diff算法

在实测中性能最优,它借鉴了文本Diff中的预处理思路,先处理新旧两组结点中相同的前置结点和相同的后置结点,当前前置结点和后置结点全部处理完毕后,如果无法简单地通过挂载新节点或者卸载已经不存在的结点来完成更新,则需要根据结点的索引关系,构造一个最长递增子序列,source数组,用来存储新的一组子节点在旧子节点中的位置索引,后面用它来计算最长递增子序列,最长递增子序列所指向的结点即为不需要移动的结点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
function patchKeyedChildren(n1,n2,container) {
//更新相同的前置结点
let j = 0;
let oldVnode = oldChildren[j]
let newVnode = newChildren[j]
while(oldVnode.key === newVnode.key) {
patch(oldVnode,newVnode,container)
j++
oldVnode = oldChildren[j]
newVnode = newChildren[j]
}
//更细相同的后置结点
//索引oldEnd指向旧的一组子节点的最后一个结点
let oldEnd = oldChildren.length-1
let newEnd = newChildren.length-1
oldVnode = oldChildren[oldEnd]
newVnode = newChildren[newEnd]
//while循环从后向前遍历,直到遇到拥有不同key值的结点为止
while(oldVnode.key === newVnode.key){
patch(oldVnode,newVnode,container)
oldEnd--;
newEnd--;
oldVnode=oldChildren[oldEnd]
newVnode=newChildren[newEnd]
}
//预处理完毕,如果j>oldEnd并且j<=newEnd,说明从j到newEnd之间的结点应该作为新节点插入
if(j>oldEnd && j<=newEnd) {
//锚点索引
const anchorIndex = newEnd+1
//锚点元素
const anchor = anchorIndex < newChildren.length ? newChildren[anchorIndex].el :null
//挂载新节点
while(j<newEnd){
patch(null,newChildren[j++],container,anchor)
}
}else if(j>newEnd&&j<=oldEnd){
//j到oldEnd之间的结点应该被卸载
while(j<=oldEnd){
unmount(oldChildren[j++])
}
}else{
//构造source数组,用来存储新的一组子节点在旧子节点中的位置索引,后面用它来计算最长递增子序列
const count = newEnd -j+1
const source =newArray(count)
source.fill(-1)
//新增pos和move用来判断结点是否需要移动
let move =false
let pos =0
//oldStart和newStart分别为起始索引,即j
const oldStart = j
const newStart = j
//构建索引表,存储新子节点数组中键值和索引
const keyIndex = {}
for(let i = newStart;i<=newEnd;i++){
keyIndex[newChildren[i].key]=i
}
//新增patched代表更新过的结点数量
let patched = 0;
//遍历旧的一组子节点

for(let i = oldStart;i<=oldEnd;i++){
const oldVnode = oldChildren[i]
//更新过的结点数量小于等于需要更新的结点数量,执行更新
if(patched<=count){
//通过索引表快速找到新子节点中和旧子节点有相同key的结点位置
const k= keyIndex[oldVnode.key]
if(typeof key !== "undefined"){
newVnode = newChildren[k]
//调用patch函数完成更新
patch(oldVnode,newVnode,container)
patched++;
//填充source数组
source[k-newStart]=i
if(k<pos){
move=true
}else{
pos=k;
}
}else{
unmount(oldVnode)
}

}else{//更新过的结点数量大于需要更新的结点数量,则卸载多于的结点
unmount(oldVnode)

}
}
if(move){
//如果move为真,则需要进行DOM移动操作
const seq = lis(sources)//[0,1]计算最长递增子序列的索引信息
//s指向最长递增子序列的最后一个元素
let s=seq.length-1
let i=count-1
for(i;i>=0;i--){
if(source[i]===-1){
//说明索引为i的结点为全新的结点,挂载
//该结点在新子节点中的索引
const pos=i+newStart
const newVnode = newChildren[pos]
//该结点的下一个结点的位置索引
const newPos=pos+1
//锚点
const anchor = newPos <newChildren.length?newChildren[newPos].el:null
//挂载
patch(null,newVnode,container,anchor)
}else if(i!==seq[s]){//说明节点需要移动
//该结点在新子节点中的索引
const pos=i+newStart
const newVnode = newChildren[pos]
//该结点的下一个结点的位置索引
const newPos=pos+1
//锚点
const anchor = newPos <newChildren.length?newChildren[newPos].el:null
//移动
insert(newVnode.el,container,anchor)
}else{
//当i===seq[j],说明该位置结点不需要移动,让s指向下一个位置
s--
}
}

}

}

}
function lis(arr) {
let len = arr.length,
res = [],
dp = new Array(len).fill(1);
// 存默认index
for (let i = 0; i < len; i++) {
res.push([i])
}
for (let i = len - 1; i >= 0; i--) {
let cur = arr[i],
nextIndex = undefined;
// 如果为-1 直接跳过,因为-1代表的是新节点,不需要进行排序
if (cur === -1) continue
for (let j = i + 1; j < len; j++) {
let next = arr[j]
// 满足递增条件
if (cur < next) {
let max = dp[j] + 1
// 当前长度是否比原本的长度要大
if (max > dp[i]) {
dp[i] = max
nextIndex = j
}
}
}
// 记录满足条件的值,对应在数组中的index
if (nextIndex !== undefined) res[i].push(...res[nextIndex])
}
let index = dp.reduce((prev, cur, i, arr) => cur > arr[prev] ? i : prev, dp.length - 1)
// 返回最长的递增子序列的index
return result[index]
}

时间复杂度:

1.传统diff算法中,通过循环递归对结点进行比较,同层级比较的时候就算使用动态规划(参考编辑距离),时间复杂度最坏为O(n^2),树递归比较的话,复杂度为O(n^3)

2.React的开发者结合Web界面的特点做出了两个大胆的假设,使得Diff算法复杂度直接从O(n^3)降低到O(n),假设如下:

两个相同组件产生类似的DOM结构,不同的组件产生不同的DOM结构;
对于同一层次的一组子节点,它们可以通过唯一的id进行区分。
通过这两个假设,他们提供了下面的Diff算法思路,diff算法过程中使用优先判断和就地复用策略提高效率

(1).同层比较

新的Diff算法是逐层进行比较,只比较同一层次的节点,大大降低了复杂度

(2).不同类型结点的比较

如果新旧结点类型不同,diff算法会字节删除旧的结点及其子节点并插入新的结点,这是由于前面提出的不同组件产生的DOM结构一般是不同的,所以可以不用浪费时间去比较。注意的是,删除节点意味着彻底销毁该节点,并不会将该节点去与后面的节点相比较。

(3)相同类型结点比较

diff算法更新结点的属性实现转换

(4)列表结点需要给key进行高效比较

这里说的缓存是指浏览器(客户端)在本地磁盘中对访问过的资源保存的副本文件。

浏览器缓存主要有以下几个优点:

  • 减少重复数据请求,避免通过网络再次加载资源,节省流量。
  • 降低服务器的压力,提升网站性能。
  • 加快客户端加载网页的速度, 提升用户体验。

浏览器缓存分为强缓存和协商缓存,两者有两个比较明显的区别:

  • 如果浏览器命中强缓存,则不需要给服务器发请求;而协商缓存是由服务器来决定是否使用缓存,即客户端与服务器之间一定存在一次通信。
  • 在 chrome 中强缓存(虽然没有发出真实的 http 请求)的请求状态码返回是 200 (from cache);而协商缓存如果命中走缓存的话,请求的状态码是 304 (not modified)。 不同浏览器的策略不同,在 Fire Fox中,from cache 状态码是 304.

请求流程

浏览器在第一次请求后缓存资源,再次请求时,会进行下面两个步骤:

  • 浏览器会获取该缓存资源的 header 中的信息,根据 response header 中的 expires 和 cache-control 来判断是否命中强缓存,如果命中则直接从缓存中获取资源。
  • 如果没有命中强缓存,浏览器就会发送请求到服务器,这次请求会带上 IF-Modified-Since 或者 IF-None-Match, 它们的值分别是第一次请求返回 Last-Modified或者 Etag,由服务器来对比这一对字段来判断是否命中。如果命中,则服务器返回 304 状态码,并且不会返回资源内容,浏览器会直接从缓存获取;否则服务器最终会返回资源的实际内容,并更新 header 中的相关缓存字段。

强缓存

强缓存是根据返回头中的 Expires 或者 Cache-Control 两个字段来控制的,都是表示资源的缓存有效时间。

  • Expires 是 http 1.0 的规范,值是一个GMT 格式的时间点字符串,比如 Expires:Mon,18 Oct 2066 23:59:59 GMT 。这个时间点代表资源失效的时间,如果当前的时间戳在这个时间之前,则判定命中缓存。有一个缺点是,失效时间是一个绝对时间,以服务器的时间为准,如果服务器时间与客户端时间偏差较大时,就会导致缓存混乱。而服务器的时间跟用户的实际时间是不一样是很正常的,所以 Expires 在实际使用中会带来一些麻烦。
  • Cache-Control这个字段是 http 1.1 的规范,一般常用该字段的 max-age 值来进行判断,它是一个相对时间,比如 .Cache-Control:max-age=3600 代表资源的有效期是 3600 秒。并且返回头中的 Date 表示消息发送的时间,表示当前资源在 Date ~ Date +3600s 这段时间里都是有效的。不过我在实际使用中常常遇到设置了 max-age 之后,在 max-age 时间内重新访问资源却会返回 304 not modified ,这是由于服务器的时间与本地的时间不同造成的。当然 Cache-Control 还有其他几个值可以设置, 不过相对来说都很少用了:
    • no-cache 不使用本地缓存。需要使用协商缓存。
    • no-store直接禁止浏览器缓存数据,每次请求资源都会向服务器要完整的资源, 类似于 network 中的 disabled cache。
    • public 可以被所有用户缓存,包括终端用户和 cdn 等中间件代理服务器。
    • private 只能被终端用户的浏览器缓存。

如果 Cache-Control与 Expires 同时存在的话, Cache-Control 的优先级高于 Expires 。

协商缓存

协商缓存是由服务器来确定缓存资源是否可用。 主要涉及到两对属性字段,都是成对出现的,即第一次请求的响应头带上某个字, Last-Modified 或者 Etag,则后续请求则会带上对应的请求字段 If-Modified-Since或者 If-None-Match,若响应头没有 Last-Modified 或者 Etag 字段,则请求头也不会有对应的字段。

  • Last-Modified/If-Modified-Since 二者的值都是 GMT 格式的时间字符串, Last-Modified 标记最后文件修改时间, 下一次请求时,请求头中会带上 If-Modified-Since 值就是 Last-Modified 告诉服务器我本地缓存的文件最后修改的时间,在服务器上根据文件的最后修改时间判断资源是否有变化, 如果文件没有变更则返回 304 Not Modified ,请求不会返回资源内容,浏览器直接使用本地缓存。当服务器返回 304 Not Modified 的响应时,response header 中不会再添加的 Last-Modified 去试图更新本地缓存的 Last-Modified, 因为既然资源没有变化,那么 Last-Modified 也就不会改变;如果资源有变化,就正常返回返回资源内容,新的 Last-Modified 会在 response header 返回,并在下次请求之前更新本地缓存的 Last-Modified,下次请求时,If-Modified-Since会启用更新后的 Last-Modified。
  • Etag/If-None-Match, 值都是由服务器为每一个资源生成的唯一标识串,只要资源有变化就这个值就会改变。服务器根据文件本身算出一个哈希值并通过 ETag字段返回给浏览器,接收到 If-None-Match 字段以后,服务器通过比较两者是否一致来判定文件内容是否被改变。与 Last-Modified 不一样的是,当服务器返回 304 Not Modified 的响应时,由于在服务器上ETag 重新计算过,response header中还会把这个 ETag 返回,即使这个 ETag 跟之前的没有变化。

HTTP 中并没有指定如何生成 ETag,可以由开发者自行生成,哈希是比较理想的选择。

ETag

主要是为了解决 Last-Modified 无法解决的一些问题:

  • 某些服务器不能精确得到文件的最后修改时间, 这样就无法通过最后修改时间来判断文件是否更新了。
  • 某些文件的修改非常频繁,在秒以下的时间内进行修改. Last-Modified只能精确到秒。
  • 一些文件的最后修改时间改变了,但是内容并未改变。 我们不希望客户端认为这个文件修改了

浏览器没有设置缓存策略,怎么处理?

浏览器会采用一个启发式的算法,通常会取响应头中的Date减去Last-Modified值的10%作为缓存时间。

禁用缓存的方法:

meta缓存头设置为禁止缓存

在html的head标签中加入下面内容,禁止浏览器读取缓存。

1
2
3
<meta http-equiv="Cache-Control" content="no-cache,no-store,must-revalidate"/>
<meta http-equiv="Pragma" content="no-cache"/>
<meta http-equiv="Expires" conetent="0">

Cache-Control作用于Http1.1;Pragma作用于HTTP1.0,Expires作用于proxies

must-revalidate作用:如果缓存不过期可以继续使用,但是过期了就必须去服务器验证

缺点:浏览器在资源没修改的时候也不能加载缓存,影响体验

js,css加上版本号

当请求js,css的时候,给它们最后加上版本号,浏览器发现版本高了,就不会读取低版本的缓存,版本号不需要改变文件名,只需要在调用js,css的时候再末尾加上?v=1.0,或者使用随机数生成随机版本号,但是同理,这样浏览器在资源没修改的时候也不能加载缓存

1
document.write("<script src='test.js?v=" + Math.random()+"'></script>")

或者:

1
2
3
let js=document.createElement('script')
js.src='test.js' + '?v=' + Math.random()
document.body.appendChild(js)

添加MD5

MD5相当于文件的唯一标识,同一个文件经过修改,MD5就不一样,我们可以通过MD5判断资源是否经过修改。

对于频繁变动的资源

使用Cache-Control:no-cache使浏览器每次都请求服务器,然后配合ETag或者Last-Modified来验证资源是否有效,这样的做法不能节省请求数量,但是能减少响应数据大小。

用户行为对浏览器缓存的影响

普通F5刷新:强缓存没有,有协商缓存,memory cache优先使用,其次是disk cache

强制刷新(Ctrl+F5):浏览器不使用缓存,因此发送的请求头部均带有Cache-control:no-cache(为了兼容,还带有Pragma:no-cache),服务器直接返回200和最新内容。

标记清理

  • 变量进入上下文,会加上标记,证明其存在于该上下文
  • 将所有在上下文中的变量以及上下文中被访问引用的变量标记去掉,表明这些变量活跃有用
  • 在此之后再被加上标记的变量标记为准备删除的变量,原因是任何在上下文中的变量都访问不到它们
  • 执行内存清理,销毁标记的所有非活跃值并回收之前被占用的内存

引用计数

引用计数策略相对而言不常用,弊端较多,其思路对每个值记录它被引用的次数,通过最后对次数的判断(引用数为0)来决定是否保留,具体规则:

  • 声明一个变量,赋予它一个引用值,计数+1
  • 同一值被赋予另外一个变量,引用+1
  • 保存对该值引用的变量被其他值覆盖,引用+1
  • 引用为0,回收内存

局限:容易造成循环引用

1
2
3
4
5
6
function problem(){
let a=new Object()
let b=new Object()
a.c=b;
b.d=a;
}

a和b通过各自的属性相互引用,意味着它们的引用数都为2,在函数结束后,这两个对象不再作用域内,在引用计数策略下,a和b在函数结束后还会存在,因为它们的引用数永远捕获变为0,如果函数被多次调用就会导致大量内存永远不会被释放。

浏览器的事件循环机制:

是什么

JavaScript 在设计之初便是单线程,即指程序运行时,只有一个线程存在,同一时间只能做一件事

为什么要这么设计,跟JavaScript的应用场景有关

JavaScript 初期作为一门浏览器脚本语言,通常用于操作 DOM ,如果是多线程,一个线程进行了删除 DOM ,另一个添加 DOM,此时浏览器该如何处理?

为了解决单线程运行阻塞问题,JavaScript用到了计算机系统的一种运行机制,这种机制就叫做事件循环(Event Loop)

事件循环(Event Loop)

JavaScript中,所有的任务都可以分为

  • 同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行
  • 异步任务:异步执行的任务,比如ajax网络请求,setTimeout 定时函数等

从上面我们可以看到,同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。上述过程的不断重复就是事件循环

宏任务与微任务

如果将任务划分为同步任务和异步任务并不是那么的准确,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
console.log(1)

setTimeout(()=>{
console.log(2)
}, 0)

new Promise((resolve, reject)=>{
console.log('new Promise')
resolve()
}).then(()=>{
console.log('then')
})

console.log(3)

如果按照上面流程图来分析代码,我们会得到下面的执行步骤:

  • console.log(1) ,同步任务,主线程中执行
  • setTimeout() ,异步任务,放到 Event Table,0 毫秒后console.log(2) 回调推入 Event Queue
  • new Promise ,同步任务,主线程直接执行
  • .then ,异步任务,放到 Event Table
  • console.log(3),同步任务,主线程执行

所以按照分析,它的结果应该是 1 => 'new Promise' => 3 => 2 => 'then'

但是实际结果是:1=>'new Promise'=> 3 => 'then' => 2

出现分歧的原因在于异步任务执行顺序,事件队列其实是一个“先进先出”的数据结构,排在前面的事件会优先被主线程读取

例子中 setTimeout回调事件是先进入队列中的,按理说应该先于 .then 中的执行,但是结果却偏偏相反

原因在于异步任务还可以细分为微任务与宏任务

微任务

一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前

常见的微任务有:

  • Promise.then
  • MutaionObserver
  • Object.observe(已废弃;Proxy 对象替代)
  • process.nextTick(Node.js)

宏任务

宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合

常见的宏任务有:

  • script (可以理解为外层同步代码)
  • setTimeout/setInterval
  • UI rendering/UI事件
  • postMessage、MessageChannel
  • setImmediate、I/O(Node.js)

按照这个流程,它的执行机制是:

  • 执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中
  • 当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完

回到上面的题目

1
2
3
4
5
6
7
8
9
10
11
console.log(1)
setTimeout(()=>{
console.log(2)
}, 0)
new Promise((resolve, reject)=>{
console.log('new Promise')
resolve()
}).then(()=>{
console.log('then')
})
console.log(3)

流程如下

1
2
3
4
5
6
7
// 遇到 console.log(1) ,直接打印 1
// 遇到定时器,属于新的宏任务,留着后面执行
// 遇到 new Promise,这个是直接执行的,打印 'new Promise'
// .then 属于微任务,放入微任务队列,后面再执行
// 遇到 console.log(3) 直接打印 3
// 好了本轮宏任务执行完毕,现在去微任务列表查看是否有微任务,发现 .then 的回调,执行它,打印 'then'
// 当一次宏任务执行完,再去执行新的宏任务,这里就剩一个定时器的宏任务了,执行它,打印 2

async与await

async 是异步的意思,await 则可以理解为等待

放到一起可以理解async就是用来声明一个异步方法,而 await 是用来等待异步方法执行

async

async函数返回一个promise对象,下面两种方法是等效的

1
2
3
4
5
6
7
8
function f() {
return Promise.resolve('TEST');
}

// asyncF is equivalent to f!
async function asyncF() {
return 'TEST';
}

await

正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值

1
2
3
4
5
6
async function f(){
// 等同于
// return 123
return await 123
}
f().then(v => console.log(v)) // 123

不管await后面跟着的是什么,await都会阻塞后面的代码

1
2
3
4
5
6
7
8
9
10
11
12
async function fn1 (){
console.log(1)
await fn2()
console.log(2) // 阻塞
}

async function fn2 (){
console.log('fn2')
}

fn1()
console.log(3)

上面的例子中,await 会阻塞下面的代码(即加入微任务队列),先执行 async 外面的同步代码,同步代码执行完,再回到 async 函数中,再执行之前阻塞的代码

所以上述输出结果为:1fn232

Node中的事件循环机制

事件循环分为6个阶段:

timers:执行timer的回调,即setTimeout,setInterval里面的回调函数

I/O事件回调阶段:执行延迟到下一个循环迭代的I/O阶段,即上一轮循环中未被执行的一些I/O回调

idle,prepare(闲置阶段):仅内部使用

poll(轮询阶段):检查新的I/O事件,执行与I/O相关的回调,(几乎所有情况下,除了关闭的回调函数,那些由计时器和setImmediate()调度的之外),其余情况node将在适当的时候在此阻塞

check(检查阶段):setImmediate()回调函数在这里执行

close callback(关闭事件回调阶段):一些关闭的回调函数,如socket.on(‘close’,…)

除了上述6个阶段,还存在process.nextTick,其不属于事件循环的任何一个阶段,它属于该阶段与下阶段之间的过渡,即本阶段执行结束,进入下一个阶段前要执行的回调,类似插队

在Node中,同样存在宏任务和微任务,与浏览器的事件循环相似

微任务:

  • next tick queue:process.nextTick
  • other queue:Promise的then回调,queueMicrotask

宏任务:

  • timer queue:setTimeout,setInterval
  • poll queue:IO事件
  • check queue:setImmediate
  • close queue:close事件

执行顺序:

  • next tick microtask queue
  • other microtask queue
  • timer queue
  • poll queue
  • check queue
  • close queue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}

async function async2() {
console.log('async2')
}

console.log('script start')

setTimeout(function () {
console.log('setTimeout0')
}, 0)

setTimeout(function () {
console.log('setTimeout2')
}, 300)

setImmediate(() => console.log('setImmediate'));

process.nextTick(() => console.log('nextTick1'));

async1();

process.nextTick(() => console.log('nextTick2'));

new Promise(function (resolve) {
console.log('promise1')
resolve();
console.log('promise2')
}).then(function () {
console.log('promise3')
})

console.log('script end')

分析过程:

  • 先找到同步任务,输出script start
  • 遇到第一个 setTimeout,将里面的回调函数放到 timer 队列中
  • 遇到第二个 setTimeout,300ms后将里面的回调函数放到 timer 队列中
  • 遇到第一个setImmediate,将里面的回调函数放到 check 队列中
  • 遇到第一个 nextTick,将其里面的回调函数放到本轮同步任务执行完毕后执行
  • 执行 async1函数,输出 async1 start
  • 执行 async2 函数,输出 async2,async2 后面的输出 async1 end进入微任务,等待下一轮的事件循环
  • 遇到第二个,将其里面的回调函数放到本轮同步任务执行完毕后执行
  • 遇到 new Promise,执行里面的立即执行函数,输出 promise1、promise2
  • then里面的回调函数进入微任务队列
  • 遇到同步任务,输出 script end
  • 执行下一轮回到函数,先依次输出 nextTick 的函数,分别是 nextTick1、nextTick2
  • 然后执行微任务队列,依次输出 async1 end、promise3
  • 执行timer 队列,依次输出 setTimeout0
  • 接着执行 check 队列,依次输出 setImmediate
  • 300ms后,timer 队列存在任务,执行输出 setTimeout2

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
script start
async1 start
async2
promise1
promise2
script end
nextTick1
nextTick2
async1 end
promise3
setTimeout0
setImmediate
setTimeout2

最后有一道是关于setTimeoutsetImmediate的输出顺序

1
2
3
4
5
6
7
setTimeout(() => {
console.log("setTimeout");
}, 0);

setImmediate(() => {
console.log("setImmediate");
});

输出情况:

1
2
3
4
5
6
7
情况一:
setTimeout
setImmediate

情况二:
setImmediate
setTimeout

分析下流程:

  • 外层同步代码一次性全部执行完,遇到异步API就塞到对应的阶段
  • 遇到setTimeout,虽然设置的是0毫秒触发,但实际上会被强制改成1ms,时间到了然后塞入times阶段
  • 遇到setImmediate塞入check阶段
  • 同步代码执行完毕,进入Event Loop
  • 先进入times阶段,检查当前时间过去了1毫秒没有,如果过了1毫秒,满足setTimeout条件,执行回调,如果没过1毫秒,跳过
  • 跳过空的阶段,进入check阶段,执行setImmediate回调

这里的关键在于这1ms,如果同步代码执行时间较长,进入Event Loop的时候1毫秒已经过了,setTimeout先执行,如果1毫秒还没到,就先执行了setImmediate

箭头函数和普通函数区别

  1. 语法更加简洁清晰

  2. 箭头函数不会创建自己的this

所以它不会有自己的this,它只会从自己的作用域链的上一层继承this,作用域链包括全局作用域,块作用域和函数作用域

  1. 箭头函数继承而来的this指向永远不变

对象obj的方法b时用箭头函数定义的,这个函数中的this永远指向它定义时所处的全局环境中的this,即使这个函数是作为对象obj的方法调用,它依然指向window对象

  1. call()/apply()/bind()无法改变箭头函数中this得指向

call()/apply()/bind()方法可以用来动态修改this的指向,但由于箭头函数的this定义时就已经确定且永远不会改变,所以使用这些犯法永远也改变不了箭头函数this的指向,但是代码也不会报错

  1. 箭头函数不能用作构造函数调用

构造函数的new原理:

箭头函数是ES6中的提出来的,它没有prototype,也没有自己的this指向,更不可以使用arguments参数,所以不能New一个箭头函数。

new操作符的实现步骤如下:

1、创建一个空的简单JavaScript对象(即{});

2、为步骤1新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;

3、将步骤1新创建的对象作为this的上下文 ;

4、如果该函数没有返回对象,则返回this。

所以,上面的第二、三步,箭头函数都是没有办法执行的。

  1. 箭头函数不能用作Generator函数,不能使用yield关键字