前言
因为各种原因女朋友离职了,然后开始步入找工作的情况,给她做一份前端中高级前端面试复习计划,助力offer,希望也能够对大家产生一定的帮助。
一 浏览器篇
1.谈谈Http和Https的区别
HTTP
是不安全的,而 HTTPS 是安全的HTTP
标准端口是80 ,而 HTTPS 的标准端口是443HTTP
无法加密,而HTTPS 对传输的数据进行SSL加密HTTP
无需证书,而HTTPS 需要CA机构颁发的SSL证书
2.谈谈你对TCP三次握手的理解
- 第一次握手:客服端发送一个请求连接,服务器端只能确认自己可以接受客服端发送的报文段
- 第二次握手: 服务端向客服端发送一个链接,确认客服端收到自己发送的报文段
- 第三次握手: 服务器端确认客服端收到了自己发送的报文段
3.谈谈cookie,sessionStorage,localStorage的区别
- 首先cookie,sessionStorage和localStorage都是存放在客户端的
- 大小限制区别:cookie数据不超过4kb,sessionStorage和localStoragelocalStorage最大支持5MB的数据
- 数据有效期不同:
- cookie在服务器设置的有效期内有效,不管窗口和浏览器是否关闭
- sessionStorage仅在当前浏览器窗口关闭前有效,关闭即销毁(临时存储)
- localStorage始终有效,需要手动销毁才会失效
localStorage:常用于长期登录,适合长期保存在本地的数据。sessionStorage :敏感账号一次性登录。cookies与服务器交互。
4.GET 和 POST 有什么区别?
- 从缓存角度来讲,GET 请求会被浏览器主动缓存下来,留下历史记录,而 POST 默认不会。
- 从参数角度来讲,GET 一般放在 URL 中,因此不安全,POST 放在请求体中,因此会更加安全。
- 从TCP角度来讲,GET 请求会把请求报文一次性发出去,而 POST 会分为两个 TCP 数据包,首先发 header 部分,如果服务器响应 100(continue), 然后发 body 部分。
5.常见 HTTP 状态码
状态码 | 描述 |
---|---|
200 | 请求成功 |
301 | 永久重定向 |
302 | 临时重定向 |
304 | 请求资源未修改,可以使用缓存的资源,不用在服务器取 |
400 | 请求有语法错误 |
401 | 没有权限访问 |
404 | 请求资源不存在 |
500 | 服务器内部错误,无法完成请求 |
503 | 请求未完成,因服务器过载、宕机或维护等 |
6.你能说说缓存么
缓存分为强缓存和协商缓存。强缓存不过服务器,协商缓存需要过服务器,协商缓存返回的状态码是304。两类缓存机制可以同时存在,强缓存的优先级高于协商缓存。当执行强缓存时,如若缓存命中,则直接使用缓存数据库中的数据,不再进行缓存协商。
强缓存
首先是检查强缓存,这个阶段不需要
发送HTTP请求。
强缓存主要有Expires和Cache-Control两个属性
- Expires :Exprires的值为服务端返回的数据到期时间。当再次请求时的请求时间小于返回的此时间,则直接使用缓存数据,但是存在一定弊端因为使用的是绝对时间,如果服务端的时间与客户端不一致,那么会导致命中缓存产生偏差,因此很少使用。
- Cache-Control :最常用的属性是max-age,可以设置具体的缓存时间,强缓存常用此属性。
协商缓存
协商缓存需要进行对比判断是否可以使用缓存。浏览器第一次请求数据时,服务器会将缓存标识与数据一起响应给客户端,客户端将它们备份至缓存中。再次请求时,客户端会将缓存中的标识发送给服务器,服务器根据此标识判断。若未失效,返回304状态码,浏览器拿到此状态码就可以直接使用缓存数据了。
协商缓存主要有Last-Modified和Etag两个属性
Last-Modified:服务器在响应请求时,会告诉浏览器资源的最后修改时间。
浏览器接收到后,如果再次请求,会在请求头中携带If-Modified-Since
字段,这个字段的值也就是服务器传来的最后修改时间,服务器拿到请求头中的If-Modified-Since
字段后,其实会和这个服务器中该资源的最后修改时间
对比。但是存在一定弊端因为是按照秒为单位的,如果是毫秒性操作,可能会存在一定误差,因此很少使用。
Etag:服务器响应请求时,通过此字段告诉浏览器当前资源在服务器生成的唯一标识。
浏览器接收到后,如果再次请求,会在请求头中携带If-None-Match
字段,这个字段的值也就是服务器生成的唯一标识,服务器拿到请求头中的If-None-Match
字段后,其实会和这个服务器中的唯一标识进行对比。协商缓存常用此属性。
7.常见的web安全及防护原理
sql注入
是将sql代码伪装到输入参数中,传递到服务器解析并执行的一种攻击手法。
防范:
1.对用户输入进行校验
2.不采用动态拼接sql
XSS(跨站脚本攻击)
往web页面插入恶意的html标签或者js代码。
防范:
1.尽量采用post而不使用get提交表单
2.避免cookie中泄漏用户的隐式
CSRF(跨站请求伪装)
通过伪装来自受信任用户的请求举例子。
防范:
在客服端页面增加伪随机数,通过验证码
二 CSS篇
1. 谈谈CSS盒模型有哪几种
CSS盒模型主要分为标准盒子模型和IE盒子模型,两种盒模型的内容宽度是不同的:
标准盒子模型:内容宽度 = content
IE盒子模型: 内容宽度= content + border + paddin
2. 谈谈CSS选择器的优先级顺序
带!important 标记的样式属性优先级最高; 样式表的来源相同时:!important > 行内样式>ID选择器 > 类选择器 > 标签 > 通配符 > 继承 > 浏览器默认属性
3.谈谈你对BFC的理解
BFC直译为块级格式化上下文,具有BFC特性的元素可以看做是隔离了的独立容器,容器里面的元素不会在布局上影响到外面的元素,并且BFC具有普通容器所没有的的一些特性。通俗一点来讲,可以把BFC理解为一个封闭的大箱子,箱子内部的元素无论如何翻江倒海,都不会影响到外部。
形成BFC的条件
- 只要元素满足下面任一条件即可触发BFC特性:
- body 根元素
- 浮动元素:float 除 none 以外的值
- 绝对定位元素:position (absolute、fixed)
- display 为 inline-block、table-cells、flex
- overflow 除了 visible 以外的值 (hidden、auto、scroll)
BFC常见作用
- 清除浮动
- 去除边距重叠现象
4.隐藏页面中某个元素的方法
1.visibility:hidden
不会改变页面布局,在文档布局中仍保留原来的空间会引起重绘
2.display:none
会改变页面布局,在文档布局中不再分配空间会引起回流+重绘
3.opacity:0
该元素隐藏起来了,但不会改变页面布局,并且,如果该元素已经绑定 一些事件,如click 事件,那么点击该区域,也能触发点击事件的
5.谈谈你对重排和重绘的理解
- 重排:一般来讲修改元素的宽度,高度等影响元素位置的几何属性均会引起重排
- 重绘:一般来讲如果修改元素的颜色或者背景颜色均会引发重绘
通常使用以下两种方式减少重排和重绘,以达到性能优化的目的:
- 最小化重绘和重排,比如样式集中改变,使用添加新样式类名
.class
或cssText
。 - 使用
**absolute**
或**fixed**
使元素脱离文档流,这在制作复杂的动画时对性能的影响比较明显。
6. 谈谈如何用纯CSS画一条0.5px的直线
<div class="half"></div>
<style>
.half {
width: 300px;
background-color: #000;
height: 1px; transform: scaleY(0.5); /* 延Y轴缩小1倍 */
transform-origin: 50% 100%; /* 改变元素变形的原点 *
}
</style>
7. 谈谈如何用纯CSS创建一个三角形
<div class="content"></div>
<style>
.content {
width: 0;
height: 0;
border: 20px solid;
border-color: transparent transparent transparent pink; // 对应上右下左,此处为 左 粉色
}
</style>
8. 谈谈如何使用CSS垂直居中
使用绝对定位和transform(适用于不知道内盒子宽高)
<div id="box">
<div id="child"/>
</div>
#box {
width: 300px;
height: 300px;
background: #ddd;
position: relative;
}
#child {
background: orange;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
使用绝对定位和margin(适用于知道内盒子宽高)
<div id="box">
<div id="child"/>
</div>
#box {
width: 300px;
height: 300px;
background: #ddd;
position: relative;
}
#child {
width: 300px;
height: 300px;
background: orange;
position: absolute;
top: 50%;
left: 50%;
margin-left: -150px;
margin-top: -150px;
}
使用flex布局(适用于任何情况)
<div id="box">
<div id="child"/>
</div>
#box {
display: flex;
background: #ddd;
}
#child {
align-items: center;
justify-content: center;
background: orange;
}
9. 实现两栏布局
利用浮动
<div id="outer">
<div id="left">左侧</div>
<div id="right">右侧</div>
</div>
#outer {
height: 100px;
}
#left {
float: left;
width: 200px;
height: 100%;
background: lightcoral;
}
#right {
margin-left: 200px;
height: 100%;
background: lightseagreen;
}
利用 flex
布局
<div id="outer">
<div id="left">左侧</div>
<div id="right">右侧</div>
</div>
#outer {
display: flex;
height: 100px;
}
#left {
width: 200px;
height: 100%;
background: lightcoral;
}
#right {
flex: 1;
height: 100%;
background: lightseagreen;
}
利用绝对定位
<div id="outer">
<div id="left">左侧</div>
<div id="right">右侧</div>
</div>
#outer {
position: relative;
height: 100px;
}
#left {
position: absolute;
width: 200px;
height: 100%;
background: lightcoral;
}
#right {
margin-left: 200px;
height: 100%;
background: lightseagreen;
}
复制代码
10. 实现圣杯布局和双飞翼布局(三分栏布局)
圣杯布局和双飞翼布局的目的:
- 三栏布局,中间一栏最先加载和渲染(内容最重要,这就是为什么还需要了解这种布局的原因)。
- 两侧内容固定,中间内容随着宽度自适应。
圣杯布局
<div id="container" class="clearfix">
<p class="center"></p>
<p class="left"></p>
<p class="right"></p>
</div>
#container {
padding-left: 200px;
padding-right: 150px;
overflow: auto;
}
#container p {
float: left;
}
.center {
width: 100%;
background-color: lightcoral;
}
.left {
width: 200px;
position: relative;
left: -200px;
margin-left: -100%;
background-color: lightcyan;
}
.right {
width: 150px;
margin-right: -150px;
background-color: lightgreen;
}
.clearfix:after {
content: "";
display: table;
clear: both;
}
双飞翼布局
<div id="main" class="float">
<div id="main-wrap">main</div>
</div>
<div id="left" class="float">left</div>
<div id="right" class="float">right</div>
.float {
float: left;
}
#main {
width: 100%;
height: 200px;
background-color: lightpink;
}
#main-wrap {
margin: 0 190px 0 190px;
}
#left {
width: 190px;
height: 200px;
background-color: lightsalmon;
margin-left: -100%;
}
#right {
width: 190px;
height: 200px;
background-color: lightskyblue;
margin-left: -190px;
}
三 JavaScript篇
1. JavaScript有哪些数据类型,它们的区别是什么?
JavaScript共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。
- 基本数据类型:Undefined、Null、Boolean、Number、String、Symbol、BigInt
- 引用数据类型:Object
其中 Symbol 和 BigInt 是ES6 中新增的数据类型:
- Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
- BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。
两种类型的区别在于存储位置的不同:
- 基本数据类型是直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
- 引用数据类型是存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
2. 数据类型检测的方式有哪些
数据类型判断主要可以使用以下三种方式进行判断,但是都具有一定的局限性,因此在项目实际开发过程中会重写类型判断方法。
typeof:Undefined、Boolean、Number、String、Symbol、Function 等基本数据类型,但是对于其他的都会认为是 object,例如 Null、Date 等,typeof返回的是一个变量的基本类型。
instanceof:可以准确地判断复杂引用数据类型,instanceof返回的是一个布尔值。
Object.prototype.toString.call():调用该方法,统一返回格式“[object Xxx]”的字符串。
function typeOf(obj) {
let res = Object.prototype.toString.call(obj).split(' ')[1]
res = res.substring(0, res.length - 1).toLowerCase()
return res
}
typeOf([]) // 'array'
typeOf({}) // 'object'
typeOf(new Date) // 'date'
3. null和undefined有什么区别
首先 null 和 undefined 都是基本数据类型,undefined 代表的含义是未定义,null 代表的含义是空对象
最初设计JS的时候只有Null,但是使用 typeof 进行判断时,Null 类型化会返回 “object”,因此JS的作者认为一个判断空的数据类型,会被判断为引用类型存在问题,因此创造了undefined
4.为什么 0.1 + 0.2 != 0.3 ?
0.1 + 0.2 != 0.3 是因为在进制转换和进阶运算的过程中出现精度损失。
因为计算机底层是采用二进制数进行对阶运算的,在进制之间进行转换的过程中精度会损失因此0.1 + 0.2 != 0.3 ?
一般在项目中会采用toFaild()进行数据四舍五入,或者做乘10转化为整数放置精度损失之后在除10的处理。
5. 谈谈let、const、var的区别
(1)块级作用域: 块作用域由 { }
包括,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题:
- 内层变量可能覆盖外层变量
- 用来计数的循环变量泄露为全局变量
(2)变量提升: var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否在会报错。
(3)给全局添加属性: 浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。
(4)重复声明: var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。
(5)暂时性死区: 在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。
(6)初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。
(7)指针指向: let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。
6. 谈谈箭头函数与普通函数的区别
(1)箭头函数比普通函数在代码书写层面更加简洁
(2)箭头函数不能被new
(3)箭头函数没有自己的this
(4)箭头函数没有自己的arguments
(5)箭头函数没有prototype
(6)箭头函数不能用作Generator函数,不能使用yeild关键字
(7)箭头函数继承来的this指向永远不会改变
(8)不能通过call()、apply()、bind()等方法改变箭头函数中this的指向
7. 谈谈你对深浅拷贝的理解
浅拷贝即只复制对象的引用,所以副本最终也是指向父对象在堆内存中的对象,无论是副本还是父对象修改了这个对象,副本或者父对象都会因此发生同样的改变
而深拷贝则是直接复制父对象在堆内存中的对象,最终在堆内存中生成一个独立的,与父对象无关的新对象。深拷贝的对象虽然与父对象无关,但是却与父对象一致。当深拷贝完成之后,如果对父对象进行了改变,不会影响到深拷贝的副本。
深拷贝一般使用JSON.parse()嵌套JSON.stringify()得形式,浅拷贝一般使用Object.assign()
但是深拷贝也有一定的弊端,例如obj里面存在时间对象,JSON.parse(JSON.stringify(obj))之后,时间对象变成了字符串,如果obj里有NaN,则序列化的结果会变成null。
所以一般在工作中我会自己封装一个深拷贝,将Date或者NaN独立判断一下。
8、 谈谈你对 new 实现的理解
- 首先创一个新的空对象。
- 根据原型链,设置空对象的
__proto__
为构造函数的prototype
。 - 构造函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)。
- 判断函数的返回值类型,如果是引用类型,就返回这个引用类型的对象。
9. 谈谈你对作用域和作用域链的理解
作用域: 作用域是定义变量的区域,它有一套访问变量的规则,这套规则来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据变量(标识符)进行变量查找。
作用域链: 在函数执行过程中,每遇到一个变量,都会检索从哪里获取和存储数据,该过程从作用域链头部,也就是从活动对象开始搜索,查找同名的标识符,如果找到了就使用这个标识符对应的变量,如果没有则继续搜索作用域链中的下一个对象,如果搜索完所有对象都未找到,则认为该标识符未定义,函数执行过程中,每个标识符都要经历这样的搜索过程。
因为作用域链是栈的结构,全局变量在栈底,每次访问全局变量都会遍历一次栈,这样肯定会影响效率,所以应该减少定义全局变量,从而进行优化。
变量查找时原型链是优先于作用域链的。js引擎先在函数AO对象查找,再到原型链查找,接着是作用域链。
10. 谈谈你对原型和原型链的理解
原型: 在 js 中我们是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性值,这个属性值是一个对 象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法
原型链: 其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。在js中,用 proto 属性来表示一个对象的原型链。当查找一个对象的属性时,js 会向上遍历原型链,直到找到给定名称的属性为止
__proto__这个属性只有在firefox(火狐)或者chrome(谷歌)浏览器中才是公开允许访问的。
简单来说,每个对象都会在其内部初始化一个属性,就是 proto,当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么他就会去__proto__里找这个属性,这个 proto__又会有自己的__proto,于是就这样一直找下去,这便是原型链的概念。
变量查找时原型链是优先于作用域链的。js引擎先在函数AO对象查找,再到原型链查找,接着是作用域链。
11. 谈谈你对Promise的理解
Promise是异步编程的一种解决方案,它是一个构造函数,可以通过new Promise得到它的实例。在Promise上有两个常用函数和一个常用方法。分别是resolve (成功之后的回调函数)和reject (失败之后的回调函数)还有.then() 方法。
在工作中比较常用的是Promise.all()和Promise.race方法
Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。
Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。
12. 如何将异步操作同步化
1.Promise得then链,缺点:在then链中哪个环节出错了不太好调查,代码书写不美观(不推荐使用)
2.async /await :async和await底层实现的原理是基于Generator函数实现的,generator 函数返回一个遍历器对象,遍历器对象 每次调用next 方法,generator 函数 yield 后面的表达式即为 返回对象 value属性的值。
13. 谈谈你对闭包的理解
我个人觉得比较经典的就是VUE底层响应式原理封装的闭包,它是将Object.defineProperty外层包裹Object.defineRecvite形成闭包环境,主要是因为内部函数可能需要用到一些局部变量,但是又需要避免变量污染全局,因此引入了闭包。
一般在工作当中不常使用闭包,因为闭包可能会引起内存泄漏和对处理速度产生一些负面影响影响,因为闭包查找变量会经过作用域链
所以从代码的整体性能角度出发的话,我一般除非不得不用,不然很少使用闭包
14. 谈谈你对垃圾回收机制的理解
在 JavaScript 内存管理中有一个概念叫做 可达性
,就是那些以某种方式可访问或者说可用的值,它们被保证存储在内存中,反之不可访问则需回收
JS垃圾回收机制最常见的便是标记清除算法
和引用计数算法
,但是V8引擎之后各大浏览器主要是采用标记清除算法
进行垃圾回收的
引用计数法,这其实是早先的一种垃圾回收算法,它把 对象是否不再需要
简化定义为 对象有没有其他对象引用到它
,如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收,目前很少使用这种算法了,因为它的问题很多
标记清除算法,主要分为 标记
和 清除
两个阶段,标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁
但是标记清除算法有一个很大的缺点,就是在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了 内存碎片
,并且由于剩余空闲内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的问题。而 标记整理(Mark-Compact)算法 就可以有效地解决,它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存
15. 谈谈你对Event Loop的理解
JavaScript是一种单线程语言,所有任务都在一个线程上完成。一旦遇到大量任务或者遇到一个耗时的任务,比如加载一个高清图片,网页就会出现"假死",因为JavaScript停不下来,也就无法响应用户的行为。为了防止主线程的阻塞,JavaScript 有了 同步 和 异步 的概念。所以 JavaScript 便使用一套机制去处理同步和异步操作,那就是事件循环 (Event Loop)。
- 所有同步任务都在主线程上依次执行,形成一个执行栈(调用栈),异步任务则放入一个任务队列
- 当执行栈中任务执行完,再去检查微任务队列里的微任务是否为空,有就执行,如果执行微任务过程中又遇到微任务,就添加到微任务队列末尾继续执行,把微任务全部执行完
- 微任务执行完后,再到任务队列检查宏任务是否为空,有就取出最先进入队列的宏任务压入执行栈中执行其同步代码
- 然后回到第2步执行该宏任务中的微任务,如此反复,直到宏任务也执行完,如此循环
四 Vue篇
1.为什么 data 是一个函数
组件中的 data 写成一个函数,主要是为了数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的 data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。
如果单纯的写成对象形式,就使得所有组件实例共用了一份 data,就会造成一个全局变量的污染。
2.怎样理解 Vue 的单向数据流?
所有的 prop 都使得其父子 prop 之间形成了一个单向数据流:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样主要是为了防止从子组件意外改变父级组件的状态,从而导致应用的数据流向难以理解。
当每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。子组件想修改时,只能通过 $emit 派发一个自定义事件,父组件接收到后,再由父组件修改。
3. v-if和v-for哪个优先级更高?
在vue2中,通过源码可以看出v-for的优先级是高于v-if,因此如果v-for和v-if放置同个标签内,便会在每次玄幻的过程中进行判断,增加内存开销,因此一般的做法是使用计算属性computed进行数据筛选,之后将筛选后的数据再进行绑定。
vue3中则完全相反,v-if的优先级高于v-for,所以v-if执行时,它调用的变量还不存在,便会导致异常。
实际开发中永远不要把 v-if
和 v-for
同时用在同一个元素上
4.computed 和 watch 的区别和运用的场景?
computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值,不然会获取缓存值,节约性能;
watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
运用场景:
- 当多个值的改变可能会影响到一个值得改变的场合,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
例如:当前人员数据库中有出生日期和入职日期,但是因为年龄和工龄是动态的,所以不适合做数据库存储,这个时候就可以用到 computed 进行缓存。
- 当一个值得改变可能会影响到多个值的变化的场合,应该使用 watch,
例如:子组件需要监听父组件传递过来的某个值,做子组件自身数据的改变,就可以用到 watch 进行监听。
5.谈谈你对生命周期的理解
2.X版本的生命周期中总共有四个阶段细分为八个钩子函数
其中四个阶段分别是创建阶段,挂载阶段,更新阶段和销毁阶段
八个钩子函数分别是beforeCreated() ,Created(),beforeMount(),mounted(),beforeUpdate(),update(),beforeDestroy(),destroyed()
按照官网生命周期得图示来分析的话,VUE执行上到下的执行顺序依次为
- 首先会创建一个Vue的实例对象
- 当执行beforeCreated钩子函数,在这个生命周期被执行的时候,只可以调用this上面的一些原生属性与默认事件,其他东西都还未创建。
- 当执行created钩子函数,实例完成了创建,可以调用data中的数据了也可以调用methods中的方法,在此阶段一般会配合axios进行后端接口数据的访问,进行数据的加工。
- Vue开始编译模板,会通过h函数将模板中的标签解析为内存模板字符串,之后把这个模板字符串通过patch方法渲染为虚拟DOM,此时只是在内存中渲染好了模板,并没有吧模板挂载到真正的页面中去
- 当执行beforeMount()钩子函数,调用此函数的时候模板已经在内存之中编译好了,但是尚未挂载到页面中去,因此页面还是旧的。
- 将虚拟DOM更新到浏览器的页面当中。
- 当执行mounted()钩子函数,此阶段一般会进行DOM节点得操作。
- 进行更新阶段的话,会通过模板中存在的data的改变而进行触发
- 当执行beforeUpdate()钩子函数,页面中显示的数据还是旧的,但是data中的数据是最新的,页面尚未与data中的数据保持同步,可以进一步更改状态,不会触发重复渲染问题
- 先根据data中最新的数据,渲染出一份最新的内存DOM,当最新的内存DOM被更新了之后,会把最新的内存DOM重新渲染到最新的页面中去。
- 当执行update()钩子函数,页面和data数据已经保持同步了,全都是最新的,避免更改状态,不然会重复触发渲染问题。
- 当执行beforeDestroy()钩子函数,我们可以在这时进行善后收尾工作,比如清除计时器
- 当执行destroyed()的时候,实例已经完全销毁了,此时所有的data和所有的methods,过滤器,指令等都不可使用了,所有的子实例也均会被销毁。
6.谈谈你对Vuex的理解
Vuex是实现组件全局状态管理的一种机制,可以方便的实现组件之间数据的共享。
其中共有五大属性分别是,actions,state,getters,mutations,modules
state: 相当于是存储库,里面会对数据进行缓存
getters: 其实就是相当于vue里面的计算属性,可以通过getters访问store中的内存数据,通过this.$store.getters访问
actions: 是进行异步操作的,通过this.$store.dispatch访问
mutations: 是进行同步操作的,通过this.$store.commit访问
modules: 就是进行模块化,这样会使代码更加整洁
在项目中通常通过axios返回的后端数据使this.$store.commit 往state里面存值,之后通过
this.$store.getters 取值
弊端:因为vuex相当于是本地缓存,因此当页面强制F5刷新的时候会有数据丢失的现象,因此一般配合localStorage做成持久化存储或者使用sessionStorage来进行浏览器存储解决,但是localStorage存储过多数据会导致页面变卡,因为localStorage的本质是对字符串的读取,具体使用方案还是需要根据具体项目情况具体分析。
7.谈谈你对虚拟DOM的理解
由于在浏览器中操作 DOM 是很昂贵的,如果直接使用真实DOM的话,会对性能造成比较大的浪费,因此引入了虚拟DOM这个概念,使用虚拟DOM的话,可以对比虚拟DOM和真实DOM的差异,从而进行局部渲染来达到优化性能的目的。
VUE底层对虚拟DOM的处理其实是借鉴了开源库 snabbdom 进行实现的,具体的就是使用一个函数将template模板里面的一些标签转化为js模板字符串形式的对象,之后再将新旧虚拟dom进行比对,从而达到优化性能的目的。
优点:
- 无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,vue会根据虚拟 DOM 和 数据双向绑定原理,帮我们以可预期的方式更新视图,极大提高我们的开发效率。
- 跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作。
缺点:
- 无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
- 页面初始化 DOM 的时候,由于多了一层虚拟 DOM 的计算,所以初始显示会慢一些。
如果对虚拟DOM想要深入理解的话,可以查看本文作者写的另一篇详解虚拟 DOM 的文章【Vue深入】之虚拟DOM - 掘金 (juejin.cn)
8.谈谈你对Diff算法的理解
vue底层是采用diff算法来进行新旧虚拟dom对比的,diff算法当中最主要的就是一个patch方法。
- sameVnode方法来判断是否为同一类型节点,如果两个节点都是一样的,那么就深入检查他们的子节点,如果两个节点不一样那就说明新节点完全被改变了,就可以直接替换新节点。
- patchVnode方法来判断如果是同一类型节点,则进行深层次比较,具體做了以下事情。
- 找到对应的真实dom,称为
el
- 判断
Vnode
和oldVnode
是否指向同一个对象,如果是,那么直接return
- 如果他们都有文本节点并且不相等,那么将
el
的文本节点设置为Vnode
的文本节点。 - 如果
oldVnode
有子节点而Vnode
没有,则删除el
的子节点 - 如果
oldVnode
没有子节点而Vnode
有,则将Vnode
的子节点真实化之后添加到el
- 如果两者都有子节点,则执行
updateChildren
函数比较子节点,这一步很重要
- updateChildren方法用于新旧虚拟节点的子节点对比,主要采用首尾指针法进行对比,命中顺序为新前对比旧前,新后对比旧后,新后对比旧前与新前对比旧后,入后以上均不满足则采用key做映射。
如果对DIFF算法想要深入理解的话,可以查看本文作者写的另一篇详解DIFF算法的文章【Vue深入】之DIFF算法 - 掘金 (juejin.cn)
9.谈谈你对Vue 中的 key 的理解
key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速,key的作用主要是为了更高效的更新虚拟DOM。
因为从源码中可以知道,vue在patch过程中判断两个节点是否是相同节点时key是一个必要条件,渲染一组列表时,key往往是唯一标识,所以如果不定义key的话,vue只能认为比较的两个节点是同一个,哪怕,它们实际上不是,这导致了频繁更新元素,使得整个patch过程比较低效,影响性能。
开发过程使用中在渲染一组列表时key必须设置,而且必须是唯一标识,应该避免使用数组索引作为key,因为这可能导致一些隐蔽的bug;
如果对key的作用想要深入理解的话,可以查看本文作者写的另一篇详解DIFF算法的文章【Vue深入】之DIFF算法 - 掘金 (juejin.cn)里面有vue源码对于key使用的判断。
10.谈谈你对Vue SSR 的理解
SSR 也就是服务端渲染,也就是将 Vue 在客户端把标签渲染成 HTML 的工作放在服务端完成,然后再把 html 直接返回给客户端,使用SSR主要是为了解决首屏加载速度比较慢的问题,但是他也有一定的弊端,它也会使我们的开发条件受到限制,项目中是否需要使用还是需要根据项目的具体需求进行分析判断。
SSR优点:
- 更好的 SEO: 因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax 获取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;
- 更快的内容到达时间(首屏加载更快): SPA 会等待所有 Vue 编译后的 js 文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间;
SSR缺点:
- 更多的开发条件限制: 例如服务端渲染只支持 beforCreate 和 created 两个钩子函数,这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序 SPA 不同,服务端渲染应用程序,需要处于 Node.js server 运行环境;
11.谈谈你对Vue双向绑定的理解
vue的双向绑定是使用v-model
标签进行处理的,v-model
是语法糖,默认情况下相当于:value
和@input
,一个简单的v-model
是将值绑定到:value
之后通过@input
方法再将值绑定为$event.target.value
加以实现的。
原理:vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的,原理是观察者observer通过Object.defineProperty()来劫持到各个属性的getter setter,在数据变动的时候,会被observer观察到,会通过Dep通知数据的订阅者watcher,之后进行相应的视图上面的变化。
数据劫持:
第一步:需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter
这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
第二步:compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
第三步:Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个update()方法
3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
第四步:MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果
12.谈谈你对Vue响应式原理的理解
数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制。
MVVM框架中要解决的一个核心问题是连接数据层和视图层,通过数据驱动应用,数据变化,视图更新,要做到这点的就需要对数据做响应式处理,这样一旦数据发生变化就可以立即做出更新处理。
以vue为例说明,通过数据响应式加上虚拟DOM和patch算法,开发人员只需要操作数据,关心业务,完全不用接触繁琐的DOM操作,从而大大提升开发效率,降低开发难度。
vue2.X版本的响应式主要依赖于底层得Object.defineProperty()与Object.defineReactive()组成得一个闭包环境,通过访问其中的getter和setter方法进行响应式
其中分为对象响应式和数组响应式两部分
对象响应式: 对象响应式中会将data通过observer进行代理或者拦截,获得getter和setter的能力,data中每一个属性都有一个依赖dep,每一个依赖都有若干个wachter进行监视,当data中数据改变时,通知给dep,dep再通知给wachter,wachter就从对应的data中拿到值后渲染到页面。
数组响应式: 数组响应式中会以Array.prototype为原型创建一个arrayMthods,之后使用Object.setOrototypeOf使我们自身的数组强制指向arrayMthods,其中会重写七大数组的原生方法(push,sort,shfit,unshfit,pop,splice,reverse)使这些方法可以额外的做更新通知,从而作出响应。
但vue2.x版本的响应式也具有一定的弊端,例如无法直接更改数组中对应索引得值和无法直接删除数组中的数据,因此引入了this.set()和this.set()和this.set()和this.delete()进行处理,并且由于初期化的时候会对整个data对象进行遍历以便对data的每个属性添加getter和settse,因此data数据的层级不能过深,否则会有一定的内存损耗。
为了解决这些问题,vue3重新编写了响应式的实现:利用ES6的Proxy代理需要响应化的数据,Proxy有着高达13中的代理方式并且是对整个对象进行代理,因此初始化性能和内存消耗都得到了大幅改善;另外由于响应化的实现代码抽取为独立的reactivity包,使得我们可以更灵活的使用它,第三方的扩展开发起来更加灵活了。
13.谈谈你对Vue依赖收集的理解
通过遍历所有data中的属性,使用Object.defineProperty为其添加getter和setter,在getter中每个属性会new Dep来被记录为一个依赖,每一个依赖都有若干个wachter进行监视,当data中数据改变时,通知给相应的dep,dep再通知给wachter,wachter就从对应的data中拿到值后渲染到页面。
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。
14.谈谈你对this.$nextTick的理解
因为vue存在异步更新策略,如果修改了data的某一个值,并不会立即反应到页面中,而是会开启一个队列,并缓冲在同一事件循环中发生的所有数据变更,只有在当前任务空闲时才会去执行队列任务,这就有一个延迟时间了。这一策略导致我们对数据的修改不会立刻体现在DOM上,此时如果想要获取更新后的DOM状态,就需要使用nextTick。
在Vue内部,nextTick之所以能够让我们看到DOM更新后的结果,是因为我们传入的callback会被添加到队列刷新函数(flushSchedulerQueue)的后面,这样等队列内部的更新函数都执行完毕,所有DOM操作也就结束了,callback自然能够获取到最新的DOM值。
this.$nextTick()
方法主要是用在随数据改变而改变的dom应用场景中,vue中数据和dom渲染由于是异步的,所以,要让dom结构随数据改变这样的操作都应该放this.$nextTick()
的回调函数中
15.Vue要做权限管理该怎么做?
权限管理一般需求是两个:页面权限和按钮权限。
-
具体实现的时候分后端和前端两种方案:
前端方案会把所有路由信息在前端配置,通过路由守卫要求用户登录,用户登录后根据角色过滤出路由表。比如我会配置一个
asyncRoutes
数组,需要认证的页面在其路由的meta
中添加一个roles
字段,等获取用户角色之后取两者的交集,若结果不为空则说明可以访问。此过滤过程结束,剩下的路由就是该用户能访问的页面,最后通过router.addRoutes(accessRoutes)
方式动态添加路由即可。后端方案会把所有页面路由信息存在数据库中,用户登录的时候根据其角色查询得到其能访问的所有页面路由信息返回给前端,前端再通过
addRoutes
动态添加路由信息按钮权限的控制通常会实现一个指令,例如
v-permission
,将按钮要求角色通过值传给v-permission指令,在指令的moutned
钩子中可以判断当前用户角色和按钮是否存在交集,有则保留按钮,无则移除按钮。 -
纯前端方案的优点是实现简单,不需要额外权限管理页面,但是维护起来问题比较大,有新的页面和角色需求就要修改前端代码重新打包部署;后端方案就不存在这个问题,通过专门的角色和权限管理页面,配置页面和按钮权限信息到数据库,应用每次登陆时获取的都是最新的路由信息,可谓一劳永逸!
16.谈谈你是如何解决跨域问题的
发生原因是因为前台和后端得服务器网址不一致而产生的。
通俗来讲如果前端服务器为3000,后端服务器为5000,就会发生跨域错误,前端会将请求头数据发送给后端,后端也会将响应数据返回给前端,但是响应数据会被ajax引擎拦截,因此发生跨域错误。
通常我们使用代理来解决跨域的问题。
使用代理进行跨域的原理是:将域名发送给本地的服务器(启动vue项目的服务,loclahost:8080),再由本地的服务器去请求真正的服务器。
在vue中主要采用两种方式配置代理
1.在package.json文件中配置proxy只想后端服务器(很少使用)
2.在vue根目录下得vue.config.js文件中进行配置(常用)
主要就是配置target,changOrigin和pathRewrite这三个参数
target参数指向后台得真实接口
changOrigin参数配置为true允许跨域
pathRewrite参数会重写路径
17.谈谈你有对 Vue 项目进行过哪些方面的优化
- 对象层级不要过深,否则性能就会差(object.defineproperty会进行遍历监听)
- 不需要响应式的数据不要放到 data 中(object.defineproperty会对对象的每个属性开启监听)
- v-if 和 v-show 区分使用场景
- computed 和 watch 区分使用场景
- v-for 遍历必须加 key,key 最好是 id 值,且避免同时使用 v-if
- 大数据列表和表格性能优化-虚拟列表
- 大数据下拉组件性能优化-数据懒加载
- 防止内部泄漏,组件销毁后把全局变量和事件销毁
- 路由懒加载
- 第三方插件的按需引入(不要全部引入,不然打包体积过大)
- 适当采用 keep-alive 缓存组件
- 防抖、节流运用
- 服务端渲染 SSR
五 常用手写篇
1 手写数组去重
实现一个方法使数组去重返回新数组
//利用ES6新语法set
function uniqueArr(arr) {
return [...new Set(arr)];
}
//利用indexOf
function uniqueArr(arr) {
const res = [];
for (let i = 0; i < arr.length; i++) {
if (res.indexOf(arr[i]) === -1){
res.push(arr[i]);
}
}
return res;
}
// console.log(uniqueArr([1, 2, 3, 4, 1, 2]));
2 手写数组扁平化
实现一个方法使多维数组变成一维数组
function flatter(arr) {
if (!arr.length) return;
return arr.reduce(
(pre, cur) =>
Array.isArray(cur) ? [...pre, ...flatter(cur)] : [...pre, cur],
[]
);
}
// console.log(flatter([1, 2, [1, [2, 3, [4, 5, [6]]]]]));
3. 手写数据类型判断:
数据类型判断主要可以使用以下三种方式进行判断,但是都具有一定的局限性,因此在项目实际开发过程中会重写类型判断方法。
typeof:Undefined、Boolean、Number、String、Symbol、Function 等基本数据类型,但是对于其他的都会认为是 object,例如 Null、Date 等,typeof返回的是一个变量的基本类型。
instanceof:可以准确地判断复杂引用数据类型,instanceof返回的是一个布尔值。
Object.prototype.toString.call():调用该方法,统一返回格式“[object Xxx]”的字符串。
function typeOf(obj) {
let res = Object.prototype.toString.call(obj).split(' ')[1]
res = res.substring(0, res.length - 1).toLowerCase()
return res
}
typeOf([]) // 'array'
typeOf({}) // 'object'
typeOf(new Date) // 'date'
4.手写深浅拷贝
浅拷贝
对于引用类型而言,直接通过原对象生成一个新对象,原对象和新对象共同指向堆中内存地址,这样的话更改新对象或原对象便会互相影响。
function assign2(target, ...source) {
if (target == null) {
return target
}
let ret = Object(target)
source.forEach(function(obj) {
if (obj != null) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
ret[key] = obj[key]
}
}
}
})
return ret
}
深拷贝
对于引用类型而言,我们通过深拷贝实现开辟新对象堆中自己的内存空间,这样的话更改新对象或原对象便不会互相影响。
function copyObj(obj){
var cloneObj;
//当输入数据为简单数据类型时直接复制
if(obj&&typeof obj!=='object'){cloneObj=obj;}
//当输入数据为对象或数组时
else if(obj&&typeof obj==='object'){
//检测输入数据是数组还是对象
cloneObj=Array.isArray(obj)?[]:{};
for(let key in obj){
if(obj.hasOwnProperty(key)){
if(obj[key]&&typeof obj[key]==='object') {
//若当前元素类型为对象时,递归调用
cloneObj[key] = copyObj(obj[key]);
}
//若当前元素类型为基本数据类型
else{cloneObj[key]=obj[key];}
}
}
}
return cloneObj;
}
5. 手写防抖节流:
防抖
防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于设置的时间,防抖的情况下只会调用一次,而节流的情况会每隔一定时间调用一次函数。
防抖应用场景:根据搜索内容进行匹配数据,但是如果用户快速的输入了一连串的字符,假设是10个字符,那么就会在瞬间触发了10次的请求,我们想要的是用户停止输入的时候才去触发查询的请求,这时候函数防抖可以帮到我们。 他的作用就是当持续触发的时候,函数是不执行的,等最后一次触发结束的一段时间之后再去执行。比如输入结束之后再去进行接口搜索回显数据。
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
var args = arguments;
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
节流
节流应用场景:滚动浏览器滚动条的时候,更新页面上的某些布局内容或者去调用后台的某接口查询内容,同样的,如果不对函数调用的频率加以限制的话,那么可能我们滚动一次滚动条就会产生N次的调用了。 但是这次的情况跟上面的有所不同,我们不是要在每完成等待某个时间后去执行某函数,而是要每间隔某个时间去执行某函数,避免函数的过多执行。
function throttle(func, wait) {
var context, args;
var previous = 0;
return function() {
var now = +new Date();
context = this;
args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
6. 手写 new 关键字
new 运算符用来创建用户自定义的对象类型的实例或者具有构造函数的内置对象的实例。
function myNew(fn, ...args) {
//创建一个新的对象
const obj = {};
//新对象的__proto__属性指向构造函数的原型对象
obj.__proto__ = fn.prototype;
//将构造函数的作用域赋值给新对象(也就是this对象指向新对象)
let result = fn.apply(obj, args)
//返回新对象
return typeof result === "object" ? result : obj
}
7. 手写函数原型三大方法:
call
call使用一个指定的 this 值和一个或多个参数来调用一个函数。
实现要点:this 可能传入 null;传入不固定个数的参数;函数可能有返回值;
Function.prototype.call2 = function (context) {
var context = context || window;
context.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
var result = eval('context.fn(' + args +')');
delete context.fn
return result;
}
apply
apply 和 call 一样,唯一的区别就是 call 是传入不固定个数的参数,而 apply 是传入一个数组。
实现要点:this 可能传入 null;传入一个数组;函数可能有返回值;
Function.prototype.apply2 = function (context, arr) {
var context = context || window;
context.fn = this;
var result;
if (!arr) {
result = context.fn();
} else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
result = eval('context.fn(' + args + ')')
}
delete context.fn
return result;
}
bind
bind 方法会创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
实现要点:bind() 除了 this 外,还可传入多个参数;bing 创建的新函数可能传入多个参数;新函数可能被当做构造函数调用;函数可能有返回值;
Function.prototype.bind2 = function (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {};
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
8. 手写Promise
Promise.all
Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。
Promise.all = function(promiseArr) {
let index = 0, result = []
return new Promise((resolve, reject) => {
promiseArr.forEach((p, i) => {
Promise.resolve(p).then(val => {
index++
result[i] = val
if (index === promiseArr.length) {
resolve(result)
}
}, err => {
reject(err)
})
})
})
}
Promise.race
Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。
Promise.race = function(promiseArr) {
return new Promise((resolve, reject) => {
promiseArr.forEach(p => {
Promise.resolve(p).then(val => {
resolve(val)
}, err => {
rejecte(err)
})
})
})
}