实习
17期-CSL
工作地点:成都
工作性质:实习
部门:待定
笔试题
- 设置元素浮动后,该元素的display值是多少()
- A.block
- B.inline
- C.inline-block
- 2.对权重排序正确的是()
a) .list .box p
b) #list .box div span
c) .list span
d) #list #box
e) p:last-child
f) style
- A.f>d>b>a>c>e
- B.f>d>b>a>e>c
- C.f>d>b>a>c=e
- 3.下列说法不正确的是()
- B.visibility:hidden;所占据的空间位置仍然存在,仅为视觉上的完全透明
- C.display:none;不为被隐藏的对象保留其物理空间;
- D.visibility:hidden;产生reflow和repaint(回流与重绘);
- 4.下面哪组全时置换元素()
- A.img,input,select
- B.span.select.textarea
- C.select.p.form
- 5.新窗口打开网页,用到以下哪个值()
- A._self
- B._blank
- C._top
- 6.下面程序输出结果()
console.log('A')setTimeout(()=>{console.log('B')},0);new Promise((r)=>{console.log('C')r()}).then(()=>{console.log('D')})console.log('E')
- A.A C D B E
- B.A C B D E
- C.A C E D B
- 7.请写出下面程序的输出()
var a = {x:1,fun:function(){this,x = 10}}var b = a.fun;b():console.log(a.x);
- A.1
- B.10
- C.undefined
- 8.下边的代码输出的结果是()
var name = 'World!';(function(){if(typeof name ==='undefined'){var name = 'Jack';console.log('Goodbye'+name);}else{console.log('Hello'+name);}})();
- A.Goodbye Jack
- B.Hello Jack
- C.Hello World
- 9.以下运行结果()
for(var i = 0;i<10;i++){setTimeout(function(){console.log(i);},1000);}
- A.0—9
- B.10个10
- C.10个9
- 10.下列关于闭包描述不正确的是()
- A.(function(){})()理论上是一个闭包
- B.闭包不耗内存,可以随意使用
- C.比包内变量执行后不会被清除
- 11.HHTP协议工作在()
- A、应用层
- B、传输层
- C、网络层
- 12.数据链路层请简述一下cookies,sessionStorage和localStorage的区别?
- 13.map和forEach的区别?
- 14.请列出至少三种垂直居中的方式?
- 15.vue实例的生命周期有哪些?
- 16.简述一下协商缓存和强缓存的区别?
- 17.算法:统计一个字符串中出现最多的字符?
- 18.算法:求一个数组中最大值和最小值的差?
- 19.算法:给n个元素的数组,元素取值只有0,1,2三种可能,请为这个数组进行排序?
- keep-alive生命周期,和怎么在路由里面单独缓存一个组件
- 缓存文件标识符Etag是怎么生成的
- promise A+规范也可以看看,
浏览器缓存的那些事儿
什么是缓存呢?
- 当我们第一次访问网站的时候,比如 juejin.cn,电脑会把网站上的图片和数据下载到电脑上,当我们再次访问该网站的时候,网站就会从电脑中直接加载出来,这就是缓存。
缓存有哪些好处?
- 缓解服务器压力,不用每次都去请求某些数据了。
2.提升性能,打开本地资源肯定会比请求服务器来的快。
3.减少带宽消耗,当我们使用缓存时,只会产生很小的网络消耗,至于为什么打开本地资源也会产生网络消耗,下面会有说明。
Web缓存种类: 数据库缓存,CDN缓存,代理服务器缓存,浏览器缓存。

一、CDN是什么?
http缓存是浏览器端缓存,cdn是服务器端缓存。
举个例子来说明cdn的作用:cdn就是代理。厂家给商家发货,你从商家买货,商家就是cdn,很方便。
二、CDN怎么缓存?
和Http类似,客户端请求数据时,先从本地缓存查找,如果被请求数据没有过期,拿过来用,如果过期,就向CDN边缘节点发起请求。CDN便会检测被请求的数据是否过期,如果没有过期,就返回数据给客户端,如果过期,CDN再向源站发送请求获取新数据。和买家买货,卖家没货,卖家再进货一个道理^^。
CDN边缘节点缓存机制,一般都遵守http标准协议,通过http响应头中的Cache-Control和max-age的字段来设置CDN边缘节点的数据缓存时间。
所谓浏览器缓存其实就是指在本地使用的计算机中开辟一个内存区,同时也开辟一个硬盘区作为数据传输的缓冲区,然后用这个缓冲区来暂时保存用户以前访问过的信息。
浏览器缓存过程: 强缓存,协商缓存。
浏览器缓存位置一般分为四类: Service Worker—>Memory Cache—>Disk Cache—>Push Cache。
强缓存
强缓存是当我们访问URL的时候,不会向服务器发送请求,直接从缓存中读取资源,但是会返回200的状态码。
如何设置强缓存?
我们第一次进入页面,请求服务器,然后服务器进行应答,浏览器会根据response Header来判断是否对资源进行缓存,如果响应头中expires、pragma或者cache-control字段,代表这是强缓存,浏览器就会把资源缓存在memory cache 或 disk cache中。
第二次请求时,浏览器判断请求参数,如果符合强缓存条件就直接返回状态码200,从本地缓存中拿数据。否则把响应参数存在request header请求头中,看是否符合协商缓存,符合则返回状态码304,不符合则服务器会返回全新资源。

expires
是HTTP1.0控制网页缓存的字段,值为一个时间戳,准确来讲是格林尼治时间,服务器返回该请求结果缓存的到期时间,意思是,再次发送请求时,如果未超过过期时间,直接使用该缓存,如果过期了则重新请求。
有个缺点,就是它判断是否过期是用本地时间来判断的,本地时间是可以自己修改的。
Cache-Control
是HTTP1.1中控制网页缓存的字段,当Cache-Control都存在时,Cache-Control优先级更高,主要取值为:
public:资源客户端和服务器都可以缓存。
privite:资源只有客户端可以缓存。
no-cache:客户端缓存资源,但是是否缓存需要经过协商缓存来验证。
no-store:不使用缓存。
max-age:缓存保质期。

Cache-Control使用了max-age相对时间,解决了expires的问题。
pragma
- 这个是HTTP1.0中禁用网页缓存的字段,其取值为no-cache,和Cache-Control的no-cache效果一样。

- 这个是HTTP1.0中禁用网页缓存的字段,其取值为no-cache,和Cache-Control的no-cache效果一样。
缓存位置
上面我们说,强缓存我们会把资源房放到memory cache 和 disk cache中,那什么资源放在memory cache,什么资源放在disk cache中?

存存储图像和网页等资源主要缓存在disk cache,操作系统缓存文件等资源大部分都会缓存在memory cache中。具体操作浏览器自动分配,看谁的资源利用率不高就分给谁。
可以看到memory cache请求时间都是0ms,这个是不是太神奇了,这方面我来梳理下。
查找浏览器缓存时会按顺序查找: Service Worker—>Memory Cache—>Disk Cache—>Push Cache。
- Service Worker
- 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
- Memory Cache
- 内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
- Disk Cache
存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。
在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自 Disk Cache。
memory cache 要比 disk cache 快的多。举个例子:从远程 web 服务器直接提取访问文件可能需要500毫秒(半秒),那么磁盘访问可能需要10-20毫秒,而内存访问只需要100纳秒,更高级的还有 L1缓存访问(最快和最小的 CPU 缓存)只需要0.5纳秒。

很神奇的,我们又看到了一个prefetch cache,这个又是什么呢?
prefetch cache(预取缓存)
link标签上带了prefetch,再次加载会出现。
prefetch是预加载的一种方式,被标记为prefetch的资源,将会被浏览器在空闲时间加载。
- Push Cache
- Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。
- CPU、内存、硬盘
这里提到了硬盘,内存,可能有些小伙伴对硬盘,内存没什么直观的概念。
CPU、内存、硬盘都是计算机的主要组成部分。
CPU:中央处理单元(CntralPocessingUit)的缩写,也叫处理器,是计算机的运算核心和控制核心。电脑靠CPU来运算、控制。让电脑的各个部件顺利工作,起到协调和控制作用。
硬盘:存储资料和软件等数据的设备,有容量大,断电数据不丢失的特点。
内存:负责硬盘等硬件上的数据与CPU之间数据交换处理。特点是体积小,速度快,有电可存,无电清空,即电脑在开机状态时内存中可存储数据,关机后将自动清空其中的所有数据。
协商缓存
协商缓存就是强缓存失效后,浏览器携带缓存标识向服务器发送请求,由服务器根据缓存标识来决定是否使用缓存的过程。
主要有以下两种情况:
协商缓存生效,返回304

协商缓存失效,返回200和请求结果

如何设置协商缓存?
Last-Modified / If-Modified-Since
Last-Modified是服务器响应请求时,返回该资源文件在服务器最后被修改的时间。

If-Modified-Since则是客户端再次发起该请求时,携带上次请求返回的Last-Modified值,通过此字段值告诉服务器该资源上次请求返回的最后被修改时间。服务器收到该请求,发现请求头含有If-Modified-Since字段,则会根据If-Modified-Since的字段值与该资源在服务器的最后被修改时间做对比,若服务器的资源最后被修改时间大于If-Modified-Since的字段值,则重新返回资源,状态码为200;否则则返回304,代表资源无更新,可继续使用缓存文件。

Etag / If-None-Match
- Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成)。

- Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成)。
If-None-Match是客户端再次发起该请求时,携带上次请求返回的唯一标识Etag值,通过此字段值告诉服务器该资源上次请求返回的唯一标识值。服务器收到该请求后,发现该请求头中含有If-None-Match,则会根据If-None-Match的字段值与该资源在服务器的Etag值做对比,一致则返回304,代表资源无更新,继续使用缓存文件;不一致则重新返回资源文件,状态码为200。

Etag / If-None-Match优先级高于Last-Modified / If-Modified-Since,同时存在则只有Etag / If-None-Match生效。
缓存方案
目前的项目大多使用这种缓存方案的:
HTML: 协商缓存;
css、js、图片:强缓存,文件名带上hash。
强缓存与协商缓存的区别
- 强缓存不发请求到服务器,所以有时候资源更新了浏览器还不知道,但是协商缓存会发请求到服务器,所以资源是否更新,服务器肯定知道。
- 大部分web服务器都默认开启协商缓存。
刷新对于强缓存和协商缓存的影响
- 当ctrl+f5强制刷新网页时,直接从服务器加载,跳过强缓存和协商缓存。
- 当f5刷新网页时,跳过强缓存,但是会检查协商缓存。
- 浏览器地址栏中写入URL,回车 浏览器发现缓存中有这个文件了,不用继续请求了,直接去缓存拿。(最快)
计算机网络基础
注意:每道题前面出现的 (xx) 数字代表这道题出现的频次,此 计算机网络 基础是基于 30+ 篇前端面经整理出的问题和对应的回答、参考链接等。文章内容为拿到 Offer 的本人整理。
问:HTTP 缓存
HTTP 缓存又分为强缓存和协商缓存:
首先通过 Cache-Control 验证强缓存是否可用,如果强缓存可用,那么直接读取缓存
如果不可以,那么进入协商缓存阶段,发起 HTTP 请求,服务器通过请求头中是否带上 If-Modified-Since 和 If-None-Match 这些条件请求字段检查资源是否更新:
若资源更新,那么返回资源和 200 状态码
如果资源未更新,那么告诉浏览器直接使用缓存获取资源
问:HTTP 常用的状态码及使用场景?
1xx:表示目前是协议的中间状态,还需要后续请求
2xx:表示请求成功
3xx:表示重定向状态,需要重新请求
4xx:表示请求报文错误
5xx:服务器端错误
常用状态码:
101 切换请求协议,从 HTTP 切换到 WebSocket
200 请求成功,有响应体
301 永久重定向:会缓存
302 临时重定向:不会缓存
304 协商缓存命中
403 服务器禁止访问
404 资源未找到
400 请求错误
500 服务器端错误
503 服务器繁忙
你知道 302 状态码是什么嘛?你平时浏览网页的过程中遇到过哪些 302 的场景?
而 302 表示临时重定向,这个资源只是暂时不能被访问了,但是之后过一段时间还是可以继续访问,一般是访问某个网站的资源需要权限时,会需要用户去登录,跳转到登录页面之后登录之后,还可以继续访问。
301 类似,都会跳转到一个新的网站,但是 301 代表访问的地址的资源被永久移除了,以后都不应该访问这个地址,搜索引擎抓取的时候也会用新的地址替换这个老的。可以在返回的响应的 location 首部去获取到返回的地址。301 的场景如下:
比如从 http://baidu.com,跳转到 https://baidu.com
域名换了
问:HTTP 常用的请求方式,区别和用途?
http/1.1 规定如下请求方法:
GET:通用获取数据
HEAD:获取资源的元信息
POST:提交数据
PUT:修改数据
DELETE:删除数据
CONNECT:建立连接隧道,用于代理服务器
OPTIONS:列出可对资源实行的请求方法,常用于跨域
TRACE:追踪请求-响应的传输路径
问:你对计算机网络的认识怎么样
- 应用层、表示层、会话层、传输层、网络层、数据链路层、物理层
问:HTTPS 是什么?具体流程
HTTPS 是在 HTTP 和 TCP 之间建立了一个安全层,HTTP 与 TCP 通信的时候,必须先进过一个安全层,对数据包进行加密,然后将加密后的数据包传送给 TCP,相应的 TCP 必须将数据包解密,才能传给上面的 HTTP。
浏览器传输一个 client_random 和加密方法列表,服务器收到后,传给浏览器一个 server_random、加密方法列表和数字证书(包含了公钥),然后浏览器对数字证书进行合法验证,如果验证通过,则生成一个 pre_random,然后用公钥加密传给服务器,服务器用 client_random、server_random 和 pre_random ,使用公钥加密生成 secret,然后之后的传输使用这个 secret 作为秘钥来进行数据的加解密。
问:三次握手和四次挥手
为什么要进行三次握手:为了确认对方的发送和接收能力。
三次握手
三次握手主要流程:
一开始双方处于 CLOSED 状态,然后服务端开始监听某个端口进入 LISTEN 状态
然后客户端主动发起连接,发送 SYN,然后自己变为 SYN-SENT,seq = x
服务端收到之后,返回 SYN seq = y 和 ACK ack = x + 1(对于客户端发来的 SYN),自己变成 SYN-REVD
之后客户端再次发送 ACK seq = x + 1, ack = y + 1给服务端,自己变成 EASTABLISHED 状态,服务端收到 ACK,也进入 ESTABLISHED
SYN 需要对端确认,所以 ACK 的序列化要加一,凡是需要对端确认的,一点要消耗 TCP 报文的序列化
为什么不是两次?
无法确认客户端的接收能力。
如果首先客户端发送了 SYN 报文,但是滞留在网络中,TCP 以为丢包了,然后重传,两次握手建立了连接。
等到客户端关闭连接了。但是之后这个包如果到达了服务端,那么服务端接收到了,然后发送相应的数据表,就建立了链接,但是此时客户端已经关闭连接了,所以带来了链接资源的浪费。
为什么不是四次?
四次以上都可以,只不过 三次就够了
四次挥手
一开始都处于 ESTABLISH 状态,然后客户端发送 FIN 报文,带上 seq = p,状态变为 FIN-WAIT-1
服务端收到之后,发送 ACK 确认,ack = p + 1,然后进入 CLOSE-WAIT 状态
客户端收到之后进入 FIN-WAIT-2 状态
过了一会等数据处理完,再次发送 FIN、ACK,seq = q,ack = p + 1,进入 LAST-ACK 阶段
客户端收到 FIN 之后,客户端收到之后进入 TIME_WAIT(等待 2MSL),然后发送 ACK 给服务端 ack = 1 + 1
服务端收到之后进入 CLOSED 状态
客户端这个时候还需要等待两次 MSL 之后,如果没有收到服务端的重发请求,就表明 ACK 成功到达,挥手结束,客户端变为 CLOSED 状态,否则进行 ACK 重发
为什么需要等待 2MSL(Maximum Segement Lifetime):
因为如果不等待的话,如果服务端还有很多数据包要给客户端发,且此时客户端端口被新应用占据,那么就会接收到无用的数据包,造成数据包混乱,所以说最保险的方法就是等服务器发来的数据包都死翘翘了再启动新应用。
1个 MSL 保证四次挥手中主动关闭方最后的 ACK 报文能最终到达对端
1个 MSL 保证对端没有收到 ACK 那么进行重传的 FIN 报文能够到达
为什么是四次而不是三次?
**如果是三次的话,那么服务端的 ACK 和 FIN 合成一个挥手,那么长时间的延迟可能让 TCP 一位 FIN 没有达到服务器端,然后让客户的不断的重发 FIN
参考资料
问:在交互过程中如果数据传送完了,还不想断开连接怎么办,怎么维持?
- 在 HTTP 中响应体的 Connection 字段指定为 keep-alive
你对 TCP 滑动窗口有了解嘛?
在 TCP 链接中,对于发送端和接收端而言,TCP 需要把发送的数据放到发送缓存区, 将接收的数据放到接收缓存区。而经常会存在发送端发送过多,而接收端无法消化的情况,所以就需要流量控制,就是在通过接收缓存区的大小,控制发送端的发送。如果对方的接收缓存区满了,就不能再继续发送了。而这种流量控制的过程就需要在发送端维护一个发送窗口,在接收端维持一个接收窗口。
TCP 滑动窗口分为两种: 发送窗口和接收窗口。
参考资料
问:WebSocket与Ajax的区别
本质不同
Ajax 即异步 JavaScript 和 XML,是一种创建交互式网页的应用的网页开发技术
websocket 是 HTML5 的一种新协议,实现了浏览器和服务器的实时通信
生命周期不同:
websocket 是长连接,会话一直保持
ajax 发送接收之后就会断开
适用范围:
websocket 用于前后端实时交互数据
ajax 非实时
发起人:
AJAX 客户端发起
WebSocket 服务器端和客户端相互推送
了解 WebSocket 嘛?
长轮询和短轮询,WebSocket 是长轮询。
具体比如在一个电商场景,商品的库存可能会变化,所以需要及时反映给用户,所以客户端会不停的发请求,然后服务器端会不停的去查变化,不管变不变,都返回,这个是短轮询。
而长轮询则表现为如果没有变,就不返回,而是等待变或者超时(一般是十几秒)才返回,如果没有返回,客户端也不需要一直发请求,所以减少了双方的压力。
参考链接
HTTP 如何实现长连接?在什么时候会超时?
通过在头部(请求和响应头)设置 Connection: keep-alive,HTTP1.0协议支持,但是默认关闭,从HTTP1.1协议以后,连接默认都是长连接
HTTP 一般会有 httpd 守护进程,里面可以设置 keep-alive timeout,当 tcp 链接闲置超过这个时间就会关闭,也可以在 HTTP 的 header 里面设置超时时间
TCP 的 keep-alive 包含三个参数,支持在系统内核的 net.ipv4 里面设置:当 TCP 链接之后,闲置了 tcp_keepalive_time,则会发生侦测包,如果没有收到对方的 ACK,那么会每隔 tcp_keepalive_intvl 再发一次,直到发送了 tcp_keepalive_probes,就会丢弃该链接。
tcp_keepalive_intvl = 15
tcp_keepalive_probes = 5
tcp_keepalive_time = 1800
实际上 HTTP 没有长短链接,只有 TCP 有,TCP 长连接可以复用一个 TCP 链接来发起多次 HTTP 请求,这样可以减少资源消耗,比如一次请求 HTML,可能还需要请求后续的 JS/CSS/图片等
参考链接
https://blog.csdn.net/weixin_37672169/article/details/80283935
问:Fetch API与传统Request的区别
fetch 符合关注点分离,使用 Promise,API 更加丰富,支持 Async/Await
语意简单,更加语意化
可以使用 isomorphic-fetch ,同构方便
参考资源
问:POST一般可以发送什么类型的文件,数据处理的问题
文本、图片、视频、音频等都可以
text/image/audio/ 或 application/json 等
问:TCP 如何保证有效传输及拥塞控制原理。
tcp 是面向连接的、可靠的、传输层通信协议
可靠体现在:有状态、可控制
有状态是指 TCP 会确认发送了哪些报文,接收方受到了哪些报文,哪些没有收到,保证数据包按序到达,不允许有差错
可控制的是指,如果出现丢包或者网络状况不佳,则会跳转自己的行为,减少发送的速度或者重发
所以上面能保证数据包的有效传输。
拥塞控制原理
原因是有可能整个网络环境特别差,容易丢包,那么发送端就应该注意了。
主要用三种方法:
慢启动阈值 + 拥塞避免
快速重传
快速回复
慢启动阈值 + 拥塞避免
对于拥塞控制来说,TCP 主要维护两个核心状态:
拥塞窗口(cwnd)
慢启动阈值(ssthresh)
在发送端使用拥塞窗口来控制发送窗口的大小。
然后采用一种比较保守的慢启动算法来慢慢适应这个网络,在开始传输的一段时间,发送端和接收端会首先通过三次握手建立连接,确定各自接收窗口大小,然后初始化双方的拥塞窗口,接着每经过一轮 RTT(收发时延),拥塞窗口大小翻倍,直到达到慢启动阈值。
然后开始进行拥塞避免,拥塞避免具体的做法就是之前每一轮 RTT,拥塞窗口翻倍,现在每一轮就加一个。
快速重传
在 TCP 传输过程中,如果发生了丢包,接收端就会发送之前重复 ACK,比如 第 5 个包丢了,6、7 达到,然后接收端会为 5,6,7 都发送第四个包的 ACK,这个时候发送端受到了 3 个重复的 ACK,意识到丢包了,就会马上进行重传,而不用等到 RTO (超时重传的时间)
选择性重传:报文首部可选性中加入 SACK 属性,通过 left edge 和 right edge 标志那些包到了,然后重传没到的包
快速恢复
如果发送端收到了 3 个重复的 ACK,发现了丢包,觉得现在的网络状况已经进入拥塞状态了,那么就会进入快速恢复阶段:
会将拥塞阈值降低为 拥塞窗口的一半
然后拥塞窗口大小变为拥塞阈值
接着 拥塞窗口再进行线性增加,以适应网络状况
问:OPTION是干啥的?举个用到OPTION的例子?
旨在发送一种探测请求,以确定针对某个目标地址的请求必须具有怎么样的约束,然后根据约束发送真正的请求。
比如针对跨域资源的预检,就是采用 HTTP 的 OPTIONS 方法先发送的。用来处理跨域请求
问:http知道嘛?哪一层的协议?(应用层)
灵活可扩展,除了规定空格分隔单词,换行分隔字段以外,其他都没有限制,不仅仅可以传输文本,还可以传输图片、视频等任意资源
可靠传输,基于 TCP/IP 所以继承了这一特性
请求-应答,有来有回
无状态,每次 HTTP 请求都是独立的,无关的、默认不需要保存上下文信息
缺点:
明文传输不安全
复用一个 TCP 链接,会发生对头拥塞
无状态在长连接场景中,需要保存大量上下文,以避免传输大量重复的信息
问:OSI七层模型和TCP/IP四层模型
应用层
表示层
会话层
传输层
网络层
数据链路层
物理层
TCP/IP 四层概念:
应用层:应用层、表示层、会话层:HTTP
传输层:传输层:TCP/UDP
网络层:网络层:IP
数据链路层:数据链路层、物理层
问:TCP 协议怎么保证可靠的,UDP 为什么不可靠?
TCP 是面向连接的、可靠的、传输层通信协议
UDP 是无连接的传输层通信协议,继承 IP 特性,基于数据报
为什么 TCP 可靠?TCP 的可靠性体现在有状态和控制
会精准记录那些数据发送了,那些数据被对方接收了,那些没有被接收,而且保证数据包按序到达,不允许半点差错,这就是有状态
当意识到丢包了或者网络环境不佳,TCP 会根据具体情况调整自己的行为,控制自己的发送速度或者重发,这是可控制的
反之 UDP 就是无状态的和不可控制的
HTTP 2 改进
改进性能:
头部压缩
多路信道复用
Server Push
参考资料
页面输入url
Tcp、Http请求
Http和Https区别
请求头
浏览器、Http缓存
设置什么可以设置浏览器缓存
Https加密
Promise 面试题
dachang
Promise.resolve().then(() => {
console.log(0);
return Promise.resolve(4);
}).then((res) => {
console.log(res)
})
Promise.resolve().then(() => {
console.log(1);
}).then(() => {
console.log(2);
}).then(() => {
console.log(3);
}).then(() => {
console.log(5);
}).then(() =>{
console.log(6);
})大厂:
Promise.resolve().then(() => {<br />console.log(0);<br />return Promise.resolve(4);<br />}).then((res) => {<br />console.log(res)<br />})<br />Promise.resolve().then(() => {<br />console.log(1);<br />}).then(() => {<br />console.log(2);<br />}).then(() => {<br />console.log(3);<br />}).then(() => {<br />console.log(5);<br />}).then(() =>{<br />console.log(6);<br />})<br />打印结果:0、1、2、3、4、5、6 😱
这里4怎么跑到3后面去了,不讲武德?Why……
在我看来,这道题有两个 Promise.resolve(),相当于创建两个状态为 fulfilled 的 Promise。
紧随他们后面的第一个 then 方法会交替将其执行函数送入微任务队列排队执行,所以这里的0和1,大家都可以理解,但是接下来执行的不是 console.log(res) 而是 console.log(2)。
如果说需要等待 return Promise.resolve(4) 执行完并将其结果和状态同步给外部的 Promise,那么这里只需要创建一个微任务去处理就应该可以了,也就是 4 会在 2 后面才对,为啥需要创建两个微任务呢?
手写前需要先了解这些
什么是宏任务与微任务?
我们都知道 Js 是单线程都,但是一些高耗时操作就带来了进程阻塞问题。为了解决这个问题,Js 有两种任务的执行模式:同步模式(Synchronous)和异步模式(Asynchronous)。
在异步模式下,创建异步任务主要分为宏任务与微任务两种。ES6 规范中,宏任务(Macrotask) 称为 Task, 微任务(Microtask) 称为 Jobs。宏任务是由宿主(浏览器、Node)发起的,而微任务由 JS 自身发起。
宏任务与微任务的几种创建方式
- \begin{array}{|c|c|}\hline 宏任务(Macrotask&微任务(Microtask)\\hline setTimeout &requestAnimationFrame(有争议)\\hline setInterval & MutationObserver(浏览器环境)\\hline MessageChannel & Promise.[ then/catch/finally ] \\hline I/O,事件队列 & process.nextTick(Node环境)\\hline setImmediate(Node环境)& queueMicrotask \\hline script(整体代码块)& \\hline \end{array}宏任务(Macrotask微任务(Microtask)setTimeoutrequestAnimationFrame(有争议)setIntervalMutationObserver(浏览器环境)MessageChannelPromise.[then/catch/finally]I/O,事件队列process.nextTick(Node环境)setImmediate(Node环境)queueMicrotaskscript(整体代码块)\begin{array}{|c|c|}\hline 宏任务(Macrotask&微任务(Microtask)\\hline setTimeout &requestAnimationFrame(有争议)\\hline setInterval & MutationObserver(浏览器环境)\\hline MessageChannel & Promise.[ then/catch/finally ] \\hline I/O,事件队列 & process.nextTick(Node环境)\\hline setImmediate(Node环境)& queueMicrotask \\hline script(整体代码块)& \\hline \end{array}宏任务(MacrotasksetTimeoutsetIntervalMessageChannelI/O,事件队列setImmediate(Node环境)script(整体代码块)微任务(Microtask)requestAnimationFrame(有争议)MutationObserver(浏览器环境)Promise.[then/catch/finally]process.nextTick(Node环境)queueMicrotask
如何理解 script(整体代码块)是个宏任务呢
- 实际上如果同时存在两个 script 代码块,会首先在执行第一个 script 代码块中的同步代码,如果这个过程中创建了微任务并进入了微任务队列,第一个 script 同步代码执行完之后,会首先去清空微任务队列,再去开启第二个 script 代码块的执行。所以这里应该就可以理解 script(整体代码块)为什么会是宏任务。
什么是 EventLoop ?

判断宏任务队列是否为空
不空 —> 执行最早进入队列的任务 —> 执行下一步
空 —> 执行下一步
判断微任务队列是否为空
不空 —> 执行最早进入队列的任务 —> 继续检查微任务队列空不空
空 —> 执行下一步
因为首次执行宏队列中会有 script(整体代码块)任务,所以实际上就是 Js 解析完成后,在异步任务中,会先执行完所有的微任务,这里也是很多面试题喜欢考察的。需要注意的是,新创建的微任务会立即进入微任务队列排队执行,不需要等待下一次轮回。
什么是 Promise A+ 规范?
看到 A+ 肯定会想到是不是还有 A,事实上确实有。其实 Promise 有多种规范,除了前面的 Promise A、promise A+ 还有 Promise/B,Promise/D。目前我们使用的 Promise 是基于 Promise A+ 规范实现的,感兴趣的移步 Promise A+规范了解一下,这里不赘述。
检验一份手写 Promise 靠不靠谱,通过 Promise A+ 规范自然是基本要求,这里我们可以借助 promises-aplus-tests 来检测我们的代码是否符合规范,后面我会讲到如何使用它。
手写开始
很多手写版本都是使用 setTimeout 去做异步处理,但是 setTimeout 属于宏任务,这与 Promise 是个微任务相矛盾,所以我打算选择一种创建微任务的方式去实现我们的手写代码。
这里我们有几种选择,一种就是 Promise A+ 规范中也提到的,process.nextTick( Node 端 ) 与MutationObserver( 浏览器端 ),考虑到利用这两种方式需要做环境判断,所以在这里我们就推荐另外一种创建微任务的方式 queueMicrotask,了解更多 —> 在 JavaScript 中通过 queueMicrotask() 使用微任务;
一、Promise 核心逻辑实现
我们先简单实现一下 Promise 的基础功能。先看原生 Promise 实现的 🌰,第一步我们要完成相同的功能。
原生🌰 👇
const promise = new Promise((resolve, reject) => {
resolve(‘success’)
reject(‘err’)
})
promise.then(value => {
console.log(‘resolve’, value)
}, reason => {
console.log(‘reject’, reason)
})
// 输出 resolve success我们来分析一下基本原理:
Promise 是一个类,在执行这个类的时候会传入一个执行器,这个执行器会立即执行
Promise 会有三种状态
Pending 等待
Fulfilled 完成
Rejected 失败
状态只能由 Pending —> Fulfilled 或者 Pending —> Rejected,且一但发生改变便不可二次修改;
Promise 中使用 resolve 和 reject 两个函数来更改状态;
then 方法内部做但事情就是状态判断
如果状态是成功,调用成功回调函数
如果状态是失败,调用失败回调函数
下面开始实现:
- 新建 MyPromise 类,传入执行器 executor
// 新建 MyPromise.js
// 新建 MyPromise 类
class MyPromise {
constructor(executor){
// executor 是一个执行器,进入会立即执行
executor()
}
}
- 新建 MyPromise 类,传入执行器 executor
- executor 传入 resolve 和 reject 方法
// MyPromise.js
// 新建 MyPromise 类
class MyPromise {
constructor(executor){
// executor 是一个执行器,进入会立即执行
// 并传入resolve和reject方法
executor(this.resolve, this.reject)
}
// resolve和reject为什么要用箭头函数?
// 如果直接调用的话,普通函数this指向的是window或者undefined
// 用箭头函数就可以让this指向当前实例对象
// 更改成功后的状态
resolve = () => {}
// 更改失败后的状态
reject = () => {}
}
- executor 传入 resolve 和 reject 方法
- 状态与结果的管理
// MyPromise.js
// 先定义三个常量表示状态
const PENDING = ‘pending’;
const FULFILLED = ‘fulfilled’;
const REJECTED = ‘rejected’;
// 新建 MyPromise 类
class MyPromise {
constructor(executor){
// executor 是一个执行器,进入会立即执行
// 并传入resolve和reject方法
executor(this.resolve, this.reject)
}
// 储存状态的变量,初始值是 pending
status = PENDING;
// resolve和reject为什么要用箭头函数?
// 如果直接调用的话,普通函数this指向的是window或者undefined
// 用箭头函数就可以让this指向当前实例对象
// 成功之后的值
value = null;
// 失败之后的原因
reason = null;
// 更改成功后的状态
resolve = (value) => {
// 只有状态是等待,才执行状态修改
if (this.status === PENDING) {
// 状态修改为成功
this.status = FULFILLED;
// 保存成功之后的值
this.value = value;
}
}
// 更改失败后的状态
reject = (reason) => {
// 只有状态是等待,才执行状态修改
if (this.status === PENDING) {
// 状态成功为失败
this.status = REJECTED;
// 保存失败后的原因
this.reason = reason;
}
}
}
- 状态与结果的管理
- then 的简单实现
// MyPromise.js
then(onFulfilled, onRejected) {
// 判断状态
if (this.status === FULFILLED) {
// 调用成功回调,并且把值返回
onFulfilled(this.value);
} else if (this.status === REJECTED) {
// 调用失败回调,并且把原因返回
onRejected(this.reason);
}
}
- then 的简单实现
- 使用 module.exports 对外暴露 MyPromise 类
// MyPromise.js
module.exports = MyPromise;
- 使用 module.exports 对外暴露 MyPromise 类
看一下我们目前实现的完整代码🥳
// MyPromise.js
// 先定义三个常量表示状态
const PENDING = ‘pending’;
const FULFILLED = ‘fulfilled’;
const REJECTED = ‘rejected’;
// 新建 MyPromise 类
class MyPromise {
constructor(executor){
// executor 是一个执行器,进入会立即执行
// 并传入resolve和reject方法
executor(this.resolve, this.reject)
}
// 储存状态的变量,初始值是 pending
status = PENDING;
// resolve和reject为什么要用箭头函数?
// 如果直接调用的话,普通函数this指向的是window或者undefined
// 用箭头函数就可以让this指向当前实例对象
// 成功之后的值
value = null;
// 失败之后的原因
reason = null;
// 更改成功后的状态
resolve = (value) => {
// 只有状态是等待,才执行状态修改
if (this.status === PENDING) {
// 状态修改为成功
this.status = FULFILLED;
// 保存成功之后的值
this.value = value;
}
}
// 更改失败后的状态
reject = (reason) => {
// 只有状态是等待,才执行状态修改
if (this.status === PENDING) {
// 状态成功为失败
this.status = REJECTED;
// 保存失败后的原因
this.reason = reason;
}
}
then(onFulfilled, onRejected) {
// 判断状态
if (this.status === FULFILLED) {
// 调用成功回调,并且把值返回
onFulfilled(this.value);
} else if (this.status === REJECTED) {
// 调用失败回调,并且把原因返回
onRejected(this.reason);
}
}
}
module.exports = MyPromise使用我的手写代码执行一下上面那个🌰
// 新建 test.js
// 引入我们的 MyPromise.js
const MyPromise = require(‘./MyPromise’)
const promise = new MyPromise((resolve, reject) => {
resolve(‘success’)
reject(‘err’)
})
promise.then(value => {
console.log(‘resolve’, value)
}, reason => {
console.log(‘reject’, reason)
})
// 执行结果:resolve success执行结果符合我们的预期,第一步完成了👏👏👏
二、在 Promise 类中加入异步逻辑
- 上面还没有经过异步处理,如果有异步逻辑加如来会带来一些问题,例如
// test.js<br />const MyPromise = require('./MyPromise')<br />const promise = new MyPromise((resolve, reject) => {<br />setTimeout(() => {<br />resolve('success')<br />}, 2000); <br />})<br />promise.then(value => {<br />console.log('resolve', value)<br />}, reason => {<br />console.log('reject', reason)<br />})<br />// 没有打印信息!!! 分析原因:
- 主线程代码立即执行,setTimeout 是异步代码,then 会马上执行,这个时候判断 Promise 状态,状态是 Pending,然而之前并没有判断等待这个状态
这里就需要我们处理一下 Pending 状态,我们改造一下之前的代码 🤔
- 缓存成功与失败回调
// MyPromise.js
// MyPromise 类中新增
// 存储成功回调函数
onFulfilledCallback = null;
// 存储失败回调函数
onRejectedCallback = null;
- 缓存成功与失败回调
- then 方法中的 Pending 的处理
// MyPromise.js
then(onFulfilled, onRejected) {
// 判断状态
if (this.status === FULFILLED) {
// 调用成功回调,并且把值返回
onFulfilled(this.value);
} else if (this.status === REJECTED) {
// 调用失败回调,并且把原因返回
onRejected(this.reason);
} else if (this.status === PENDING) {
// ==== 新增 ====
// 因为不知道后面状态的变化情况,所以将成功回调和失败回调存储起来
// 等到执行成功失败函数的时候再传递
this.onFulfilledCallback = onFulfilled;
this.onRejectedCallback = onRejected;
}
}
- then 方法中的 Pending 的处理
- resolve 与 reject 中调用回调函数
// MyPromise.js<br />// 更改成功后的状态<br />resolve = (value) => {<br />// 只有状态是等待,才执行状态修改<br />if (this.status === PENDING) {<br />// 状态修改为成功<br />this.status = FULFILLED;<br />// 保存成功之后的值<br />this.value = value;<br />// ==== 新增 ====<br />// 判断成功回调是否存在,如果存在就调用<br />this.onFulfilledCallback && this.onFulfilledCallback(value);<br />}<br />}<br />
// MyPromise.js
// 更改失败后的状态
reject = (reason) => {
// 只有状态是等待,才执行状态修改
if (this.status === PENDING) {
// 状态成功为失败
this.status = REJECTED;
// 保存失败后的原因
this.reason = reason;
// ==== 新增 ====
// 判断失败回调是否存在,如果存在就调用
this.onRejectedCallback && this.onRejectedCallback(reason)
}
}
- resolve 与 reject 中调用回调函数
我们再执行一下上面的🌰
// test.js
const MyPromise = require(‘./MyPromise’)
const promise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(‘success’)
}, 2000);
})
promise.then(value => {
console.log(‘resolve’, value)
}, reason => {
console.log(‘reject’, reason)
})
// 等待 2s 输出 resolve success目前已经可以简单处理异步问题了✌️
- 上面还没有经过异步处理,如果有异步逻辑加如来会带来一些问题,例如
三、实现 then 方法多次调用添加多个处理函数
Promise 的 then 方法是可以被多次调用的。这里如果有三个 then 的调用,如果是同步回调,那么直接返回当前的值就行;如果是异步回调,那么保存的成功失败的回调,需要用不同的值保存,因为都互不相同。之前的代码需要改进。
同样的先看一个🌰
// test.js
const MyPromise = require(‘./MyPromise’)
const promise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(‘success’)
}, 2000);
})
promise.then(value => {
console.log(1)
console.log(‘resolve’, value)
})
promise.then(value => {
console.log(2)
console.log(‘resolve’, value)
})
promise.then(value => {
console.log(3)
console.log(‘resolve’, value)
})
// 3
// resolve success目前的代码只能输出:3 resolve success,怎么可以把 1、2 弄丢呢!
我们应该一视同仁,保证所有 then 中的回调函数都可以执行 🤔 继续改造
- MyPromise 类中新增两个数组
// MyPromise.js
// 存储成功回调函数
// onFulfilledCallback = null;
onFulfilledCallbacks = [];
// 存储失败回调函数
// onRejectedCallback = null;
onRejectedCallbacks = [];
- MyPromise 类中新增两个数组
- 回调函数存入数组中
// MyPromise.js
then(onFulfilled, onRejected) {
// 判断状态
if (this.status === FULFILLED) {
// 调用成功回调,并且把值返回
onFulfilled(this.value);
} else if (this.status === REJECTED) {
// 调用失败回调,并且把原因返回
onRejected(this.reason);
} else if (this.status === PENDING) {
// ==== 新增 ====
// 因为不知道后面状态的变化,这里先将成功回调和失败回调存储起来
// 等待后续调用
this.onFulfilledCallbacks.push(onFulfilled);
this.onRejectedCallbacks.push(onRejected);
}
}
- 回调函数存入数组中
- 循环调用成功和失败回调
// MyPromise.js<br />// 更改成功后的状态<br />resolve = (value) => {<br />// 只有状态是等待,才执行状态修改<br />if (this.status === PENDING) {<br />// 状态修改为成功<br />this.status = FULFILLED;<br />// 保存成功之后的值<br />this.value = value;<br />// ==== 新增 ====<br />// resolve里面将所有成功的回调拿出来执行<br />while (this.onFulfilledCallbacks.length) {<br />// Array.shift() 取出数组第一个元素,然后()调用,shift不是纯函数,取出后,数组将失去该元素,直到数组为空<br />this.onFulfilledCallbacks.shift()(value)<br />}<br />}<br />}<br />
- 循环调用成功和失败回调
// MyPromise.js
// 更改失败后的状态
reject = (reason) => {
// 只有状态是等待,才执行状态修改
if (this.status === PENDING) {
// 状态成功为失败
this.status = REJECTED;
// 保存失败后的原因
this.reason = reason;
// ==== 新增 ====
// resolve里面将所有失败的回调拿出来执行
while (this.onRejectedCallbacks.length) {
this.onRejectedCallbacks.shift()(reason)
}
}
}
- 再来运行一下,看看结果👇<br />1<br />resolve success<br />2<br />resolve success<br />3<br />resolve success- 👏👏👏 完美,继续- 四、实现 then 方法的链式调用- then 方法要链式调用那么就需要返回一个 Promise 对象- then 方法里面 return 一个返回值作为下一个 then 方法的参数,如果是 return 一个 Promise 对象,那么就需要判断它的状态- 举个栗子 🌰<br />// test.js<br />const MyPromise = require('./MyPromise')<br />const promise = new MyPromise((resolve, reject) => {<br />// 目前这里只处理同步的问题<br />resolve('success')<br />})<br />function other () {<br />return new MyPromise((resolve, reject) =>{<br />resolve('other')<br />})<br />}<br />promise.then(value => {<br />console.log(1)<br />console.log('resolve', value)<br />return other()<br />}).then(value => {<br />console.log(2)<br />console.log('resolve', value)<br />})- 用目前的手写代码运行的时候会报错 😣 无法链式调用<br />}).then(value => {<br />^<br />TypeError: Cannot read property 'then' of undefined- 接着改 💪<br />// MyPromise.js<br />class MyPromise {<br />......<br />then(onFulfilled, onRejected) {<br />// ==== 新增 ====<br />// 为了链式调用这里直接创建一个 MyPromise,并在后面 return 出去<br />const promise2 = new MyPromise((resolve, reject) => {<br />// 这里的内容在执行器中,会立即执行<br />if (this.status === FULFILLED) {<br />// 获取成功回调函数的执行结果<br />const x = onFulfilled(this.value);<br />// 传入 resolvePromise 集中处理<br />resolvePromise(x, resolve, reject);<br />} else if (this.status === REJECTED) {<br />onRejected(this.reason);<br />} else if (this.status === PENDING) {<br />this.onFulfilledCallbacks.push(onFulfilled);<br />this.onRejectedCallbacks.push(onRejected);<br />}<br />}) <br />return promise2;<br />}<br />}<br />function resolvePromise(x, resolve, reject) {<br />// 判断x是不是 MyPromise 实例对象<br />if(x instanceof MyPromise) {<br />// 执行 x,调用 then 方法,目的是将其状态变为 fulfilled 或者 rejected<br />// x.then(value => resolve(value), reason => reject(reason))<br />// 简化之后<br />x.then(resolve, reject)<br />} else{<br />// 普通值<br />resolve(x)<br />}<br />}- 执行一下,结果👇<br />1<br />resolve success<br />2<br />resolve other- 符合预期 😎- 五、then 方法链式调用识别 Promise 是否返回自己- 如果 then 方法返回的是自己的 Promise 对象,则会发生循环调用,这个时候程序会报错- 例如下面这种情况👇<br />// test.js<br />const promise = new Promise((resolve, reject) => {<br />resolve(100)<br />})<br />const p1 = promise.then(value => {<br />console.log(value)<br />return p1<br />})- 使用原生 Promise 执行这个代码,会报类型错误<br />100<br />Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>- 我们在 MyPromise 实现一下<br />// MyPromise.js<br />class MyPromise {<br />......<br />then(onFulfilled, onRejected) {<br />const promise2 = new MyPromise((resolve, reject) => {<br />if (this.status === FULFILLED) {<br />const x = onFulfilled(this.value);<br />// resolvePromise 集中处理,将 promise2 传入<br />resolvePromise(promise2, x, resolve, reject);<br />} else if (this.status === REJECTED) {<br />onRejected(this.reason);<br />} else if (this.status === PENDING) {<br />this.onFulfilledCallbacks.push(onFulfilled);<br />this.onRejectedCallbacks.push(onRejected);<br />}<br />}) <br />return promise2;<br />}<br />}<br />function resolvePromise(promise2, x, resolve, reject) {<br />// 如果相等了,说明return的是自己,抛出类型错误并返回<br />if (promise2 === x) {<br />return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))<br />}<br />if(x instanceof MyPromise) {<br />x.then(resolve, reject)<br />} else{<br />resolve(x)<br />}<br />}- 执行一下,竟然报错了 😱<br /> resolvePromise(promise2, x, resolve, reject);<br />^<br />ReferenceError: Cannot access 'promise2' before initialization- 为啥会报错呢?从错误提示可以看出,我们必须要等 promise2 完成初始化。这个时候我们就要用上宏微任务和事件循环的知识了,这里就需要创建一个异步函数去等待 promise2 完成初始化,前面我们已经确认了创建微任务的技术方案 --> queueMicrotask<br />// MyPromise.js<br />class MyPromise {<br />......<br />then(onFulfilled, onRejected) {<br />const promise2 = new MyPromise((resolve, reject) => {<br />if (this.status === FULFILLED) {<br />// ==== 新增 ====<br />// 创建一个微任务等待 promise2 完成初始化<br />queueMicrotask(() => {<br />// 获取成功回调函数的执行结果<br />const x = onFulfilled(this.value);<br />// 传入 resolvePromise 集中处理<br />resolvePromise(promise2, x, resolve, reject);<br />}) <br />} else if (this.status === REJECTED) {<br />......<br />}) <br />return promise2;<br />}<br />}- 执行一下<br />// test.js<br />const MyPromise = require('./MyPromise')<br />const promise = new MyPromise((resolve, reject) => {<br />resolve('success')<br />})<br />// 这个时候将promise定义一个p1,然后返回的时候返回p1这个promise<br />const p1 = promise.then(value => {<br />console.log(1)<br />console.log('resolve', value)<br />return p1<br />})<br />// 运行的时候会走reject<br />p1.then(value => {<br />console.log(2)<br />console.log('resolve', value)<br />}, reason => {<br />console.log(3)<br />console.log(reason.message)<br />})- 这里得到我们的结果 👇<br />1<br />resolve success<br />3<br />Chaining cycle detected for promise #<Promise>- 哈哈,搞定 😎 开始下一步- 六、捕获错误及 then 链式调用其他状态代码补充- 目前还缺少重要的一个环节,就是我们的错误捕获还没有处理- 1. 捕获执行器错误- 捕获执行器中的代码,如果执行器中有代码错误,那么 Promise 的状态要变为失败<br />// MyPromise.js<br />constructor(executor){<br />// ==== 新增 ====<br />// executor 是一个执行器,进入会立即执行<br />// 并传入resolve和reject方法<br />try {<br />executor(this.resolve, this.reject)<br />} catch (error) {<br />// 如果有错误,就直接执行 reject<br />this.reject(error)<br />}<br />}- 验证一下:<br />// test.js<br />const MyPromise = require('./MyPromise')<br />const promise = new MyPromise((resolve, reject) => {<br />// resolve('success')<br />throw new Error('执行器错误')<br />})<br />promise.then(value => {<br />console.log(1)<br />console.log('resolve', value)<br />}, reason => {<br />console.log(2)<br />console.log(reason.message)<br />})- 执行结果 👇<br />2<br />执行器错误- OK,通过 😀- 2. then 执行的时错误捕获<br />// MyPromise.js<br />then(onFulfilled, onRejected) {<br />// 为了链式调用这里直接创建一个 MyPromise,并在后面 return 出去<br />const promise2 = new MyPromise((resolve, reject) => {<br />// 判断状态<br />if (this.status === FULFILLED) {<br />// 创建一个微任务等待 promise2 完成初始化<br />queueMicrotask(() => {<br />// ==== 新增 ====<br />try {<br />// 获取成功回调函数的执行结果<br />const x = onFulfilled(this.value);<br />// 传入 resolvePromise 集中处理<br />resolvePromise(promise2, x, resolve, reject);<br />} catch (error) {<br />reject(error)<br />} <br />}) <br />} else if (this.status === REJECTED) {<br />// 调用失败回调,并且把原因返回<br />onRejected(this.reason);<br />} else if (this.status === PENDING) {<br />// 等待<br />// 因为不知道后面状态的变化情况,所以将成功回调和失败回调存储起来<br />// 等到执行成功失败函数的时候再传递<br />this.onFulfilledCallbacks.push(onFulfilled);<br />this.onRejectedCallbacks.push(onRejected);<br />}<br />}) <br />return promise2;<br />}- 验证一下:<br />// test.js<br />const MyPromise = require('./MyPromise')<br />const promise = new MyPromise((resolve, reject) => {<br />resolve('success')<br />// throw new Error('执行器错误')<br />})<br />// 第一个then方法中的错误要在第二个then方法中捕获到<br />promise.then(value => {<br />console.log(1)<br />console.log('resolve', value)<br />throw new Error('then error')<br />}, reason => {<br />console.log(2)<br />console.log(reason.message)<br />}).then(value => {<br />console.log(3)<br />console.log(value);<br />}, reason => {<br />console.log(4)<br />console.log(reason.message)<br />})- 执行结果 👇<br />1<br />resolve success<br />4<br />then error- 这里成功打印了1中抛出的错误 then error- 七、参考 fulfilled 状态下的处理方式,对 rejected 和 pending 状态进行改造- 改造内容包括:- 增加异步状态下的链式调用- 增加回调函数执行结果的判断- 增加识别 Promise 是否返回自己- 增加错误捕获- 1<br />// MyPromise.js<br />then(onFulfilled, onRejected) {<br />// 为了链式调用这里直接创建一个 MyPromise,并在后面 return 出去<br />const promise2 = new MyPromise((resolve, reject) => {<br />// 判断状态<br />if (this.status === FULFILLED) {<br />// 创建一个微任务等待 promise2 完成初始化<br />queueMicrotask(() => {<br />try {<br />// 获取成功回调函数的执行结果<br />const x = onFulfilled(this.value);<br />// 传入 resolvePromise 集中处理<br />resolvePromise(promise2, x, resolve, reject);<br />} catch (error) {<br />reject(error)<br />} <br />}) <br />} else if (this.status === REJECTED) { <br />// ==== 新增 ====<br />// 创建一个微任务等待 promise2 完成初始化<br />queueMicrotask(() => {<br />try {<br />// 调用失败回调,并且把原因返回<br />const x = onRejected(this.reason);<br />// 传入 resolvePromise 集中处理<br />resolvePromise(promise2, x, resolve, reject);<br />} catch (error) {<br />reject(error)<br />} <br />}) <br />} else if (this.status === PENDING) {<br />// 等待<br />// 因为不知道后面状态的变化情况,所以将成功回调和失败回调存储起来<br />// 等到执行成功失败函数的时候再传递<br />this.onFulfilledCallbacks.push(() => {<br />// ==== 新增 ====<br />queueMicrotask(() => {<br />try {<br />// 获取成功回调函数的执行结果<br />const x = onFulfilled(this.value);<br />// 传入 resolvePromise 集中处理<br />resolvePromise(promise2, x, resolve, reject);<br />} catch (error) {<br />reject(error)<br />} <br />}) <br />});<br />this.onRejectedCallbacks.push(() => {<br />// ==== 新增 ====<br />queueMicrotask(() => {<br />try {<br />// 调用失败回调,并且把原因返回<br />const x = onRejected(this.reason);<br />// 传入 resolvePromise 集中处理<br />resolvePromise(promise2, x, resolve, reject);<br />} catch (error) {<br />reject(error)<br />} <br />}) <br />});<br />}<br />}) <br />return promise2;<br />}- 八、then 中的参数变为可选- 上面我们处理 then 方法的时候都是默认传入 onFulfilled、onRejected 两个回调函数,但是实际上原生 Promise 是可以选择参数的单传或者不传,都不会影响执行。- 例如下面这种 👇<br />// test.js<br />const promise = new Promise((resolve, reject) => {<br />resolve(100)<br />})<br />promise<br />.then()<br />.then()<br />.then()<br />.then(value => console.log(value))<br />// 输出 100- 所以我们需要对 then 方法做一点小小的调整<br />// MyPromise.js<br />then(onFulfilled, onRejected) {<br />// 如果不传,就使用默认函数<br />onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;<br />onRejected = typeof onRejected === 'function' ? onRejected : reason => {throw reason};<br />// 为了链式调用这里直接创建一个 MyPromise,并在后面 return 出去<br />const promise2 = new MyPromise((resolve, reject) => {<br />......<br />}- 改造完自然是需要验证一下的- 先看情况一:resolve 之后<br />// test.js<br />const MyPromise = require('./MyPromise')<br />const promise = new MyPromise((resolve, reject) => {<br />resolve('succ')<br />})<br />promise.then().then().then(value => console.log(value))<br />// 打印 succ- 先看情况一:reject 之后<br />// test.js<br />const MyPromise = require('./MyPromise')<br />const promise = new MyPromise((resolve, reject) => {<br />reject('err')<br />})<br />promise.then().then().then(value => console.log(value), reason => console.log(reason))<br />// 打印 err- 写到这里,麻雀版的 Promise 基本完成了,鼓掌 👏👏👏- 九、实现 resolve 与 reject 的静态调用- 就像开头挂的那道面试题使用 return Promise.resolve 来返回一个 Promise 对象,我们用现在的手写代码尝试一下<br />const MyPromise = require('./MyPromise')<br />MyPromise.resolve().then(() => {<br />console.log(0);<br />return MyPromise.resolve(4);<br />}).then((res) => {<br />console.log(res)<br />})- 结果它报错了 😥<br />MyPromise.resolve().then(() => {<br />^<br />TypeError: MyPromise.resolve is not a function- 除了 Promise.resolve 还有 Promise.reject 的用法,我们都要去支持,接下来我们来实现一下<br />// MyPromise.js<br />MyPromise {<br />......<br />// resolve 静态方法<br />static resolve (parameter) {<br />// 如果传入 MyPromise 就直接返回<br />if (parameter instanceof MyPromise) {<br />return parameter;<br />}<br />// 转成常规方式<br />return new MyPromise(resolve => {<br />resolve(parameter);<br />});<br />}<br />// reject 静态方法<br />static reject (reason) {<br />return new MyPromise((resolve, reject) => {<br />reject(reason);<br />});<br />}<br />}- 这样我们再测试上面的 🌰 就不会有问题啦- 执行结果 👇<br />0 4- 到这里手写工作就基本完成了,前面主要为了方便理解,所以有一些冗余代码,我规整一下<br />// MyPromise.js<br />// 先定义三个常量表示状态<br />const PENDING = 'pending';<br />const FULFILLED = 'fulfilled';<br />const REJECTED = 'rejected';<br />// 新建 MyPromise 类<br />class MyPromise {<br />constructor(executor){<br />// executor 是一个执行器,进入会立即执行<br />// 并传入resolve和reject方法<br />try {<br />executor(this.resolve, this.reject)<br />} catch (error) {<br />this.reject(error)<br />}<br />}<br />// 储存状态的变量,初始值是 pending<br />status = PENDING;<br />// 成功之后的值<br />value = null;<br />// 失败之后的原因<br />reason = null;<br />// 存储成功回调函数<br />onFulfilledCallbacks = [];<br />// 存储失败回调函数<br />onRejectedCallbacks = [];<br />// 更改成功后的状态<br />resolve = (value) => {<br />// 只有状态是等待,才执行状态修改<br />if (this.status === PENDING) {<br />// 状态修改为成功<br />this.status = FULFILLED;<br />// 保存成功之后的值<br />this.value = value;<br />// resolve里面将所有成功的回调拿出来执行<br />while (this.onFulfilledCallbacks.length) {<br />// Array.shift() 取出数组第一个元素,然后()调用,shift不是纯函数,取出后,数组将失去该元素,直到数组为空<br />this.onFulfilledCallbacks.shift()(value)<br />}<br />}<br />}<br />// 更改失败后的状态<br />reject = (reason) => {<br />// 只有状态是等待,才执行状态修改<br />if (this.status === PENDING) {<br />// 状态成功为失败<br />this.status = REJECTED;<br />// 保存失败后的原因<br />this.reason = reason;<br />// resolve里面将所有失败的回调拿出来执行<br />while (this.onRejectedCallbacks.length) {<br />this.onRejectedCallbacks.shift()(reason)<br />}<br />}<br />}<br />then(onFulfilled, onRejected) {<br />const realOnFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;<br />const realOnRejected = typeof onRejected === 'function' ? onRejected : reason => {throw reason};<br />// 为了链式调用这里直接创建一个 MyPromise,并在后面 return 出去<br />const promise2 = new MyPromise((resolve, reject) => {<br />const fulfilledMicrotask = () => {<br />// 创建一个微任务等待 promise2 完成初始化<br />queueMicrotask(() => {<br />try {<br />// 获取成功回调函数的执行结果<br />const x = realOnFulfilled(this.value);<br />// 传入 resolvePromise 集中处理<br />resolvePromise(promise2, x, resolve, reject);<br />} catch (error) {<br />reject(error)<br />} <br />}) <br />}<br />const rejectedMicrotask = () => { <br />// 创建一个微任务等待 promise2 完成初始化<br />queueMicrotask(() => {<br />try {<br />// 调用失败回调,并且把原因返回<br />const x = realOnRejected(this.reason);<br />// 传入 resolvePromise 集中处理<br />resolvePromise(promise- 到这一步手写部分基本大功告成 🎉🎉🎉- Promise A+ 测试- 上面介绍了 Promise A+ 规范,当然我们手写的版本也得符合了这个规范才有资格叫 Promise, 不然就只能是伪 Promise 了。- 上文讲到了 promises-aplus-tests,现在我们正式开箱使用- 1. 安装一下- npm install promises-aplus-tests -D- 2. 手写代码中加入 deferred<br />// MyPromise.js<br />MyPromise {<br />......<br />}<br />MyPromise.deferred = function () {<br />var result = {};<br />result.promise = new MyPromise(function (resolve, reject) {<br />result.resolve = resolve;<br />result.reject = reject;<br />});<br />return result;<br />}<br />module.exports = MyPromise;- 3. 配置启动命令<br />{<br />"name": "promise",<br />"version": "1.0.0",<br />"description": "my promise",<br />"main": "MyPromise.js",<br />"scripts": {<br />"test": "promises-aplus-tests MyPromise"<br />},<br />"author": "ITEM",<br />"license": "ISC",<br />"devDependencies": {<br />"promises-aplus-tests": "^2.1.2"<br />}<br />}- 开启测试- npm run test- 迫不及待了吧 😄 看看我们的结果如何,走起 🐱🏍- 虽然功能上没啥问题,但是测试却失败了 😥- 针对提示信息,我翻看了一下 Promise A+ 规范,发现我们应该是在 2.3.x 上出现了问题,这里规范使用了不同的方式进行了 then 的返回值判断。- 自红线向下的细节,我们都没有处理,这里要求判断 x 是否为 object 或者 function,满足则接着判断 x.then 是否存在,这里可以理解为判断 x 是否为 promise,这里都功能实际与我们手写版本中 x instanceof MyPromise 功能相似。- 我们还是按照规范改造一下 resolvePromise 方法吧<br />// MyPromise.js<br />function resolvePromise(promise, x, resolve, reject) {<br />// 如果相等了,说明return的是自己,抛出类型错误并返回<br />if (promise === x) {<br />return reject(new TypeError('The promise and the return value are the same'));<br />}<br />if (typeof x === 'object' || typeof x === 'function') {<br />// x 为 null 直接返回,走后面的逻辑会报错<br />if (x === null) {<br />return resolve(x);<br />}<br />let then;<br />try {<br />// 把 x.then 赋值给 then <br />then = x.then;<br />} catch (error) {<br />// 如果取 x.then 的值时抛出错误 error ,则以 error 为据因拒绝 promise<br />return reject(error);<br />}<br />// 如果 then 是函数<br />if (typeof then === 'function') {<br />let called = false;<br />try {<br />then.call(<br />x, // this 指向 x<br />// 如果 resolvePromise 以值 y 为参数被调用,则运行 [[Resolve]](promise, y)<br />y => {<br />// 如果 resolvePromise 和 rejectPromise 均被调用,<br />// 或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用<br />// 实现这条需要前面加一个变量 called<br />if (called) return;<br />called = true;<br />resolvePromise(promise, y, resolve, reject);<br />},<br />// 如果 rejectPromise 以据因 r 为参数被调用,则以据因 r 拒绝 promise<br />r => {<br />if (called) return;<br />called = true;<br />reject(r);<br />});<br />} catch (error) {<br />// 如果调用 then 方法抛出了异常 error:<br />// 如果 resolvePromise 或 rejectPromise 已经被调用,直接返回<br />if (called) return;<br />// 否则以 error 为据因拒绝 promise<br />reject(error);<br />}<br />} else {<br />// 如果 then 不是函数,以 x 为参数执行 promise<br />resolve(x);<br />}<br />} else {<br />// 如果 x 不为对象或者函数,以 x 为参数执行 promise<br />resolve(x);<br />}<br />}- 改造后启动测试- 完美通过 👏👏- 最终时刻,如何解释那道面试题的执行结果- 先用我们自己的 Promise 运行一下那道面试题 👇<br />// test.js<br />const MyPromise = require('./MyPromise.js')<br />MyPromise.resolve().then(() => {<br />console.log(0);<br />return MyPromise.resolve(4);<br />}).then((res) => {<br />console.log(res)<br />})<br />MyPromise.resolve().then(() => {<br />console.log(1);<br />}).then(() => {<br />console.log(2);<br />}).then(() => {<br />console.log(3);<br />}).then(() => {<br />console.log(5);<br />}).then(() =>{<br />console.log(6);<br />})- 执行结果:0、1、2、4、3、5、6 🤯- 这里我们手写版本的 4 并没有和 原生 Promise 一样在 3 后面,而是在 2 后面- 其实从我们的手写代码上看,在判断 then 内部函数执行结果,也就是在这里 👇<br />// MyPromise.js<br />// 获取成功回调函数的执行结果<br />const x = realOnFulfilled(this.value);<br />// 传入 resolvePromise 集中处理<br />resolvePromise(promise2, x, resolve, reject);- 面试题中 x 等于 realOnFulfilled(this.value) 的执行结果,也就是 return 出来的 MyPromise.resolve(4),所以在 x 传入 resolvePromise 方法中进行类型判断时,会发现它是一个 Promise 对象(存在 then 方法),并让其调用 then 方法完成状态转换。再看 resolvePromis 方法中这一块判断逻辑 👇<br />if (typeof x === 'object' || typeof x === 'function') {<br />// x 为 null 直接返回,走后面的逻辑会报错<br />if (x === null) {<br />return resolve(x);<br />}<br />let then;<br />try {<br />// 把 x.then 赋值给 then <br />then = x.then;<br />} catch (error) {<br />// 如果取 x.then 的值时抛出错误 error ,则以 error 为据因拒绝 promise<br />return reject(error);<br />}<br />// 如果 then 是函数<br />if (typeof then === 'function') {<br />let called = false;<br />try {<br />then.call(<br />x, // this 指向 x<br />// 如果 resolvePromise 以值 y 为参数被调用,则运行 [[Resolve]](promise, y)<br />y => {<br />// 如果 resolvePromise 和 rejectPromise 均被调用,<br />// 或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用<br />// 实现这条需要前面加一个变量 called<br />if (called) return;<br />called = true;<br />resolvePromise(promise, y, resolve, reject);<br />},<br />// 如果 rejectPromise 以据因 r 为参数被调用,则以据因 r 拒绝 promise<br />r => {<br />if (called) return;<br />called = true;<br />reject(r);<br />});<br />} <br />......- 那么问题来了- 为什么我们 Promise A+ 测试全部通过的手写代码,执行结果却与原生 Promise 不同?- 在我们手写代码使用创建一次微任务的方式,会带来什么问题吗?- 大家现在肯定和我一样很困惑 🤦🏻♂️ 既然发现不了问题,直接看源码吧 👇- ES6 中的 Promise 虽然是遵循 Promise A+ 规范实现的,但实际上也 Promise A+ 上做了一些功能扩展,例如:Promise.all、Promise.race 等,所以即使都符合 Promise A+ ,执行结果也是可能存在差异的。我们这里更需要思考的是第二个问题,不这么做会带来什么问题,也就是加一次微任务的必要性。- 我尝试过很多例子,都没有找到相关例证,我们手写实现的 Promise 都很好的完成工作,拿到了结果。我不得不去翻看更多的相关文章,我发现有些人会为了让执行结果与原生相同,强行去再多加一次微任务,这种做法是很牵强的。- 毕竟实现 Promise 的目的是为了解决异步编程的问题,能够拿到正确的结果才是最重要的,强行为了符合面试题的输出顺序去多加一次微任务,只能让手写代码变的更加复杂,不好理解。- 在 stackoverflow 上,有一个类似的问题 What is the difference between returned Promise? 回答中有一个信息就是- It only required the execution context stack contains only platform code. 也就相当于等待 execution context stack 清空。- 实际上我们已经在 static resolve 创建了一个新的 MyPromsie,并调用其 then 方法,创建了一个微任务。- 所以,就目前的信息来说,两次微任务依旧不能证明其必要性,目前的 Promise 日常操作,一次微任务都是可以满足。
还问了下数据结构和算法
flex布局咋理解
HEADER标签内一般有什么内容
谈谈你对UTF8字符集的了解
http和https的区别
vue生命周期
vue响应式原理
flex布局的理解
闭包的理解
我觉得问的多的还是居中问题,跟vue的生命周期
今天面试问到了js垃圾回收机制,尬住了,老师这个问题重点是啥
一个可以聊闭包
一个是内存泄露
习题
var F=function(){};
Object.prototype.a=function(){
console.log(‘a()’)
};
Function.prototype.b=function(){
console.log(‘b()’)
}
var f=new F(); 想请问一下f.b()为什么不能沿原型链找到呢?1.fetch API 是对传统ajax api的改进而非取代
2.fetch API是HTML5新增的Web API
3.fetch API拥有更加精细的功能分割:比如头部信息、请求信息、响应信息等均能分布到不同的对象中,更利于处理复杂的ajax场景
4.使用Promise API,更利于异步代码的书写








