程序员的资源宝库

网站首页 > gitee 正文

列表页

sanyeah 2024-04-25 18:46:37 gitee 89 ℃ 0 评论

2022秋招面试题目记录,参考文章有:

小林coding (xiaolincoding.com)

面试官:双向数据绑定是什么 | web前端面试 - 面试官系列 (vue3js.cn)

声明·感谢: | 忙 · 南易 (lmongo.com)

web前端面试总结(自认为还算全面哈哈哈哈哈!!!!) - 掘金 (juejin.cn)

57code/vue-interview: 总结前端面试中经典的vue相关题目,分析最佳回答策略。加面试群关注公众号”村长学前端“。 (github.com)

Vue 面试小结1_51CTO博客_vue 面试

2万字 | 前端基础拾遗90问 - 掘金 (juejin.cn)

yayxs/front-end-interview-questions: 前 端 面 试 题 2.0 (github.com)

ES6 · 前端躬行记 · 看云 (kancloud.cn)

TypeScript TS「面试题及答案」不断更新 - 掘金 (juejin.cn)

牛客网 - 找工作神器|笔试题库|面试经验|实习招聘内推,求职就业一站解决_牛客网 (nowcoder.com)

前端常见面试题总结 | 大厂面试题每日一题 (shanyue.tech)

前端工程化实践 | 山月行 (shanyue.tech)

【建议星星】要就来45道Promise面试题一次爽到底(1.1w字用心整理) - 掘金 (juejin.cn)

前端面试题整理_野槐的博客-CSDN博客

CSS3

1、介绍一下标准的CSS的盒子模型?与低版本IE的盒子模型(怪异盒子模型)有什么不同的

  • 标准盒子模型:宽度=内容的宽度(content)+ border(边距) + padding(内边距) + margin(内边距)
  • 怪异盒子模型:宽度=内容宽度(content+border+padding)+ margin

1、margin 和 padding 分别适合什么场景使用?

margin是用来隔开元素与元素的间距;padding是用来隔开元素与内容的间隔。
margin用于布局分开元素使元素与元素互不相干。
padding用于元素与内容之间的间隔,让内容(文字)与(包裹)元素之间有一段距离。

何时应当使用margin:
?需要在border外侧添加空白时。
?空白处不需要背景(色)时。
?上下相连的两个盒子之间的空白,需要相互抵消时。如15px+20px的margin,将得到20px的空白。

何时应当时用padding:
?需要在border内测添加空白时。
?空白处需要背景(色)时。
?上下相连的两个盒子之间的空白,希望等于两者之和时。如15px+20px的padding,将得到35px的空白。

2、什么是margin重叠

回答:

margin重叠指的是在垂直方向上,两个相邻元素的margin发生重叠的情况。

一般来说可以分为四种情形:

第一种是相邻兄弟元素的marin-bottom和margin-top的值发生重叠。这种情况下我们可以通过设置其中一个元素为BFC
来解决。

第二种是父元素的margin-top和子元素的margin-top发生重叠。它们发生重叠是因为它们是相邻的,所以我们可以通过这
一点来解决这个问题。我们可以为父元素设置border-top、padding-top值来分隔它们,当然我们也可以将父元素设置为BFC
来解决。

第三种是高度为auto的父元素的margin-bottom和子元素的margin-bottom发生重叠。它们发生重叠一个是因为它们相
邻,一个是因为父元素的高度不固定。因此我们可以为父元素设置border-bottom、padding-bottom来分隔它们,也可以为
父元素设置一个高度,max-height和min-height也能解决这个问题。当然将父元素设置为BFC是最简单的方法。

第四种情况,是没有内容的元素,自身的margin-top和margin-bottom发生的重叠。我们可以通过为其设置border、padding或者高度来解决这个问题。

3、box-sizing

box-sizing属性主要用来控制元素盒模型的解析模式。默认值是 content-box。

content-box让元素维持W3C的标准盒模型。元素的宽度/高度由 border+ padding+content的宽度/高度决定,设置 width/height属性指的是指定 content部分的宽度/高度。

border-box让元素维持IE传统盒模型(IE6以下版本和IE6、IE7的怪异模式)。设置 width/height属性指的是指定 border+ padding+ content的宽度/高度。

标准浏览器下,按照W3C规范解析盒模型。一旦修改了元素的边框或内距,就会影响元素的盒子尺寸,就不得不重新计算元素的盒子尺寸,从而影响整个页面的布局。

2、CSS 中哪些属性可以继承?

每一个属性在定义中都给出了这个属性是否具有继承性,一个具有继承性的属性会在没有指定值的时候,会使用父元素的同属性的值
来作为自己的值。

一般具有继承性的属性有,字体相关的属性,font-size和font-weight等。文本相关的属性,color和text-align等。
表格的一些布局属性、列表属性如list-style等。还有光标属性cursor、元素可见性visibility。

当一个属性不是继承属性的时候,我们也可以通过将它的值设置为inherit来使它从父元素那获取同名的属性值来继承。

3、CSS选择器的优先级是怎样的?

选择器

  • id选择器(#myid)
  • 类选择器(.myclass)
  • 属性选择器(a[rel=“external”])
  • 伪类选择器(a:hover, li:nth-child)
  • 标签选择器(div, h1,p)
  • 相邻选择器(h1 + p)
  • 子选择器(ul > li)
  • 后代选择器(li a)
  • 通配符选择器(*)

判断优先级时,首先我们会判断一条属性声明是否有权重,也就是是否在声明后面加上了!important。一条声明如果加上了权重,
那么它的优先级就是最高的,前提是它之后不再出现相同权重的声明。如果权重相同,我们则需要去比较匹配规则的特殊性。

CSS选择器的优先级是:内联 > ID选择器 > 类选择器 > 标签选择器

到具体的计算层面,优先级是由 A 、B、C、D 的值来决定的,其中它们的值计算规则如下:

  • A 的值等于 1 的前提是存在内联样式, 否则 A = 0;
  • B 的值等于 ID选择器 出现的次数;
  • C 的值等于 类选择器 和 属性选择器 和 伪类 出现的总次数;
  • D 的值等于 标签选择器 和 伪元素 出现的总次数 。

就比如下面的选择器,它不存在内联样式,所以A=0,不存在id选择器B=0,存在一个类选择器C=1,存在三个标签选择器D=3,那么最终计算结果为: {0, 0, 1 ,3}

ul ol li .red {
    ...
}

按照这个结算方式,下面的计算结果为: {0, 1, 0, 0}

#red {

}

我们的比较优先级的方式是从A到D去比较值的大小,A、B、C、D权重从左到右,依次减小。判断优先级时,从左到右,一一比较,直到比较出最大值,即可停止。

比如第二个例子的B与第一个例子的B相比,1>0,接下来就不需要比较了,第二个选择器的优先级更高。

4、伪类和伪元素的区别是什么?

是什么?

伪类(pseudo-class) 是一个以冒号(:)作为前缀,被添加到一个选择器末尾的关键字,当你希望样式在特定状态下才被呈现到指定的元素时,你可以往元素的选择器后面加上对应的伪类。

伪元素用于创建一些不在文档树中的元素,并为其添加样式。比如说,我们可以通过::before来在一个元素前增加一些文本,并为这些文本添加样式。虽然用户可以看到这些文本,但是这些文本实际上不在文档树中。

区别

其实上文已经表达清楚两者区别了,伪类是通过在元素选择器上加入伪类改变元素状态,而伪元素通过对元素的操作进行对元素的改变。

我们通过p::before对这段文本添加了额外的元素,通过 p:first-child改变了文本的样式。

::before 和 :after中双冒号和单冒号有什么区别?解释一下这2个伪元素的作用

  1. 单冒号(:)用于CSS3伪类,双冒号(::)用于CSS3伪元素。
  2. ::before就是以一个子元素的存在,定义在元素主体内容之前的一个伪元素。并不存在于dom之中,只存在在页面之中。
  3. 伪类和伪元素都不出现在源?件和DOM树中。也就是说在html源?件中是看不到伪类和伪元素的。 不同之处: 伪类其实就是基于普通DOM元素?产?的不同状态,他是DOM元素的某?特征。 伪元素能够创建在DOM树中不存在的抽象对象,?且这些抽象对象是能够访问到的。

5、BFC/IFC

块元素和行内元素有哪些

span设定了margin-top:20px之后上方会不会有20px的间隙(不会):

img和span都是行内元素,他们的默认基线都是以底部对齐的方式呈现,如字母文字,都是如此。当你在span设置inline-block的时候,这时它是行内块元素,就可以设置margin,但是它还是有行元素的性质,也就是说基线底部对齐,为了解决这个问题你可以改变它的默认对齐方式,譬如vertical-align:top;,或者让它浮动,都可以解决这个问题。

行内元素与块级元素直观上的区别二、行内元素与块级元素的三个区别

1.行内元素会在一条直线上排列(默认宽度只与内容有关),都是同一行的,水平方向排列。

块级元素各占据一行(默认宽度是它本身父容器的100%(和父元素的宽度一致),与内容无关),垂直方向排列。块级元素从新行开始,结束接着一个断行。

2.块级元素可以包含行内元素和块级元素。行内元素不能包含块级元素,只能包含文本或者其它行内元素。

3.行内元素与块级元素属性的不同,主要是盒模型属性上:行内元素设置width无效,height无效(可以设置line-height),margin上下无效,padding上下无效

行内元素和块级元素转换

display:block; (字面意思表现形式设为块级元素)

display:inline; (字面意思表现形式设为行内元素)

inline-block

inline-block 的元素(如input、img)既具有 block 元素可以设置宽高的特性,同时又具有 inline 元素默认不换行的特性。当然不仅仅是这些特性,比如 inline-block 元素也可以设置 vertical-align(因为这个垂直对齐属性只对设置了inline-block的元素有效) 属性。

HTML 中的换行符、空格符、制表符等合并为空白符,字体大小不为 0 的情况下,空白符自然占据一定的宽度,使用inline-block 会产生元素间的空隙。

什么是块级元素?

总是在新行上开始;

高度,行高以及外边距和内边距都可控制;

宽度缺省是它的容器的100%,除非设定一个宽度。

它可以容纳内联元素和其他块元素

例如:<address>、<center>、<h1>~<h6>、<hr>、<p>、<pre>、<ul>、<ol>、<dl>、<table>、<div>、<form>

什么是行内元素?

和其他元素都在一行上;

高,行高及外边距和内边距不可改变;

宽度就是它的文字或图片的宽度,不可改变

内联元素只能容纳文本或者其他内联元素

例如:<span>、<a>、<br>、<b>、<strong>、<img>、<input>、<textarea>、<select>、<sup>、<sub>、<em>、<del>

元素之间相互的影响,导致了意料之外的情况,这里就涉及到BFC概念

BFC(Block Formatting Context),即块级格式化上下文,它是页面中的一块渲染区域,并且有一套属于自己的渲染规则:

  • BFC(Block Formatting Context)叫做“块级格式化上下文"

    • 几个特征

      1. 内部的盒子会在垂直方向,一个个地放置;
      2. 盒子垂直方向的距离由margin决定,属于同一个BFC的两个相邻Box的上下margin会发生重叠;
      3. 每个元素的左边,与包含的盒子的左边相接触,即使存在浮动也是如此;
      4. BFC的区域不会与float重叠;
      5. BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素,反之也如此;
      6. 计算BFC的高度时,浮动元素也参与计算。
    • 触发条件

      1. float的属性不为none;

      2. position为absolute或fixed;

      3. display为inline-block,table-cell,table-caption,flex;

      4. overflow不为visible overflow: auto或overflow: hidden

  • 应用场景

    • 防止margin重叠(塌陷)
    • 清除内部浮动
    • 自适应多栏布局

IFC(inline Formatting Context)叫做“行级格式化上下”

  • 几个特征
    1. 内部的盒子会在水平方向,一个个地放置;
    2. IFC的高度,由里面最高盒子的高度决定;
    3. 一行不够放置的时候会自动切换到下一行;

6、display 有哪些值?说明他们的作用。

display作用:display 属性规定元素应该生成的框的类型。
值	     描述
block	块类型。默认宽度为父元素宽度,可设置宽高,换行显示。
none	元素不显示,并从文档流中移除。
inline	行内元素类型。默认宽度为内容宽度,不可设置宽高,同行显示。
inline-block 默认宽度为内容宽度,可以设置宽高,同行显示。---设置为行内块级元素
list-item	像块类型元素一样显示,并添加样式列表标记。
table	此元素会作为块级表格来显示。
inherit	规定应该从父元素继承display属性的值。
flex	此盒子会采用弹性盒子布局
grid




(overflow属性指定如果内容溢出一个元素的框,会发生什么。)
值	描述
visible	默认值。内容不会被修剪,会呈现在元素框之外。
hidden	内容会被修剪,并且其余内容是不可见的。
scroll	内容会被修剪,但是浏览器会显示滚动条以便查看其余的内容。
auto	如果内容被修剪,则浏览器会显示滚动条以便查看其余的内容。
inherit	规定应该从父元素继承 overflow 属性的值。

7、position的值?

  • static: 正常文档流定位,此时 top, right, bottom, left 和 z-index 属性无效,块级元素从上往下纵向排布,行级元素从左向右排列。
  • relative:相对定位,此时的『相对』是相对于正常文档流的位置。
  • absolute:相对于最近的非 static 定位祖先元素的偏移,来确定元素位置,比如一个绝对定位元素它的父级、和祖父级元素都为relative,它会相对他的父级而产生偏移。
  • fixed:指定元素相对于屏幕视口(viewport)的位置来指定元素位置。元素的位置在屏幕滚动时不会改变,比如那种回到顶部的按钮一般都是用此定位方式。-----------完全脱离文档流,相对于浏览器窗口(html)进行定位
  • sticky:粘性定位,特性近似于relative和fixed的合体,其在实际应用中的近似效果就是IOS通讯录滚动的时候的『顶屁股』。

position 的值 relative 和 absolute 定位原点是?

relative定位的元素,是相对于元素本身的正常位置来进行定位的。

absolute定位的元素,是相对于它的第一个position值不为static的祖先元素的padding box来进行定位的。这句话
我们可以这样来理解,我们首先需要找到绝对定位元素的一个position的值不为static的祖先元素,然后相对于这个祖先元
素的padding box来定位,也就是说在计算定位距离的时候,padding的值也要算进去。

8、浮动

为什么要设置浮动:float

在进行网页布局时往往需要为各部分进行定位,在CSS中有一种非常方便的方法,那就是“浮动float”

浮动元素会脱离文档流并向左/向右浮动,直到碰到父元素或者另一个浮动元素。

一、文档流是啥?
HTML中全部元素都是盒模型,盒模型占用一定空间,将窗体自上而下分成一行一行,并且在每行中按照从左往右依次排放元素,称为文档流。

二、什么是脱离文档流?
元素脱离文档流之后就不再在文档流中占据空间,而是处于浮动状态,相当于漂浮在其他元素上方,那么其他元素就会忽略该元素并填补这个元素原来的空间

被浮动的元素可以内联排列。

浮动元素脱离了文档流,并不占据文档流的位置,自然父元素也就不能被撑开,所以没了高度

有三个div块,它们长宽颜色不一,依次是红、黄、绿。如图(初始状态1.1)。

我们在第一个色块(红色)上添加左浮动 “float:left;” 我们将会看到黄色色块消失,绿色色块减半。 红色色块被添加了浮动样式,那么它将从文档流中“飘出去”,而它原本所占据的高度塌陷,排在它后面的色块将会占据它的位置。

如果同时在红黄绿三个色块上加左浮动

因为是左浮动,所以从左到右依次排列,如果设置右浮动,那么红色将靠最右,往左依次是黄、绿

我们只在黄色和绿色上面添加浮动效果,红色无浮动。

黄色是左浮动状态,如果它的前方(红色)也是左浮动状态,那么黄色将会左飘到红色的右侧。但如果红色不是浮动状态,那么黄色将被死死顶着,无法向上飘动。

设置元素浮动后,该元素的display值是多少?

  • 自动变成display:block

清除浮动有哪些方法?

浮动元素可以左右移动,直到遇到另一个浮动元素或者遇到它外边缘的包含框。浮动框不属于文档流中的普通流,当元素浮动之后,
不会影响块级元素的布局,只会影响内联元素布局。此时文档流中的普通流就会表现得该浮动框不存在一样的布局模式。当包含框
的高度小于浮动框的时候,此时就会出现“高度塌陷”。

清除浮动是为了清除使用浮动元素产生的影响。浮动的元素,高度会塌陷,而高度的塌陷使我们页面后面的布局不能正常显示。

清除浮动的方式:

1. clear清除浮动

clear:both 不允许元素的左边或右边挨着浮动元素,底层原理是在被清除浮动的元素上边或者下边添加足够的清除空间。

<div style="clear:both;"></div>

2. 利用伪元素清除浮动(现代浏览器clearfix方案,最佳实践方案)

// clearfix方案,不支持IE6/7
.clearfix:after {
    display: table;
    content: " ";
    clear: both;
}

// 引入了zoom以支持IE6/7
.clearfix:after {
    display: table;
    content: " ";
    clear: both;
}
.clearfix{
    *zoom: 1;
}

// 最佳实践方案
// 加入:before以解决现代浏览器上边距折叠的问题
.clearfix:before,
.clearfix:after {
    display: table;
    content: " ";
}
.clearfix:after {
    clear: both;
}
.clearfix{
    *zoom: 1;
}

**3.overflow: auto或overflow: hidden方法,使用BFC **父元素CSS添加:overflow:auto;

  1. BFC容器是一个隔离的容器,和其他元素互不干扰;所以我们可以用触发两个元素的BFC来解决垂直边距折叠问题。
  2. BFC可以包含浮动;通常用来解决浮动父元素高度坍塌的问题。

子元素浮动之后如何撑开父元素(清除浮动和其他方法)

按照CSS规范,浮动元素(float)后会被移出文档流,不会影响到块状盒子的布局而只会影响内联盒子(通常是文本)的排列,所以不会撑开父元素的高度。但是很多时候我们需要页面根据内容自动调整高度的。如何解决这个问题呢?

有6个方法:

(1)在浮动子元素后面添加<div style="clear:both;"></div>

(2)父元素CSS添加z-index:1; overflow:hidden;

(3)绝对定位/静止定位(absolute/fixed)。

(4)父元素也跟着浮动。

(5)父元素设定高度。这个是掩耳盗铃的方法,实质上并没有解决问题,但效果看起来是一样的,如果父元素高度固定,可以用这个方法。

(6)父元素CSS添加:overflow:auto;_height:1%; 这个是我认为是最佳方案。

9、'display'、'position'和'float'的相互关系?

(1)首先我们判断display属性是否为none,如果为none,则position和float属性的值不影响元素最后的表现。

(2)然后判断position的值是否为absolute或者fixed,如果是,则float属性失效,并且display的值应该被
设置为table或者block,具体转换需要看初始转换值。

(3)如果position的值不为absolute或者fixed,则判断float属性的值是否为none,如果不是,则display
的值则按上面的规则转换。注意,如果position的值为relative并且float属性的值存在,则relative相对
于浮动后的最终位置定位。

(4)如果float的值为none,则判断元素是否为根元素,如果是根元素则display属性按照上面的规则转换,如果不是,
则保持指定的display属性值不变。

总的来说,可以把它看作是一个类似优先级的机制,"position:absolute"和"position:fixed"优先级最高,有它存在
的时候,浮动不起作用,'display'的值也需要调整;其次,元素的'float'特性的值不是"none"的时候或者它是根元素
的时候,调整'display'的值;最后,非根元素,并且非浮动元素,并且非绝对定位的元素,'display'特性值同设置值。

11、说一下流式布局与响应式布局

响应式布局

  1. 响应式布局就是一个网站能够兼容多个终端——而不是为每个终端做一个特定的版本。这个概念是为解决移动互联网浏览而诞生的。比如媒体查询

    • 优点
      1. 面对不同分辨率设备灵活性强
      2. 能够快捷解决多设备显示适应问题
    • 缺点
      1. 不能完全兼容所有浏览器,代码累赘,会出现隐藏无用的元素,加载时间加长
      2. 一定程度上改变了网站原有的布局结构,会出现用户混淆的情况。

设计方法

媒体查询+流式布局。通常使用 @media 媒体查询 和网格系统 (Grid System) 配合相对布局单位进行布局,实际上就是综合响应式、流动等上述技术通过 CSS 给单一网页不同设备返回不同样式的技术统称。

优点:适应pc和移动端,如果足够耐心,效果完美。

缺点:(1)媒体查询是有限的,也就是可以枚举出来的,只能适应主流的宽高。(2)要匹配足够多的屏幕大小,工作量不小,设计也需要多个版本。

响应式页面在头部会加上这一段代码:
<meta name="applicable-device" content="pc,mobile">
<meta http-equiv="Cache-Control" content="no-transform ">

你对媒体查询的理解?

是什么

媒体查询由一个可选的媒体类型和零个或多个使用媒体功能的限制了样式表范围的表达式组成,例如宽度、高度和颜色。媒体查询,添加自CSS3,允许内容的呈现针对一个特定范围的输出设备而进行裁剪,而不必改变内容本身,非常适合web网页应对不同型号的设备而做出对应的响应适配。

如何使用

媒体查询包含一个可选的媒体类型和,满足CSS3规范的条件下,包含零个或多个表达式,这些表达式描述了媒体特征,最终会被解析为true或false。如果媒体查询中指定的媒体类型匹配展示文档所使用的设备类型,并且所有的表达式的值都是true,那么该媒体查询的结果为true.那么媒体查询内的样式将会生效。

<!-- link元素中的CSS媒体查询 -->
<link rel="stylesheet" media="(max-width: 800px)" href="example.css" />

<!-- 样式表中的CSS媒体查询 -->
<style>
@media (max-width: 600px) {
  .facet_sidebar {
    display: none;
  }
}
</style>

流式布局

  1. 流式布局就是页面中元素的宽度按照屏幕分辨率自动进行适配调整,它可以保证当前屏幕分辨率发生改变的时候,页面中的元素大小也可以跟着改变,所以流式布局是移动端开发常用的一种布局

设计方法

使用%百分比定义宽度,高度大都是用px来固定住,可以根据可视区域 (viewport) 和父元素的实时尺寸进行调整,尽可能的适应各种分辨率。往往配合 max-width/min-width 等属性控制尺寸流动范围以免过大或者过小影响阅读。

这种布局方式在Web前端开发的早期历史上,用来应对不同尺寸的PC屏幕(那时屏幕尺寸的差异不会太大),在当今的移动端开发也是常用布局方式,但缺点明显:主要的问题是如果屏幕尺度跨度太大,那么在相对其原始设计而言过小或过大的屏幕上不能正常显示。因为宽度使用%百分比定义,但是高度和文字大小等大都是用px来固定,所以在大屏幕的手机下显示效果会变成有些页面元素宽度被拉的很长,但是高度、文字大小还是和原来一样(即,这些东西无法变得“流式”),显示非常不协调

12、如何理解层叠上下文?

是什么

层叠上下文是HTML元素的三维概念,这些HTML元素在一条假想的相对于面向(电脑屏幕的)视窗或者网页的用户的z轴上延伸,HTML元素依据其自身属性按照优先级顺序占用层叠上下文的空间。

如何产生

触发一下条件则会产生层叠上下文:

  • 根元素 (HTML),
  • z-index 值不为 "auto"的 绝对/相对定位,
  • 一个 z-index 值不为 "auto"的 flex 项目 (flex item),即:父元素 display: flex|inline-flex,
  • opacity 属性值小于 1 的元素(参考 the specification for opacity),
  • transform 属性值不为 "none"的元素,
  • mix-blend-mode 属性值不为 "normal"的元素,
  • filter值不为“none”的元素,
  • perspective值不为“none”的元素,
  • isolation 属性被设置为 "isolate"的元素,
  • position: fixed
  • 在 will-change 中指定了任意 CSS 属性,即便你没有直接指定这些属性的值(参考 这篇文章)
  • -webkit-overflow-scrolling 属性被设置 "touch"的元素

13、如何理解z-index?

CSS 中的z-index属性控制重叠元素的垂直叠加顺序,默认元素的z-index为0,我们可以修改z-index来控制元素的图层位置,而且z-index只能影响设置了position值的元素。

我们可以把视图上的元素认为是一摞书的层叠,而人眼是俯视的视角,设置z-index的位置,就如同设置某一本书在这摞书中的位置。

  • 顶部: 最接近观察者
  • ...
  • 3 层
  • 2 层
  • 1 层
  • 0 层 默认层
  • -1 层
  • -2 层
  • -3 层
  • ...
  • 底层: 距离观察者最远

14、CSS3有哪些新特性?

  1. 颜色----

    RGBA和透明度

  2. 背景-----

    background-image background-origin(content-box/padding-box/border-box) background-size background-repeat

  3. 文字-----

    word-wrap(对长的不可分割单词换行)word-wrap:break-wor

    文字阴影:text-shadow: 5px 5px 5px #FF0000;(水平阴影,垂直阴影,模糊距离,阴影颜色)

    font-face属性:定义自己的字体

  4. 边框----

    圆角(边框半径):border-radius 属性用于创建圆角

    边框图片:border-image: url(border.png) 30 30 round

    盒阴影:box-shadow: 10px 10px 5px #888888

  5. 媒体查询:定义两套css,当浏览器的尺寸变化时会采用不同的属性

  6. transition 过渡

    transition: CSS属性,花费时间,效果曲线(默认ease),延迟时间(默认0)
    
  7. transform 转换:transform属性允许你旋转,缩放,倾斜或平移给定元素

  8. animation 动画:做一个预设的动画。和一些页面交互的动画效果,结果和过渡应该一样,让页面不会那么生硬

  9. 新的选择器

15、Animation与 Transition的异同是什么?

Animation与 Transition的功能相同,都是通过改变元素的属性值来实现动画效果的。它们的区别在于,使用 Transition的功能时只能用指定属性的开始值和结束值,然后在这两个属性值之间使用平滑过渡的方式实现动画效果,因此不能实现比较复杂的动画效果。

Animation功能通过定义多个关键帧,以及定义每个关键帧中元素的属性值来实现更为复杂的动画效果。

Animation属性值有哪些?

两个必要属性如下。

animation-name,即动画名称。

animation- duration,即动画持续时间。

其他属性值如下。

animation- play-state,即播放状态( running表示播放, paused表示暂停),可以用来控制动画暂停。

animation- timing- function,即动画运动形式。

animation- delay,即动画延迟时间。

mation-iteration- count,即重复次数。

animation-direction,即播放前重置( alternate动画直接从上一次停止的位置开始执行)

Transition属性值有哪些?必要属性有

transition属性是一个简写属性,用于设置以下4个过渡属性。

transition- property,哪个属性需要实现过渡

transition- duration,完成过渡效果需要多少秒/毫秒

transition- timing- function,速度效果的运动曲线,如 linear、ease-in、ease、ease-out、 ease-in-out, cube-bezier。

transition- delay,规定过渡开始前的延迟时间。

transition的默认值为all 0 ease 0;所以我们在书写CSS代码的时候也要按照这种顺序来。

16、有哪些方式(CSS)可以隐藏页面元素?

  • opacity:0:本质上是将元素的透明度将为0,就看起来隐藏了,但是依然占据空间且可以交互
  • visibility:hidden: 与上一个方法类似的效果,占据空间,但是不可以交互了
  • overflow:hidden: 这个只隐藏元素溢出的部分,但是占据空间且不可交互
  • display:none: 这个是彻底隐藏了元素,元素从文档流中消失,既不占据空间也不交互,也不影响布局
  • z-index:-9999: 原理是将层级放到底部,这样就被覆盖了,看起来隐藏了
  • transform: scale(0,0): 平面变换,将元素缩放为0,但是依然占据空间,但不可交互

display:none与visibility:hidden的区别?

  • display:none 不显示对应的元素,在文档布局中不再分配空间(回流+重绘)
  • visibility:hidden 隐藏对应元素,在文档布局中仍保留原来的空间(重绘)

关于display: nonevisibility: hiddenopacity: 0的区别,如下表所示:

display: none visibility: hidden opacity: 0
页面中 不存在 存在 存在
重排 不会 不会
重绘 不一定
自身绑定事件 不触发 不触发 可触发
transition 不支持 支持 支持
子元素可复原 不能 不能
被遮挡的元素可触发事件 不能

17、回流/重排(reflow)和重绘(repaint)

HTML中,每个元素都可以理解成一个盒子,在浏览器解析过程中,会涉及到回流与重绘:

  • 回流:布局引擎会根据各种样式计算每个盒子在页面上的大小与位置
  • 重绘:当计算好盒模型的位置、大小及其他属性后,浏览器根据每个盒子特性进行绘制

具体的浏览器解析渲染机制如下所示:

  • 解析HTML,生成DOM树,解析CSS,生成CSSOM树
  • 将DOM树和CSSOM树结合,生成渲染树(Render Tree)
  • Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
  • Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
  • Display:将像素发送给GPU,展示在页面上

在页面初始渲染阶段,回流不可避免的触发,可以理解成页面一开始是空白的元素,后面添加了新的元素使页面布局发生改变

当我们对 DOM 的修改引发了 DOM几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性,然后再将计算的结果绘制出来

当我们对 DOM的修改导致了样式的变化(colorbackground-color),却并未影响其几何属性时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式,这里就仅仅触发了重绘

  • 回流/重排(大):意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树;
  • 重绘(小):意味着元素发生的改变只影响了节点的一些样式(背景色,边框颜色,文字颜色等),只需要应用新样式绘制这个元素就可以了;
  • 区别

他们的区别很大: 回流必将引起重绘,而重绘不一定会引起回流。比如:只有颜色改变的时候就只会发生重绘而不会引起回流 当页面布局和几何属性改变时就需要回流 比如:添加或者删除可见的DOM元素,元素位置改变,元素尺寸改变——边距、填充、边框、宽度和高度,内容改变

  • 回流的几种优化方式
  1. 如果想设定元素的样式,通过改变元素的 class 类名 (尽可能在 DOM 树的最里层)

  2. 避免设置多项内联样式

  3. 应用元素的动画,使用 position 属性的 fixed 值或 absolute 值(如前文示例所提)

  4. 避免使用 table 布局,table 中每个元素的大小以及内容的改动,都会导致整个 table 的重新计算

  5. 对于那些复杂的动画,对其设置 position: fixed/absolute,尽可能地使元素脱离文档流,从而减少对其他元素的影响

  6. 使用css3硬件加速,可以让transformopacityfilters这些动画不会引起回流重绘

  7. 避免使用 CSS 的 JavaScript 表达式

  8. 在使用 JavaScript 动态插入多个节点时, 可以使用DocumentFragment. 创建后一次插入. 就能避免多次的渲染性能

  9. 我们还可以通过通过设置元素属性display: none,将其从页面上去掉,然后再进行后续操作,这些后续操作也不会触发回流与重绘,这个过程称为离线操作

18、CSS 单位 总共列了 17 个单位

  • % 百分比、cm 厘米、mm 毫米、px 像素(计算机屏幕上的一个点)、in 英寸、pt 磅 rgb(x,x,x) rgb(x%,x%,x%) #rrggbb(十六进制)
  • em:1em 等于当前字体尺寸(继承父元素的字体尺寸
  • rem:r 为 root,1rem 等于根元素字体尺寸(继承 html 的字体尺寸)
  • vh:1vh 等于可视窗口高度的 1/100
  • vw: 1vw 等于可视窗口宽度的 1/100
  • vmin:可视窗口宽高更小的值的 1/100
  • vmax:可视窗口宽高更大的值的 1/100
  • ex:当前字体的一个 x-height,一般为当前字体的一个 em 的一半,因为一个 'x' 字母一般为字体大小的一半
  • ch:设置 width:40ch 表示这个宽度可以容纳 40 个特定字体的字符

em\px\rem区别?

  • px:绝对单位,页面按精确像素展示。
  • em:相对单位,基准点为父节点字体的大小,如果自身定义了font-size按自身来计算(浏览器默认字体是16px),整个页面内1em不是一个固定的值。
  • rem:相对单位,可理解为”root em”, 相对根节点html的字体大小来计算,CSS3新加属性,chrome/firefox/IE9+支持

rem 布局的优缺点

  • 相对于 em 的好处是:不会发生逐渐增大或者减小字体尺寸的情况,因为始终继承根元素的字体尺寸; rem 单位不仅可应用于字体大小,还可以用于设定宽高等其他大小,使页面可以适配不同屏幕尺寸。
  • rem 一般只用于移动端。

19、有一个高度自适应的 div,里面有两个 div,一个高度 100px,希望另一个填满剩下的高度。

(1)外层div使用position:relative;高度要求自适应的div使用position:absolute;top:100px;bottom:0;
left:0;right:0;

(2)使用flex布局,设置主轴为竖轴,第二个div的flex-grow为1。

20、用纯CSS创建一个三角形的原理是什么?

  1. 把元素的宽度、高度设为0。

  2. 设置边框样式。

    width: 0;
    height: 0;
    border-top: 40px solid transparent;
    border-left: 40px solid transparent;
    border-right: 40px solid transparent;
    border-bottom: 40px solid #ff0000;
    

21、两栏布局

22、三栏布局

23、css li 冒泡

24、如何实现单行/多行文本溢出的省略样式?

25、视差滚动效果

26、CSS实现水平垂直居中的几种方法

  1. 绝对定位元素的居中实现

    .center-vertical{
        width: 100px;
        height: 100px;
        position: absolute;
        top: 50%;
        left: 50%;
        margin-top: -50px; /*高度的一半*/
        margin-left: -50px; /*宽度的一半*/
    }
    /*
        优点:兼容性好
        缺点:需要提前知道元素的尺寸。如果不知道元素尺寸,这个时候就需要JS获取了。
    */
    /*CSS3.0的兴起,使这个问题有了更好的解决方法,就是使用 transform 代替 margin */
    .content{
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate( -50%, -50%);
    }
    /*
        优点:无论绝对定位元素的尺寸是多少,它都是水平垂直居中显示的。
        缺点:就是兼容性问题。
    */
    
  2. margin: auto;实现绝对定位元素的居中

    .center-vertical{
        width: 100px;
        height: 100px;
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        margin: auto;
    }
    /*注意:上下左右均为 0 位置绝对定位。margin: auto;*/
    
  3. CSS3.0弹性布局

    .parent-element{
        display: flex;
        align-items: center;/*定义父元素垂直居中*/
        justify-content: center;/*定义父元素水平居中*/
    }
    
  4. display:table实现

    .parent{
        width: 300px;
        height: 300px;
        background: orange;
        text-align: center;
        display: table;
    }
    .child{
        display: table-cell;
        background-color: yellow;
        vertical-align: middle;
    }
    

27、请解释一下CSS3的flexbox(弹性盒布局模型),以及适用场景?

  • 该布局模型的目的是提供一种更加高效的方式来对容器中的条目进行布局、对齐和分配空间。在传统的布局方式中,block 布局是把块在垂直方向从上到下依次排列的;而 inline 布局则是在水平方向来排列。弹性盒布局并没有这样内在的方向限制,可以由开发人员自由操作。
  • 试用场景:弹性布局适合于移动前端开发,在Android和ios上也完美支持。
  1. CSS3弹性盒布局的理解: web应用有不同设备尺寸和分辨率,这时需要响应式界面设计来满足复杂的布局需求,Flex弹性盒模型的优势在于开发人员只是声明布局应该具有的行为,而不需要给出具体的实现方式,浏览器负责完成实际布局。

    采用Flex布局的元素,称为flex容器container

    它的所有子元素自动成为容器成员,称为flex项目item

    容器中默认存在两条轴,主轴和交叉轴,呈90度关系。项目默认沿主轴排列,通过flex-direction来决定主轴的方向

    每根轴都有起点和终点,这对于元素的对齐非常重要

  2. 容器的属性

    /*主轴的方向*/
    flex-direction: row | row-reverse | column | column-reverse;
    row(默认值):主轴为水平方向,起点在左端。
    row-reverse:主轴为水平方向,起点在右端。
    column:主轴为垂直方向,起点在上沿。
    column-reverse:主轴为垂直方向,起点在下沿。
    
    /*换行属性*/
    flex-wrap: nowrap | wrap | wrap-reverse;
    nowrap(默认):不换行。
    wrap:换行,第一行在上方。
    wrap-reverse:换行,第一行在下方。
    
    /*简写:方向 + 换行*/
    flex-flow: <flex-direction> || <flex-wrap>;
    
    /*主轴对齐方式*/
    justify-content: flex-start | flex-end | center | space-between | space-around;
    flex-start(默认值):左对齐
    flex-end:右对齐
    center: 居中
    space-between:两端对齐,项目之间的间隔都相等。
    space-around:每个项目两侧的间隔相等。所以,项目之间的间隔比项目与边框的间隔大一倍。
    
    /*交叉轴对齐方式*/
    align-items: flex-start | flex-end | center | baseline | stretch;
    flex-start:交叉轴的起点对齐。
    flex-end:交叉轴的终点对齐。
    center:交叉轴的中点对齐。
    baseline: 项目的第一行文字的基线对齐。
    stretch(默认值):如果项目未设置高度或设为auto,将占满整个容器的高度。
    
  3. 项目的属性

    /*排列顺序,数值越小,排列越靠前,默认为0。*/
    order: <integer>;
    
    /*项目的放大比例,默认为0,即如果存在剩余空间,也不放大。*/
    flex-grow: <number>; /* default 0 */
    
    /*项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。*/
    flex-shrink: <number>; /* default 1 */
    
    /*项目占据的空间,默认值为auto,即项目的本来大小*/
    flex-basis: <length> | auto; /* default auto */
    
    /*简写:flex-grow, flex-shrink 和 flex-basis*/
    flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]
    flex: 1 = flex: 1 1 0%
    flex: 2 = flex: 2 1 0%
    flex: auto = flex: 1 1 auto
    flex: none = flex: 0 0 auto,常用于固定尺寸不伸缩
    flex:1 和 flex:auto 的区别,可以归结于flex-basis:0和flex-basis:auto的区别
    注意:建议优先使用这个属性,而不是单独写三个分离的属性,因为浏览器会推算相关值
    

在以前的文章中,我们能够通过flex简单粗暴的实现元素水平垂直方向的居中,以及在两栏三栏自适应布局中通过flex完成,这里就不再展开代码的演示

28、介绍一下grid网格布局

29、defer和async的区别

没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。

有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。

有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded事件触发之前完成。

然后从实用角度来说呢,首先把所有脚本都丢到 之前是最佳实践,因为对于旧浏览器来说这是唯一的优化选择,此法可保证非脚本的其他一切元素能够以最快的速度得到加载和解析。

接着,我们来看一张图咯:

在页面脚本引用的时候设置defer或者async,为什么会有这两个属相来辅助脚本加载那,因为浏览器在遇到script标签的时候,文档的解析会停止,不再构建document,有时打开一个网页上会出现空白一段时间,浏览器显示是刷新请求状态(也就是一直转圈),这就会给用户很不好的体验,defer和async的合理使用就可以避免这个情况,而且通常script的位置建议写在页面底部(移动端应用的比较多,这两个都是html5中的新属性)。

所以相对于默认的script引用,这里配合defer和async就有两种新的用法,它们之间什么区别那?

1.默认引用 script:

当浏览器遇到 script 标签时,文档的解析将停止,并立即下载并执行脚本,脚本执行完毕后将继续解析文档。

2.async模式

当浏览器遇到 script 标签时,文档的解析不会停止,其他线程将下载脚本,脚本下载完成后开始执行脚本,脚本执行的过程中文档将停止解析,直到脚本执行完毕。

3.defer模式

当浏览器遇到 script 标签时,文档的解析不会停止,其他线程将下载脚本,待到文档解析完成,脚本才会执行。

所以async和defer的最主要的区别就是async是异步下载并立即执行,然后文档继续解析,defer是异步加载后解析文档,然后再执行脚本,这样说起来是不是理解了一点了呢?

推荐的应用场景

defer

如果你的脚本代码依赖于页面中的DOM元素(文档是否解析完毕),或者被其他脚本文件依赖。
例如:

  • 评论框
  • 代码语法高亮
  • polyfill.js

async

如果你的脚本并不关心页面中的DOM元素(文档是否解析完毕),并且也不会产生其他脚本需要的数据。

例如:

  • 百度统计

如果不太能确定的话,用defer总是会比async稳定。。。

30、CSS提高性能的方法有哪些?

JavaScript

1、JavaScript 有几种类型的值?你能画一下他们的内存图吗?

涉及知识点:

  • 栈:原始数据类型(Undefined、Null、Boolean、Number、String、symbol)
  • 堆:引用数据类型(对象、数组和函数)除了上述说的三种之外,还包括DateRegExpMapSet等......
两种类型的区别是:存储位置不同。
原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储。

引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;

引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
在操作系统中,内存被分为栈区和堆区。

栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

堆区内存一般由程序员分配释放,若程序员不释放,程序结束时可能由垃圾回收机制回收。

2、js中的闭包

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)

也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域

下面给出一个简单的例子:

function init() {
    var name = "Mozilla"; // name 是一个被 init 创建的局部变量
    function displayName() { // displayName() 是内部函数,一个闭包
        alert(name); // 使用了父函数中声明的变量
    }
    displayName();
}

init();

displayName() 没有自己的局部变量。然而,由于闭包的特性,它可以访问到外部函数的变量

我们首先知道闭包有3个特性:

  1. 函数嵌套函数
  2. 函数内部可以引用函数外部的参数和变量
  3. 参数和变量不会被垃圾回收机制回收

闭包的好处与坏处

  • 好处

    1. 保护函数内的变量安全 ,实现封装,防止变量流入其他环境发生命名冲突
    2. 在内存中维持一个变量,可以做缓存(但使用多了同时也是一项缺点,消耗内存)
    3. 匿名自执行函数可以减少内存消耗
  • 坏处

  1. 其中一点上面已经有体现了,就是被引用的私有变量不能被销毁,增大了内存消耗,造成内存泄漏,解决方法是可以在使用完变量后手动为它赋值为null;
  2. 其次由于闭包涉及跨域访问,所以会导致性能损失,我们可以通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响

哪里会出现闭包

1)返回一个函数

2)作为函数参数传递

var a = 1;
function foo(){
    var a = 2;
    function baz(){
        console.log(a);
    }
bar(baz);
}
function bar(fn){
    // 这就是闭包
    fn();
}
    // 输出2,而不是1
foo();

3)在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers 或者任何异步中,只要使用了回调函 数,实际上就是在使用闭包。 以下的闭包保存的仅仅是window和当前作用域

    // 定时器
setTimeout(function timeHandler(){
    console.log('111');
},100)
    // 事件监听
$('#app').click(function(){
    console.log('DOM Listener');
})

4)IIFE(立即执行函数表达式)创建闭包, 保存了 全局作用域 window 和当前函数的作用域 ,因此可以全局的变量。

var a = 2;
(function IIFE(){
    // 输出2
    console.log(a);
})();

函数的柯里化

柯里化的目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用

// 假设我们有一个求长方形面积的函数
function getArea(width, height) {
 return width * height
}
// 如果我们碰到的长方形的宽老是10
const area1 = getArea(10, 20)
const area2 = getArea(10, 30)
const area3 = getArea(10, 40)

// 我们可以使用闭包柯里化这个计算面积的函数
function getArea(width) {
 return height => {
     return width * height
 }
}

const getTenWidthArea = getArea(10)
// 之后碰到宽度为10的长方形就可以这样计算面积
const area1 = getTenWidthArea(20)

// 而且如果遇到宽度偶尔变化也可以轻松复用
const getTwentyWidthArea = getArea(20)

3、执行上下文机制

简单的来说,执行上下文是一种对Javascript代码执行环境的抽象概念,也就是说只要有Javascript代码运行,那么它就一定是运行在执行上下文中

执行上下文的类型分为三种:

  • 全局执行上下文:只有一个,浏览器中的全局对象就是 window对象,this 指向这个全局对象
  • 函数执行上下文:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文
  • Eval 函数执行上下文: 指的是运行在 eval 函数中的代码,很少用而且不建议使用

生命周期:

执行上下文的生命周期包括三个阶段:创建阶段 → 执行阶段 → 回收阶段

创建阶段做了三件事:

  • 确定 this 的值,也被称为 This Binding
  • LexicalEnvironment(词法环境) 组件被创建
  • VariableEnvironment(变量环境) 组件被创建--------变量环境是词法环境的一种

执行栈

执行栈,也叫调用栈,具有 LIFO(后进先出)结构,用于存储在代码执行期间创建的所有执行上下文

Javascript引擎开始执行你第一行脚本代码的时候,它就会创建一个全局执行上下文然后将它压到执行栈中

每当引擎碰到一个函数的时候,它就会创建一个函数执行上下文,然后将这个执行上下文压到执行栈中

引擎会执行位于执行栈栈顶的执行上下文(一般是函数执行上下文),当该函数执行结束后,对应的执行上下文就会被弹出,然后控制流程到达执行栈的下一个执行上下文

举个例子:

let a = 'Hello World!';
function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}
function second() {
  console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');

转化成图的形式

简单分析一下流程:

  • 创建全局上下文请压入执行栈
  • first函数被调用,创建函数执行上下文并压入栈
  • 执行first函数过程遇到second函数,再创建一个函数执行上下文并压入栈
  • second函数执行完毕,对应的函数执行上下文被推出执行栈,执行下一个执行上下文first函数
  • first函数执行完毕,对应的函数执行上下文也被推出栈中,然后执行全局上下文
  • 所有代码执行完毕,全局上下文也会被推出栈中,程序结束

4、v8回收机制

介绍一下引用计数和标记清除---------判断是否回收的方法

  • 引用计数:给一个变量赋值引用类型,则该对象的引用次数+1,如果这个变量变成了其他值,那么该对象的引用次数-1,垃圾回收器会回收引用次数为0的对象。但是当对象循环引用时,会导致引用次数永远无法归零,造成内存无法释放。
  • 标记清除:垃圾收集器先给内存中所有对象加上标记,然后从根节点开始遍历,去掉被引用的对象和运行环境中对象的标记,剩下的被标记的对象就是无法访问的等待回收的对象。

栈内存的回收:

栈内存调用栈上下文切换后就被回收,比较简单

堆内存的回收:

V8的堆内存分为新生代内存和老生代内存,新生代内存是临时分配的内存,存在时间短,老生代内存存在时间长

  • 新生代内存回收机制:
    • 新生代内存容量小,64位系统下仅有32M。新生代内存分为From、To两部分,进行垃圾回收时,先扫描From,将非存活对象回收,将存活对象顺序复制到To中,之后调换From/To,等待下一次回收
  • 老生代内存回收机制
    • 晋升:如果新生代的变量经过多次回收依然存在,那么就会被放入老生代内存中
    • 标记清除:老生代内存会先遍历所有对象并打上标记,然后对正在使用或被强引用的对象取消标记,回收被标记的对象
    • 整理内存碎片:把对象挪到内存的一端

5、原型链是什么?

原型

JavaScript 常被描述为一种基于原型的语言——每个对象拥有一个原型对象

当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾

准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非实例对象本身

下面举个例子:

函数可以有属性。 每个函数都有一个特殊的属性叫作原型prototype

function doSomething(){}
console.log( doSomething.prototype );

控制台输出

{
    constructor: ? doSomething(),
    __proto__: {
        constructor: ? Object(),
        hasOwnProperty: ? hasOwnProperty(),
        isPrototypeOf: ? isPrototypeOf(),
        propertyIsEnumerable: ? propertyIsEnumerable(),
        toLocaleString: ? toLocaleString(),
        toString: ? toString(),
        valueOf: ? valueOf()
    }
}

上面这个对象,就是大家常说的原型对象

函数有原型,函数有一个属性叫prototype,函数的这个原型指向一个对象,这个对象叫原型对象。这个原型对象有一个constructor属性,指向这个函数本身。

可以看到,原型对象有一个自有属性constructor,这个属性指向该函数,如下图关系展示

原型链

用new运算符加上函数的调用,调用的结果就是一个对象,new出来的这个对象我们称他为实例对象

函数prototype(原型)是一个指针指向了一个原型对象,这个对象里面存放所有的实例共享的属性和方法

实例的__proto__指向了构造函数的prototype,

构造函数是object的实例,构造函数的prototype下的__proto__指向了object的prototype

Person.__proto__ 指向内置匿名函数 anonymous,因为 Person 是个函数对象,默认由 Function 作为类创建.Function.prototypeFunction.__proto__同时指向内置匿名函数 anonymous,这样原型链的终点就是 null

我们称他为原型链

原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法

在对象实例和它的构造器之间建立一个链接(它是__proto__属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法

下面举个例子:

function Person(name) {
    this.name = name;
    this.age = 18;
    this.sayName = function() {
        console.log(this.name);
    }
}
// 第二步 创建实例
var person = new Person('person')

根据代码,我们可以得到下图

下面分析一下:

  • 构造函数Person存在原型对象Person.prototype
  • 构造函数生成实例对象personperson__proto__指向构造函数Person原型对象
  • Person.prototype.__proto__ 指向内置对象,因为 Person.prototype 是个对象,默认是由 Object函数作为类创建的,而 Object.prototype 为内置对象
  • Person.__proto__ 指向内置匿名函数 anonymous,因为 Person 是个函数对象,默认由 Function 作为类创建
  • Function.prototypeFunction.__proto__同时指向内置匿名函数 anonymous,这样原型链的终点就是 null

总结

下面首先要看几个概念:

__proto__作为不同对象之间的桥梁,用来指向创建它的构造函数的原型对象的

每个对象的__proto__都是指向它的构造函数的原型对象prototype

person1.__proto__ === Person.prototype

构造函数是一个函数对象,是通过 Function构造器产生的

Person.__proto__ === Function.prototype

原型对象本身是一个普通对象,而普通对象的构造函数都是Object

Person.prototype.__proto__ === Object.prototype

刚刚上面说了,所有的构造器都是函数对象,函数对象都是 Function构造产生的

Object.__proto__ === Function.prototype

Object的原型对象也有__proto__属性指向nullnull是原型链的顶端

Object.prototype.__proto__ === null

下面作出总结:

  • 一切对象都是继承自Object对象,Object 对象直接继承根源对象null
  • 一切的函数对象(包括 Object 对象),都是继承自 Function 对象
  • Object 对象直接继承自 Function 对象
  • Function对象的__proto__会指向自己的原型对象,最终还是继承自Object对象

原型链上的属性能不能遍历到,如果能,如何不遍历到原型链的属性,不能说明原因

使用for in可遍历对象的属性,原型链上的所有属性都将被访问,使用Object.keys()只能遍历自身属性

for in 语句以任意顺序遍历一个对象的除Symbol以外的可枚举属性。会遍历原型上的属性

Object.keys()方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和使用 for…in 循环遍历该对象时返回的顺序一致 。不会遍历原型上的属性

Object.getOwnPropertyNames()方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。不会获得原型上的属性

使用obj.hasOwnProperty(属性名)过滤,可不遍历原型上的属性

6、JavaScript 继承的几种实现方式?

  • 原型链继承
  • 构造函数继承(借助 call)
  • 组合继承
  • 原型式继承
  • 寄生式继承
  • 寄生组合式继承

通过Object.create 来划分不同的继承方式,最后的寄生式组合继承方式是通过组合继承改造之后的最优继承方式,而 extends 的语法糖和寄生组合继承的方式基本类似

原型链继承

原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针

举个例子

function Parent() {
 this.name = 'parent1';
 this.play = [1, 2, 3]
}
function Child() {
 this.type = 'child2';
}
Child1.prototype = new Parent();
console.log(new Child())

上面代码看似没问题,实际存在潜在问题

var s1 = new Child2();
var s2 = new Child2();
s1.play.push(4);
console.log(s1.play, s2.play); // [1,2,3,4]

改变s1play属性,会发现s2也跟着发生变化了,这是因为两个实例使用的是同一个原型对象,内存空间是共享的

构造函数继承

借助 call调用Parent函数

function Parent(){
 this.name = 'parent1';
}

Parent.prototype.getName = function () {
 return this.name;
}

function Child(){
 Parent1.call(this);
 this.type = 'child'
}

let child = new Child();
console.log(child);  // 没问题
console.log(child.getName());  // 会报错

可以看到,父类原型对象中一旦存在父类之前自己定义的方法,那么子类将无法继承这些方法

相比第一种原型链继承方式,父类的引用属性不会被共享,优化了第一种继承方式的弊端,但是只能继承父类的实例属性和方法,不能继承原型属性或者方法

组合继承

前面我们讲到两种继承方式,各有优缺点。组合继承则将前两种方式继承起来

function Parent3 () {
 this.name = 'parent3';
 this.play = [1, 2, 3];
}

Parent3.prototype.getName = function () {
 return this.name;
}
function Child3() {
 // 第二次调用 Parent3()
 Parent3.call(this);
 this.type = 'child3';
}

// 第一次调用 Parent3()
Child3.prototype = new Parent3();
// 手动挂上构造器,指向自己的构造函数
Child3.prototype.constructor = Child3;
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play);  // 不互相影响
console.log(s3.getName()); // 正常输出'parent3'
console.log(s4.getName()); // 正常输出'parent3'

这种方式看起来就没什么问题,方式一和方式二的问题都解决了,但是从上面代码我们也可以看到Parent3 执行了两次,造成了多构造一次的性能开销

原型式继承

这里主要借助Object.create方法实现普通对象的继承

同样举个例子

let parent4 = {
 name: "parent4",
 friends: ["p1", "p2", "p3"],
 getName: function() {
   return this.name;
 }
};

let person4 = Object.create(parent4);
person4.name = "tom";
person4.friends.push("jerry");

let person5 = Object.create(parent4);
person5.friends.push("lucy");

console.log(person4.name); // tom
console.log(person4.name === person4.getName()); // true
console.log(person5.name); // parent4
console.log(person4.friends); // ["p1", "p2", "p3","jerry","lucy"]
console.log(person5.friends); // ["p1", "p2", "p3","jerry","lucy"]

这种继承方式的缺点也很明显,因为Object.create方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能

寄生式继承

寄生式继承在上面继承基础上进行优化,利用这个浅拷贝的能力再进行增强,添加一些方法

let parent5 = {
 name: "parent5",
 friends: ["p1", "p2", "p3"],
 getName: function() {
     return this.name;
 }
};

function clone(original) {
 let clone = Object.create(original);
 clone.getFriends = function() {
     return this.friends;
 };
 return clone;
}

let person5 = clone(parent5);

console.log(person5.getName()); // parent5
console.log(person5.getFriends()); // ["p1", "p2", "p3"]

其优缺点也很明显,跟上面讲的原型式继承一样

寄生组合式继承

寄生组合式继承,借助解决普通对象的继承问题的Object.create 方法,在前面几种继承方式的优缺点基础上进行改造,这也是所有继承方式里面相对最优的继承方式

function clone (parent, child) {
 // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
 child.prototype = Object.create(parent.prototype);
 child.prototype.constructor = child;
}

function Parent6() {
 this.name = 'parent6';
 this.play = [1, 2, 3];
}
Parent6.prototype.getName = function () {
 return this.name;
}
function Child6() {
 Parent6.call(this);
 this.friends = 'child5';
}

clone(Parent6, Child6);

Child6.prototype.getFriends = function () {
 return this.friends;
}

let person6 = new Child6();
console.log(person6); //{friends:"child5",name:"child5",play:[1,2,3],__proto__:Parent6}
console.log(person6.getName()); // parent6
console.log(person6.getFriends()); // child5

可以看到 person6 打印出来的结果,属性都得到了继承,方法也没问题

文章一开头,我们是使用ES6 中的extends关键字直接实现 JavaScript的继承

class Person {
constructor(name) {
 this.name = name
}
// 原型方法
// 即 Person.prototype.getName = function() { }
// 下面可以简写为 getName() {...}
getName = function () {
 console.log('Person:', this.name)
}
}
class Gamer extends Person {
constructor(name, age) {
 // 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
 super(name)
 this.age = age
}
}
const asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功访问到父类的方法

利用babel工具进行转换,我们会发现extends实际采用的也是寄生组合继承方式,因此也证明了这种方式是较优的解决继承的方式

7、寄生式组合继承的实现?

function Person(name) {
  this.name = name;
}

Person.prototype.sayName = function() {
  console.log("My name is " + this.name + ".");
};

function Student(name, grade) {
  Person.call(this, name);
  this.grade = grade;
}

Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;

Student.prototype.sayMyGrade = function() {
  console.log("My grade is " + this.grade + ".");
};

8、Javascript 的作用域链?

作用域,即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合
换句话说,作用域决定了代码区块中变量和其他资源的可见性


作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境的变量和函数。

当我们查找一个变量时,如果当前执行环境中没有找到,我们可以沿着作用域链向后查找。

作用域链的创建过程跟执行上下文的建立有关....

我们在全局是无法获取到(闭包除外)函数内部的变量

我们一般将作用域分成:

  • 全局作用域
  • 函数作用域
  • 块级作用域

1.1 全局作用域

任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问

// 全局变量
var greeting = 'Hello World!';
function greet() {
  console.log(greeting);
}
// 打印 'Hello World!'
greet();

1.2 函数作用域

函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问

function greet() {
  var greeting = 'Hello World!';
  console.log(greeting);
}
// 打印 'Hello World!'
greet();
// 报错: Uncaught ReferenceError: greeting is not defined
console.log(greeting);

可见上述代码中在函数内部声明的变量或函数,在函数外部是无法访问的,这说明在函数内部定义的变量或者方法只是函数作用域

1.3 块级作用域

ES6引入了letconst关键字,和var关键字不同,在大括号中使用letconst声明的变量存在于块级作用域中。在大括号之外不能访问这些变量

{
  // 块级作用域中的变量
  let greeting = 'Hello World!';
  var lang = 'English';
  console.log(greeting); // Prints 'Hello World!'
}
// 变量 'English'
console.log(lang);
// 报错:Uncaught ReferenceError: greeting is not defined
console.log(greeting);

二、词法作用域

词法作用域,又叫静态作用域,变量被创建时就确定好了,而非执行阶段确定的。也就是说我们写好代码时它的作用域就确定了,JavaScript 遵循的就是词法作用域

var a = 2;
function foo(){
    console.log(a)
}
function bar(){
    var a = 3;
    foo();
}
bar()

上述代码改变成一张图

由于JavaScript遵循词法作用域,相同层级的 foobar 就没有办法访问到彼此块作用域中的变量,所以输出2

作用域链

当在Javascript中使用一个变量的时候,首先Javascript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域

如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错

9、谈谈 This 对象的理解。

this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模
式来判断。
  • 1.第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。

  • 2.第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。

  • 3.第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。

  • 4.第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。

这四种方式,使用构造器调用模式的优先级最高,然后是 apply 、 call 和 bind 调用模式,然后是方法调用模式,然后
是函数调用模式。

10、this和super的区别

  1. 属性的区别:
    • this访问本类中的属性,如果本类没有此属性则从父类中继续查找。
    • super访问父类中的属性。
  2. 方法的区别:
    • this访问本类中的方法,如果本类没有此方法则从父类中继续查找。
    • super访问父类中的方法。
  3. 构造的区别:
    • this调用本类构造,必须放在构造方法的首行。
    • super调用父类构造,必须放在子类构造方法首行。
  4. 其他区别:
    • this表示当前对象。
    • super不能表示当前对象

11、new的时候做了哪些操作

  1. 创建一个新对象
  2. 将对象与构建函数通过原型链连接起来
  3. 将构造函数的作用域赋值给新对象(this指向这个新对象)
  4. 执行构造函数中的代码(为这个新对象添加属性)
  5. 返回新对象

12、说一下apply、call与bind

JavaScript 的一大特点是,函数存在「定义时上下文」和「运行时上下文」以及「上下文是可以改变的」这样的概念。

  1. 共同点:
    • 都是函数对象的一个方法,作用是改变函数执行时的上下文,即改变函数体内部this的指向

那么什么情况下需要改变this的指向呢?下面举个例子

var name = "lucy";
var obj = {
    name: "martin",
    say: function () {
        console.log(this.name);
    }
};
obj.say(); // martin,this 指向 obj 对象
setTimeout(obj.say,0); // lucy,this 指向 window 对象

从上面可以看到,正常情况say方法输出martin

但是我们把say放在setTimeout方法中,在定时器中是作为回调函数来执行的,因此回到主栈执行时是在全局执行上下文的环境中执行的,这时候this指向window,所以输出lucy

我们实际需要的是this指向obj对象,这时候就需要该改变this指向了

setTimeout(obj.say.bind(obj),0); //martin,this指向obj对象
  1. 区别:
  • 三者都可以改变函数的this对象指向
  • 三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefinednull,则默认指向全局window
  • 三者都可以传参,但是apply是数组,而call、bind是参数列表,且applycall是一次性传入参数,而bind可以分为多次传入
  • bind是返回绑定this之后的函数,applycall 则是立即执行

apply

apply接受两个参数,第一个参数是this的指向,第二个参数是函数接受的参数,以数组的形式传入

改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次

function fn(...args){
    console.log(this,args);
}
let obj = {
    myname:"张三"
}

fn.apply(obj,[1,2]); // this会变成传入的obj,传入的参数必须是一个数组;
fn(1,2) // this指向window

一个参数为nullundefined的时候,默认指向window(在浏览器中)

fn.apply(null,[1,2]); // this指向window
fn.apply(undefined,[1,2]); // this指向window

call

call方法的第一个参数也是this的指向,后面传入的是一个参数列表

apply一样,改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次

function fn(...args){
    console.log(this,args);
}
let obj = {
    myname:"张三"
}

fn.call(obj,1,2); // this会变成传入的obj,传入的参数必须是一个数组;
fn(1,2) // this指向window

同样的,当第一个参数为nullundefined的时候,默认指向window(在浏览器中)

fn.call(null,[1,2]); // this指向window
fn.call(undefined,[1,2]); // this指向window

bind

bind方法和call很相似,第一参数也是this的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入)

改变this指向后不会立即执行,而是返回一个永久改变this指向的函数

function fn(...args){
    console.log(this,args);
}
let obj = {
    myname:"张三"
}

const bindFn = fn.bind(obj); // this 也会变成传入的obj ,bind不是立即执行需要执行一次
bindFn(1,2) // this指向obj
fn(1,2) // this指向window

主要应用场景:

1.call经常做继承。 Object.prototype.toString.call()判断数据类型

2.apply经常跟数组有关系,比如借助于数学对象实现数组最大值最小值。

3.bind不调用函数,但是海想改变this指向,比如改变定时器内部的this指向。

13、JS的进程与线程

进程:程序的一次执行,占有独有的一片内存空间。可以通过任务管理器查看。

线程:

  • 是进程内的一个独立执行单元
  • 是程序执行的一个完整流程
  • 是CPU的最小的调度单元

相关知识:

  • 应用程序必须运行在某个进程的某个线程上
  • 一个进程中至少有一个运行的线程:主线程,进程启动后自动创建
  • 一个进程中也可以同时运行多个线程,我们会说程序是多线程运行的
  • 一个进程内的数据可以供其中的多个线程直接共享
  • 多个进程之间的数据是不能直接共享的
  • 线程池(threadpool):保存多个线程对象的容器,实现线程对象的反复利用

多进程运行:一应用程序可以同时启动多个实例运行
多线程:在一个进程内,同时有多个线程运行

多线程:

优点:1、能有效提升CPU的利用率 2、创建多线程开销

缺点:1、线程间切换开销 2、死锁与状态同步问题

单线程:

优点:顺序编程简单易懂
缺点:效率低

js: js是单线程运行的,但使用H5中的WebWorkers可以多线程运行

浏览器:都是多线程的

单进程(ie、firefox) 多线程(chrome、edge)

14、DOM流与事件委托机制/事件流的三个阶段

javascript中的事件,可以理解就是在HTML文档或者浏览器中发生的一种交互操作,使得网页具备互动性, 常见的有加载事件、鼠标事件、自定义事件等

由于DOM是一个树结构,如果在父子节点绑定事件时候,当触发子节点的时候,就存在一个顺序问题,这就涉及到了事件流的概念

事件流都会经历三个阶段:

  • 事件捕获阶段(capture phase)
  • 处于目标阶段(target phase)
  • 事件冒泡阶段(bubbling phase)

事件冒泡是一种从下往上的传播方式,由最具体的元素(触发节点)然后逐渐向上传播到最不具体的那个节点,也就是DOM中最高层的父节点

事件捕获与事件冒泡相反,事件最开始由不太具体的节点最早接受事件, 而最具体的节点(触发节点)最后接受事件

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Event Bubbling</title>
    </head>
    <body>
        <button id="clickMe">Click Me</button>
    </body>
</html>

然后,我们给button和它的父元素,加入点击事件

var button = document.getElementById('clickMe');

button.onclick = function() {
  console.log('1.Button');
};
document.body.onclick = function() {
  console.log('2.body');
};
document.onclick = function() {
  console.log('3.document');
};
window.onclick = function() {
  console.log('4.window');
};

点击按钮,输出如下

1.button
2.body
3.document
4.window

点击事件首先在button元素上发生,然后逐级向上传播

事件委托和事件代理

事件委托本质上是利用了浏览器事件冒泡的机制。

因为事件在冒泡过程中会上传到父节点,并且父节点可以通过事件对象获取到目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件代理

使用事件代理我们可以不必要为每一个子元素都绑定一个监听事件,这样减少了内存上的消耗。并且使用事件代理我们还可以实现事件的动态绑定,比如说新增了一个子节点,我们并不需要单独地为它添加一个监听事件,它所发生的事件会交给父元素中的监听函数来处理。

阻止冒泡和阻止默认

html结构:

    <body>
      <form id="form1" runat="server">
        <div id="divOne" onclick="alert('我是最外层');">
          <div id="divTwo" onclick="alert('我是中间层!')">
            <a
              id="hr_three"
              href="http://www.baidu.com"
              mce_href="http://www.baidu.com"
              onclick="alert('我是最里层!')"
              >点击我</a
            >
          </div>
        </div>
      </form>
    </body>
  </body>
  
  
上述点击结果:
会依次弹出:我是最里层---->我是中间层---->我是最外层---->然后再链接到百度.
这就涉及到事件冒泡了.

阻止冒泡:

event.stopPropagation();

事件处理过程中,阻止了事件冒泡,但不会阻击默认行为(它就执行了超链接的跳转)

<script>
//上面点击会出现事件冒泡
document.getElementById("hr_three").onclick=function(event){

    event.stopPropagation(); //阻止了事件冒泡,只有弹出自己的"我是最里层",然后链接到百度
    
}

</script>

阻止默认行为:

return false和event.preventDefault()

<script>
//上面点击会出现事件冒泡
document.getElementById("hr_three").onclick=function(event){

    return false; //自己弹框和跳转都阻止了,只提示中间和外层
    //event.preventDefault(); //自己弹框和跳转都阻止了,只提示中间和外层
}

</script>

我是中间层---->我是最外层

15、js 单线程 事件循环机制(event loop)

相关知识点:

任务队列是一个存储着待执行任务的队列,其中的任务严格按照时间先后顺序执行,排在队头的任务将会率先执行,而排在队尾的任务会最后执行。事件队列每次仅执行一个任务,在该任务执行完毕之后,再执行下一个任务。

执行栈则是一个类似于函数调用栈的运行容器,当执行栈为空时,JS 引擎便检查事件队列,如果不为空的话,任务队列便将第一个任务压入执行栈中运行。

异步任务在任务队列中执行,同步任务在执行栈中执行

回答:

因为 js 是单线程运行的,但是这并不意味着单线程就是阻塞,而实现单线程非阻塞的方法就是事件循环

简要分析:在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。在执行同步代码的时候,如果遇到了异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当同步事件执行完毕后,再将异步事件对应的回调加入到与当前执行栈中不同的另一个任务队列中等待执行。

event loop它的执行顺序:

  • 一开始整个脚本作为一个宏任务执行
  • 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
  • 当前宏任务执行完出队,检查微任务列表,有则依次执行,直到全部执行完
  • 执行浏览器UI线程的渲染工作
  • 检查是否有Web Worker任务,有则执行
  • 执行完本轮的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空

微任务包括:MutationObserverPromise.then()或catch()Promise为基础开发的其它技术,比如fetch APIV8的垃圾回收过程、Node独有的process.nextTick

宏任务包括scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

注意??:在所有任务开始的时候,由于宏任务中包括了script,所以浏览器会先执行一个宏任务,在这个过程中你看到的延迟任务(例如setTimeout)将被放到下一轮宏任务中来执行。

宏任务和微任务

任务队列可以分为宏任务对列和微任务对列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务对列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。

微任务包括了 promise 的回调(promise.then,而new promise是同步任务)、node 中的 process.nextTick 、对 Dom 变化监听的MutationObserver。-------------------值得注意的是async、await中await 会阻塞其下面的代码(即加入微任务队列),因此执行 async外面的同步代码,同步代码执行完,再回到 async 函数中,再执行之前阻塞的代码

宏任务包括了 script 脚本的执行、setTimeout ,setInterval ,setImmediate 一类的定时事件,还有如 I/O 操作、UI 渲
染等。

16、判断宏任务、微任务执行顺序(常见面试题)

通过对上面的了解,我们对JavaScript对各种场景的执行顺序有了大致的了解

这里直接上代码:

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('settimeout')
})
async1()
new Promise(function (resolve) {
    console.log('promise1')
    resolve()
}).then(function () {
    console.log('promise2')
})
console.log('script end')

分析过程:

  1. 执行整段代码,遇到 console.log('script start') 直接打印结果,输出 script start
  2. 遇到定时器了,它是宏任务,先放着不执行
  3. 遇到 async1(),执行 async1 函数,先打印 async1 start,下面遇到await怎么办?先执行 async2,打印 async2,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码
  4. 跳到 new Promise 这里,直接执行,打印 promise1,下面遇到 .then(),它是微任务,放到微任务列表等待执行
  5. 最后一行直接打印 script end,现在同步代码执行完了,开始执行微任务,即 await下面的代码,打印 async1 end
  6. 继续执行下一个微任务,即执行 then 的回调,打印 promise2
  7. 上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印 settimeout

所以最后的结果是:script startasync1 startasync2promise1script endasync1 endpromise2settimeout

题目
async function async1 () {
  console.log('async1 start');
  await new Promise(resolve => {
    console.log('promise1')
  })
  console.log('async1 success');
  return 'async1 end'
}
console.log('srcipt start')
async1().then(res => console.log(res))
console.log('srcipt end')
复制代码

这道题目比较有意思,大家要注意了。

async1await后面的Promise是没有返回值的,也就是它的状态始终是pending状态,因此相当于一直在awaitawaitawait却始终没有响应...

所以在await之后的内容是不会执行的,也包括async1后面的 .then

答案为:

'script start'
'async1 start'
'promise1'
'script end'
复制代码
题目

让我们给5.5中的Promise加上resolve

async function async1 () {
  console.log('async1 start');
  await new Promise(resolve => {
    console.log('promise1')
    resolve('promise1 resolve')
  }).then(res => console.log(res))
  console.log('async1 success');
  return 'async1 end'
}
console.log('srcipt start')
async1().then(res => console.log(res))
console.log('srcipt end')
复制代码

现在Promise有了返回值了,因此await后面的内容将会被执行:

'script start'
'async1 start'
'promise1'
'script end'
'promise1 resolve'
'async1 success'
'async1 end'
复制代码
题目
async function async1 () {
  console.log('async1 start');
  await new Promise(resolve => {
    console.log('promise1')
    resolve('promise resolve')
  })
  console.log('async1 success');
  return 'async1 end'
}
console.log('srcipt start')
async1().then(res => {
  console.log(res)
})
new Promise(resolve => {
  console.log('promise2')
  setTimeout(() => {
    console.log('timer')
  })
})
复制代码

这道题应该也不难,不过有一点需要注意的,在async1中的new Promise它的resovle的值和async1().then()里的值是没有关系的,很多小伙伴可能看到resovle('promise resolve')就会误以为是async1().then()中的返回值。

因此这里的执行结果为:

'script start'
'async1 start'
'promise1'
'promise2'
'async1 success'
'async1 end'
'timer'

17、JS中的深拷贝与浅拷贝的区别?

浅拷贝

浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝

如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址

即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址

  • Object.assign
  • slice
  • contact
  • 拓展运算符

深拷贝

深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性

  • JSON.stringify()
  • 手写递归循环

区别

  • 深拷贝递归地复制新对象中的所有值或属性,而浅拷贝只复制引用。
  • 在深拷贝中,新对象中的更改不会影响原始对象,而在浅拷贝中,新对象中的更改,原始对象中也会跟着改。

,浅拷贝和深拷贝都创建出一个新的对象,但在复制对象属性的时候,行为就不一样

  • 浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,两个对象指向同一个地址
  • 深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址

18、DOM常见的操作有哪些?

dom和dom树的区别

DOM
JavaScript操作网页的接口,全称为“文档对象模型”(Document Object Model)。
有这几个概念:文档、元素、节点
整个文档是一个文档节点
每个标签是一个元素节点
包含在元素中的文本是文本节点
每一个属性是一个属性节点
注释属于注释节点
DOM树:
DOM树是结构
所谓层级结构是指元素和元素之间的关系
父子,兄弟
解析器输出的树是由DOM元素和属性节点组成的
当我们说树中包含DOM节点时,意思就是这个树是由实现了DOM接口的元素组成。这些实现包含了其它一些浏览器内部所需的属性。
脱离文档流后层级结构关系还是没有变的

  • DOM 是 Document Object Model(文档对象模型)的缩写。提供给Javascript用来动态修改文档状态…

5个常用的DOM方法:getElemenById、getElementsByTagName、getElementsByClassName、getAttribute和setAttribute

文档:DOM中的"D"

如果没有document(文档),DOM也就无从谈起。当创建了一个网页并把它加载到Web浏览器中时,DOM就在幕后悄然而生了。它把你编写的网页文档转换成为一个文档对象。

对象:DOM中的"O"

对象(object)是一种非常重要的数据类型。对象是自包含的数据集合,包含在对象里的数据可以通过两种形式访问——属性和方法。

属性是隶属于某个特定对象的变量;

方法是只有某个特定对象才能调用的函数

对象就是由一些属性和方法组合在一起而构成的一个数据实体。在javaScript中,属性(property)和方法(method)都使用“点”语法来访问:

object.propertyobject.method()

JavaScript语言里面的对象可以分为三种类型。

用户定义对象:由程序员自行创建的对象。

內建对象:內建在JavaScript语言里的对象,如Array、Math和Date等。

宿主对象:由浏览器提供的对象。

即使是在JavaScript的最初版本里,对编写脚本来说非常重要的一些宿主对象就已经可用了,他们当中最基础的对象是window(浏览器窗口)对象。

模型:DOM中的"M"

DOM中的“M”代表着“Model”(模型),它的含义是某种事物的表现形式,DOM代表着加载到浏览器窗口的当前网页。浏览器提供了网页的模型,而我们可以通过JavaScript去读取这张地图。

利用JS操作 DOM 可以让你更改网页的交互方式。所有网页的交互都依赖这种 DOM 技术。DOM 是一颗树,树枝和树叶都做了编号,通过脚本的函数去寻找哪一个枝干的哪一个叶子,对这个叶子做什么改变。例如

image

形如一颗倒长的树。一颗家谱树,而家谱树本身就是一种模型,其典型用法是表示表示人类家族谱系。

它很容易表明家族成员之间的关系,把复杂的关系简明地表示出来,因此这种模型非常适合表示一份html的文档:

DOM常见方法:

在以前,我们使用Jqueryzepto等库来操作DOM,之后在vueAngularReact等框架出现后,我们通过操作数据来控制DOM(绝大多数时候),越来越少的去直接操作DOM

但这并不代表原生操作不重要。相反,DOM操作才能有助于我们理解框架深层的内容

下面就来分析DOM常见的操作,主要分为:

  • 创建节点
  • 查询节点
  • 更新节点
  • 添加节点
  • 删除节点

创建节点

createElement

创建新元素,接受一个参数,即要创建元素的标签名

const divEl = document.createElement("div");

createTextNode

创建一个文本节点

const textEl = document.createTextNode("content");

createDocumentFragment

用来创建一个文档碎片,它表示一种轻量级的文档,主要是用来存储临时节点,然后把文档碎片的内容一次性添加到DOM

const fragment = document.createDocumentFragment();

当请求把一个DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment自身,而是它的所有子孙节点

createAttribute

创建属性节点,可以是自定义属性

const dataAttribute = document.createAttribute('custom');
consle.log(dataAttribute);

获取节点

querySelector

传入任何有效的css 选择器,即可选中单个 DOM元素(首个):

document.querySelector('.element')
document.querySelector('#element')
document.querySelector('div')
document.querySelector('[name="username"]')
document.querySelector('div + p > span')

如果页面上没有指定的元素时,返回 null

querySelectorAll

返回一个包含节点子树内所有与之相匹配的Element节点列表,如果没有相匹配的,则返回一个空节点列表

const notLive = document.querySelectorAll("p");

需要注意的是,该方法返回的是一个 NodeList的静态实例,它是一个静态的“快照”,而非“实时”的查询

getElement

document.getElementById('id属性值');返回拥有指定id的对象的引用
document.getElementsByClassName('class属性值');返回拥有指定class的对象集合
document.getElementsByTagName('标签名');返回拥有指定标签名的对象集合
document.getElementsByName('name属性值'); 返回拥有指定名称的对象结合
document/element.querySelector('CSS选择器');  仅返回第一个匹配的元素
document/element.querySelectorAll('CSS选择器');   返回所有匹配的元素
document.documentElement;  获取页面中的HTML标签
document.body; 获取页面中的BODY标签
document.all[''];  获取页面中的所有元素节点的对象集合型

除此之外,每个DOM元素还有parentNodechildNodesfirstChildlastChildnextSiblingpreviousSibling属性,关系图如下图所示

更新节点

innerHTML

不但可以修改一个DOM节点的文本内容,还可以直接通过HTML片段修改DOM节点内部的子树

// 获取<p id="p">...</p >
var p = document.getElementById('p');
// 设置文本为abc:
p.innerHTML = 'ABC'; // <p id="p">ABC</p >
// 设置HTML:
p.innerHTML = 'ABC <span style="color:red">RED</span> XYZ';
// <p>...</p >的内部结构已修改

innerText、textContent

自动对字符串进行HTML编码,保证无法设置任何HTML标签

// 获取<p id="p-id">...</p >
var p = document.getElementById('p-id');
// 设置文本:
p.innerText = '<script>alert("Hi")</script>';
// HTML被自动编码,无法设置一个<script>节点:
// <p id="p-id">&lt;script&gt;alert("Hi")&lt;/script&gt;</p >

两者的区别在于读取属性时,innerText不返回隐藏元素的文本,而textContent返回所有文本

style

DOM节点的style属性对应所有的CSS,可以直接获取或设置。遇到-需要转化为驼峰命名

// 获取<p id="p-id">...</p >
const p = document.getElementById('p-id');
// 设置CSS:
p.style.color = '#ff0000';
p.style.fontSize = '20px'; // 驼峰命名
p.style.paddingTop = '2em';

添加节点

innerHTML

如果这个DOM节点是空的,例如,<div></div>,那么,直接使用innerHTML = '<span>child</span>'就可以修改DOM节点的内容,相当于添加了新的DOM节点

如果这个DOM节点不是空的,那就不能这么做,因为innerHTML会直接替换掉原来的所有子节点

appendChild

把一个子节点添加到父节点的最后一个子节点

举个例子

<!-- HTML结构 -->
<p id="js">JavaScript</p >
<div id="list">
 <p id="java">Java</p >
 <p id="python">Python</p >
 <p id="scheme">Scheme</p >
</div>

添加一个p元素

const js = document.getElementById('js')
js.innerHTML = "JavaScript"
const list = document.getElementById('list');
list.appendChild(js);

现在HTML结构变成了下面

<!-- HTML结构 -->
<div id="list">
 <p id="java">Java</p >
 <p id="python">Python</p >
 <p id="scheme">Scheme</p >
 <p id="js">JavaScript</p >  <!-- 添加元素 -->
</div>

上述代码中,我们是获取DOM元素后再进行添加操作,这个js节点是已经存在当前文档树中,因此这个节点首先会从原先的位置删除,再插入到新的位置

如果动态添加新的节点,则先创建一个新的节点,然后插入到指定的位置

const list = document.getElementById('list'),
const haskell = document.createElement('p');
haskell.id = 'haskell';
haskell.innerText = 'Haskell';
list.appendChild(haskell);

insertBefore

把子节点插入到指定的位置,使用方法如下:

parentElement.insertBefore(newElement, referenceElement)

子节点会插入到referenceElement之前

setAttribute

在指定元素中添加一个属性节点,如果元素中已有该属性改变属性值

const div = document.getElementById('id')
div.setAttribute('class', 'white');//第一个参数属性名,第二个参数属性值。

删除节点

删除一个节点,首先要获得该节点本身以及它的父节点,然后,调用父节点的removeChild把自己删掉

// 拿到待删除节点:
const self = document.getElementById('to-be-removed');
// 拿到父节点:
const parent = self.parentElement;
// 删除:
const removed = parent.removeChild(self);
removed === self; // true

删除后的节点虽然不在文档树中了,但其实它还在内存中,可以随时再次被添加到别的位置

addEventListener()

addEventListener() 方法用于向指定元素添加监听事件。且同一元素目标可重复添加,不会覆盖之前相同事件,配合 removeEventListener() 方法来移除事件。

使用方法:

document.getElementById(元素id).addEventListener("click", function(){
 console.log("目标元素被点击了");
});

参数说明:有三个参数

参数一、事件名称,字符串,必填。

  • 事件名称不用带 "on" 前缀,点击事件直接写:"click",键盘放开事件写:"keyup"

参数二、执行函数,必填。

  • 填写需要执行的函数,如:function()

  • 当目标对象事件触发时,会传入一个事件参数,参数名称可自定义,如填写event,不需要也可不填写。 事件对象的类型取决于特定的事件。例如, “click” 事件属于 MouseEvent(鼠标事件) 对象。

? function(event){console.log(event)}

参数三、触发类型,布尔型,可空

  • true - 事件在捕获阶段执行
  • false - 事件在冒泡阶段执行,默认是false

19、BOM

BOM (Browser Object Model),浏览器对象模型,提供了独立于内容与浏览器窗口进行交互的对象

其作用就是跟浏览器做一些交互效果,比如如何进行页面的后退,前进,刷新,浏览器的窗口发生变化,滚动条的滚动,以及获取客户的一些信息如:浏览器品牌版本,屏幕分辨率

浏览器的全部内容可以看成DOM,整个浏览器可以看成BOM。区别如下:

在浏览器中,window对象有双重角色,即是浏览器窗口的一个接口,又是全局对象

因此所有在全局作用域中声明的变量、函数都会变成window对象的属性和方法


window

Bom的核心对象是window,它表示浏览器的一个实例

在浏览器中,window对象有双重角色,即是浏览器窗口的一个接口,又是全局对象

因此所有在全局作用域中声明的变量、函数都会变成window对象的属性和方法

var name = 'js每日一题';
function lookName(){
  alert(this.name);
}

console.log(window.name);  //js每日一题
lookName();                //js每日一题
window.lookName();         //js每日一题

关于窗口控制方法如下:

  • moveBy(x,y):从当前位置水平移动窗体x个像素,垂直移动窗体y个像素,x为负数,将向左移动窗体,y为负数,将向上移动窗体
  • moveTo(x,y):移动窗体左上角到相对于屏幕左上角的(x,y)点
  • resizeBy(w,h):相对窗体当前的大小,宽度调整w个像素,高度调整h个像素。如果参数为负值,将缩小窗体,反之扩大窗体
  • resizeTo(w,h):把窗体宽度调整为w个像素,高度调整为h个像素
  • scrollTo(x,y):如果有滚动条,将横向滚动条移动到相对于窗体宽度为x个像素的位置,将纵向滚动条移动到相对于窗体高度为y个像素的位置
  • scrollBy(x,y): 如果有滚动条,将横向滚动条向左移动x个像素,将纵向滚动条向下移动y个像素

window.open() 既可以导航到一个特定的url,也可以打开一个新的浏览器窗口

如果 window.open() 传递了第二个参数,且该参数是已有窗口或者框架的名称,那么就会在目标窗口加载第一个参数指定的URL

window.open('htttp://www.vue3js.cn','topFrame')
==> < a href=" " target="topFrame"></ a>

window.open() 会返回新窗口的引用,也就是新窗口的 window 对象

const myWin = window.open('http://www.vue3js.cn','myWin')

window.close() 仅用于通过 window.open() 打开的窗口

新创建的 window 对象有一个 opener 属性,该属性指向打开他的原始窗口对象


location
  • 提供了当前窗口中加载文档的信息,以及通常的导航功能
  • 既是window的属性,也是document的属性
  • 保存着当前加载文档的信息
  • 保存把URL解析为离散片段后能够通过属性访问的信息

url地址如下:

http://foouser:barpassword@www.wrox.com:80/WileyCDA/?q=javascript#contents

location属性描述如下:

属性名 例子 说明
hash "#contents" utl中#后面的字符,没有则返回空串
host www.wrox.com:80 服务器名称和端口号
hostname www.wrox.com 域名,不带端口号
href http://www.wrox.com:80/WileyCDA/?q=javascript#contents 完整url
pathname "/WileyCDA/" 服务器下面的文件路径
port 80 url的端口号,没有则为空
protocol http: 使用的协议
search ?q=javascript url的查询字符串,通常为?后面的内容

除了 hash之外,只要修改location的一个属性,就会导致页面重新加载新URL

location.href-- 返回或设置当前文档的 URL

location.search -- 返回 URL 中的查询字符串部分。例
如 http://www.dreamdu.com/dreamdu.php?id=5&name=dreamdu 返回包括(?)后面的内
容?id=5&name=dreamdu

location.hash -- 返回 URL#后面的内容,如果没有#,返回空

location.host -- 返回 URL 中的域名部分,例如 www.dreamdu.com

location.hostname -- 返回 URL 中的主域名部分,例如 dreamdu.com

location.pathname -- 返回 URL 的域名后的部分。例如 http://www.dreamdu.com/xhtml/ 返
回/xhtml/

location.port -- 返回 URL 中的端口部分。例如 http://www.dreamdu.com:8080/xhtml/ 返回
8080

location.protocol -- 返回 URL 中的协议部分。例如 http://www.dreamdu.com:8080/xhtml/ 返
回(//)前面的内容 http:

location.assign -- 设置当前文档的 URL

location.replace() -- 设置当前文档的 URL,并且在 history 对象的地址列表中移除这个
URL location.replace(url);

location.reload() -- 此方法可以重新刷新当前页面。这个方法会根据最有效的方式刷新页面,如果页面自上一次请求以来没有改变过,页面就会从浏览器缓存中重新加载

如果要强制从服务器中重新加载,传递一个参数true即可


navigator 对象主要用来获取浏览器的属性,区分浏览器类型。属性较多,且兼容性比较复杂

navigator.userAgent -- 返回用户代理头的字符串表示(就是包括浏览器版本信息等的字符串)

navigator.cookieEnabled -- 返回浏览器是否支持(启用)cookie

screen

保存的纯粹是客户端能力信息,也就是浏览器窗口外面的客户端显示器的信息,比如像素宽度和像素高度


history

history对象主要用来操作浏览器URL的历史记录,可以通过参数向前,向后,或者向指定URL跳转

常用的属性如下:

  • history.go()

接收一个整数数字或者字符串参数:向最近的一个记录中包含指定字符串的页面跳转,

history.go('maixaofei.com')

当参数为整数数字的时候,正数表示向前跳转指定的页面,负数为向后跳转指定的页面

history.go(3) //向前跳转三个记录
history.go(-1) //向后跳转一个记录
  • history.forward():向前跳转一个页面
  • history.back():向后跳转一个页面
  • history.length:获取历史记录数

20、js的几种设计模式

  1. 工厂模式

    • 简单的工厂模:可以理解为解决多个相似的问题(提示框,只是提示的文字需要修改)
    • 复杂的工厂模式:将其成员对象的实列化推迟到子类中,子类可以重写父类接口方法以便创建的时候指定自己的对象类型,各种UI组件,根据你要的类型不同(比如:按钮,提示框,表格等)
  2. 单例模式

    两个特点:一个类只有一个实例,并且提供可全局访问点 全局对象是最简单的单例模式:window demo:登录弹出框只需要实例化一次,就可以反复用了

  3. 模块模式

    模块模式的思路是为单体模式添加私有变量和私有方法能够减少全局变量的使用 demo:返回对象的匿名函数。在这个匿名函数内部,先定义了私有变量和函数

  4. 代理模式

    代理对象可以代替本体被实例化,并使其可以被远程访问 demo: 虚拟代理实现图片的预加载

  5. 命令模式

    有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道请求的操作是什么,此时希望用一种松耦合的方式来设计程序代码;使得请求发送者和请求接受者消除彼此代码中的耦合关系。 demo:几个按钮绑定不同的事件,然后bindEvent(el, event);

  6. 发布订阅模式介绍

    发布—订阅模式又叫观察者模式,它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。 demo: 比如你向买房,只要把手机给房产中介,房产中介一有消息就发布消息。

  7. 适配器模式

    适配器模式主要解决两个接口之间不匹配的问题,不会改变原有的接口,而是由一个对象对另一个对象的包装。 demo:两个地图(2个类),他们有一个共同方法但是名字不同,这时候需要定义适配器类, 对其中的一个类进行封装。

21、JS 时区时间转换

/*将本地时间转换为东八区时间*/
var timezone = 8; //目标时区时间,东八区
var offset_GMT = new Date().getTimezoneOffset(); // 本地时间和格林威治的时间差,单位为分钟
var nowDate = new Date().getTime(); // 本地时间距 1970 年 1 月 1 日午夜(GMT 时间)之间的毫秒数
var targetDate = new Date(nowDate + offset_GMT * 60 * 1000 + timezone * 60 * 60 * 1000);
console.log("东8区现在是:" + targetDate);

22、JS 中 == 和 === 区别是什么?

  1. 对于string,number等基础类型,== 和 === 有区别
    • 不同类型间比较,之比较“转化成同一类型后的值”看“值”是否相等,=如果类型不同,其结果就是不等。
    • 同类型比较,直接进行“值”比较,两者结果一样。
  2. 基础类型与高级类型,== 和 === 有区别
    • 对于==,将高级转化为基础类型,进行“值”比较。
    • 因为类型不同,===结果为false。
  3. 对于Array,Object等高级类型,== 和===没有区别
    • 进行“指针地址”比较。

23、typeof 与 instanceof 区别

typeof 操作符返回一个字符串,表示未经计算的操作数的类型

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

typeof 使用如下

typeof 1 // 'number'
typeof NaN //'number'
NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN != NaN
为 true。

typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
typeof [] // 'object'
typeof {} // 'object'
typeof console // 'object'
typeof console.log // 'function'


从上面例子,前6个都是基础数据类型。虽然typeof null为object,但这只是JavaScript 存在的一个悠久 Bug,不代表null就是引用数据类型,并且null本身也不是对象

所以,null在 typeof之后返回的是有问题的结果,不能作为判断null的方法。如果你需要在 if 语句中判断是否为 null,直接通过===null来判断就好

同时,可以发现引用类型数据,用typeof来判断的话,除了function会被识别出来之外,其余的都输出object

如果我们想要判断一个变量是否存在,可以使用typeof:(不能使用if(a), 若a未声明,则报错)

if(typeof a != 'undefined'){
    //变量存在
}

instanceof使用如下:

object instanceof constructor

object为实例对象,constructor为构造函数

构造函数通过new可以实例对象,instanceof能判断这个对象是否是之前那个构造函数生成的对象

// 定义构建函数
let Car = function() {}
let benz = new Car()
benz instanceof Car // true
let car = new String('xxx')
car instanceof String // true
let str = 'xxx'
str instanceof String // false


instanceof实现

// 实现:

function myInstanceof(left, right) {
  let proto = Object.getPrototypeOf(left), // 获取对象的原型
    prototype = right.prototype; // 获取构造函数的 prototype 对象

  // 判断构造函数的 prototype 对象是否在对象的原型链上
  while (true) {
    if (!proto) return false;
    if (proto === prototype) return true;

    proto = Object.getPrototypeOf(proto);
  }
}

引申:如何判断一个对象是否属于某个类?

第一种方式是使用 instanceof 运算符来判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。

第二种方式可以通过对象的 constructor 属性来判断,对象的 constructor 属性指向该对象的构造函数,但是这种方式不是很安全,因为 constructor 属性可以被改写。

第三种方式,如果需要判断的是某个内置的引用类型的话,可以使用 Object.prototype.toString() 方法来打印对象的
[[Class]] 属性来进行判断。

由上可知,typeofinstanceof都是判断数据类型的方法,区别如下:

  • typeof会返回一个变量的基本类型,instanceof返回的是一个布尔值
  • instanceof 可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型
  • typeof 也存在弊端,它虽然可以判断基础数据类型(null 除外),但是引用数据类型中,除了function 类型以外,其他的也无法判断

可以看到,上述两种方法都有弊端,并不能满足所有场景的需求

如果需要通用检测数据类型,可以采用Object.prototype.toString,调用该方法,统一返回格式“[object Xxx]”的字符串

Object.prototype.toString({})       // "[object Object]"
Object.prototype.toString.call({})  // 同上结果,加上call也ok
Object.prototype.toString.call(1)    // "[object Number]"
Object.prototype.toString.call('1')  // "[object String]"
Object.prototype.toString.call(true)  // "[object Boolean]"
Object.prototype.toString.call(function(){})  // "[object Function]"
Object.prototype.toString.call(null)   //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(/123/g)    //"[object RegExp]"
Object.prototype.toString.call(new Date()) //"[object Date]"
Object.prototype.toString.call([])       //"[object Array]"
Object.prototype.toString.call(document)  //"[object HTMLDocument]"
Object.prototype.toString.call(window)   //"[object Window]"

js如何判断是不是数组

(构造函数+typeof,原型prototype,Array.isArray,Object.toString.call)

24、js的几种遍历方式

数组遍历方法,怎么终止数组遍历

终止循环的方法基本就是如下三种:

  • break - 中止当前循环,switch语句或label 语句,并把程序控制流转到紧接着被中止语句后面的语句;
  • continue - 终止当前循环或标记循环的当前迭代中的语句执行,并在下一次迭代时继续执行循环;
  • return - 终止函数的执行,并返回一个指定的值给函数调用者;
let arr = ['a', 'b', 'c', 'd']
for (let i = 0; i < arr.length; i++) {
  console.log('for:', arr[i])
  if (i === 2) {
    return
  }
}
// 输出 a b

for (let i = 0; i < arr.length; i++) {
  console.log('for:', arr[i])
  if (i === 2) {
    break
  }
}
// 输出 a b

for (let i = 0; i < arr.length; i++) {
  console.log('for:', arr[i])
  if (i === 2) {
    continue
  }
}
// 输出 a b d

常见的遍历方法

let arr = ['a', 'b', 'c']
let numArr = [1, 2, 3, 4, 5]
let user = {
  name: 'test',
  age: 18,
}
for (let i = 0; i < arr.length; i++) {
  console.log('for', arr[i])
}

// for in 遍历对象 i=key
for (let i in arr) {
  console.log('for in obj:', i)
}
// for in 遍历数组 i=index( 下标 )
for (let i in user) {
  console.log('for in arr:', i)
}

// for of 遍历数组 i = 数组中的值
// for of 不能遍历对象
for (let i of arr) {
  console.log('for of arr:', i)
}

// forEach 只能用于数组
// forEach 接受一个回调函数,第一个是此次循环的元素,第二个是此次循环的下标,第三个是当前正在操作的数组
// 除了抛出异常,没有其他办法终止 foreach 循环
arr.forEach((item, index, arr) => {
  console.log('arr foreach', item, index, arr)
  // return 会停止继续执行,但是循环不会停止
  // 因为当前的执行环境只是一个回调函数,所以 return 停止了当前函数的执行,但是循环并没有停止
  if (index === 1) {
    return false
  }
  console.log('test', index)
})

// map 只能用于数组
// 接受一个回调函数,第一个是此次循环的元素,第二个是此次循环的下标,第三个是当前正在操作的数组
// 除了抛出异常,没有其他办法终止循环
// 不会改变原数组,但是会返回根据 return 的值生成一个新的数组
let newArr = arr.map((item, index, arr) => {
  console.log(item, index, arr)
  return 'item:' + item
})
console.log(arr); // ['a', 'b', 'c', 'd']
console.log(newArr); // ['item:a', 'item:b', 'item:c', 'item:d']

// filter 只能用于数组
// 接受一个回调函数,第一个是此次循环的元素,第二个是此次循环的下标,第三个是当前正在操作的数组
// 除了抛出异常,没有其他办法终止循环
// 不会改变原数组,但是会返回将根据满足条件的值生成一个新的数组
let filterNewArr = arr.filter((item, index, arr) => {
  console.log(item, index, arr)
  return item !== 'a'
})
console.log(arr) // ['a', 'b', 'c', 'd']
console.log(filterNewArr) // ['b', 'c', 'd']

// 测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值
// 若收到一个空数组,此方法在一切情况下都会返回 true
// 如果有一个值不符合,则立即返回 false
let everyNum = numArr.every((item, index, arr) => {
  console.log(item, index, arr)
  return item > 3
})
console.log(everyNum)

// 测试数组中是不是至少有1个元素通过了被提供的函数测试。它返回一个布尔值
// 若收到一个空数组,此方法在一切情况下都会返回 false
// 如果有一个满足条件,则立即返回 true
let someNum = numArr.some((item, index, arr) => {
  console.log(item, index, arr)
  return item > 3
})
console.log(someNum)

// 返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。
let findNum = numArr.find((item, index, arr) => {
  console.log(item, index, arr)
  return item > 3
})
console.log(findNum)

// 返回数组中满足提供的测试函数的第一个元素的索引。若没有找到对应元素则返回-1。
let findIndexNum = numArr.findIndex((item, index, arr) => {
  console.log(item, index, arr)
  return item > 3
})
console.log(findIndexNum)

for in和for of的区别

1.共性
for of 和 for in都是用来遍历的属性

2.区别
for...in 语句用于遍历数组或者对象的属性(对数组或者对象的属性进行循环操作)。
for in得到对对象的key或数组,字符串的下标

for of和forEach一样,是直接得到值
for of不能用于对象

for..infor..of 的 本质区别 其实是:

  • for..in 用于枚举对象中的可枚举属性名。
  • for..of 用于遍历 可迭代对象 (即前文的:iterable)的元素。

迭代器

不应该通过一个对象是否能被遍历而确定它是否是 iterable,即使它能够被 for 和 for..in 遍历,那在程序内部中的其他方法也未必就会把它当作一个 iterable 。

那么该怎么区分 iterable 和 非iterbale 两者呢?

区别:是否实现了 iterable 接口。
iterable接口
实现 iterable接口 的对象,就 一定 会暴露了一个属性作为 默认的 iterator(即:迭代器,稍后会做介绍),且这个属性必须使用特殊的 Symbol.iterator 作为键。

这个属性的值必须是一个 迭代器工厂函数 ,调用这个工厂函数必须返回一个新的 迭代器。

可以输出这个工厂函数,看看检查对象是否实现了 iterable接口 如下:

console.log(obj[Symbol.iterator]); // undefine
console.log(arr[Symbol.iterator]); // ? values() { [native code] }

现在可以看到前面的 obj 并没有实现 iterable接口,与之相对的 arr 实现了 iterable接口,在结合 for..of 的特性,所以 for..of 才无法遍历 obj 而可以遍历 arr 。

enumerable

在JavaScript中,对象的属性分为可枚举和不可枚举之分,它们是由属性的enumerable值决定的。可枚举性决定了这个属性能否被for…in查找遍历到。

属性的枚举性会影响以下三个函数的结果

  1. for in (不可枚举的属性不会被遍历出来)
  2. Object.keys(只返回对象本身具有的可枚举的属性)
  3. JSON.stringify() (此方法也只读取对象本身可枚举属性,并序列化为JSON字符串)
  4. Object.assign() (此方法也是复制自身可枚举的属性,进行浅拷贝)
两者对比例子(遍历对象)
const obj = {
        a: 1,
        b: 2,
        c: 3
    }
    for (let i in obj) {
        console.log(i)    //输出 : a   b  c
    }
    for (let i of obj) {
        console.log(i)    //输出: Uncaught TypeError: obj is not iterable 报错了
    }

for infor of 对一个obj对象进行遍历,for in 正常的获取了对象的 key值,分别打印 a、b、c,而 for of却报错了。

两者对比例子(遍历数组)
   const arr = ['a', 'b', 'c']
   // for in 循环
   for (let i in arr) {
       console.log(i)         //输出  0  1  2
   }

   // for of
   for (let i of arr) {
       console.log(i)         //输出  a   b   c
   }

25、object getter/setter应用(访问器属性)

访问器属性(accessor properties),它们本质上是用于获取和设置值的函数,但从外部代码来看就像常规属性。

你还可以把getter/setter 用作“真实”属性值的包装器,进行值的限制。

示例1:假如你想禁止太短的 user 的 name,可以创建一个 setter name,并将值存储在一个单独的属性 _name 中,即

let user = {
  get name() {
    return this._name;
  },

  set name(value) {
    if (value.length < 4) {
      console.log("名字太多了,至少4个字符");
      return;
    }
    this._name = value;
  }
};
复制代码

这个时候你在对name进行设置

可以看到,只有满足条件的才可以设置成功。

示例中name 被存储在 _name 属性中,并通过 getter 和 setter 进行访问。从技术上讲,外部代码可以使用 user._name 直接访问 name。但是,这儿有一个众所周知的约定,即以下划线 "_" 开头的属性是内部属性,不应该从对象外部进行访问。

你还可以利用 getter 和 setter 替换“正常的”数据属性,来控制和调整这些属性的行为。

示例2:假如你开始是使用数据属性 nameage 来实现 user 对象

后面你对象里又不想要age了,想要改成birthday

这个时候存在一个问题,之前用到age属性的地方怎么办?

你可以找到所有的age,然后修改它,但是很明显这太麻烦了。这个时候你就可以使用getter来优雅的解决

function User(name, birthday) {
  this.name = name;
  this.birthday = birthday;

  // 年龄是根据当前日期和生日计算得出的
  Object.defineProperty(this, "age", {
    get() {
      let todayYear = new Date().getFullYear();
      return todayYear - this.birthday.getFullYear();
    }
  });
}
复制代码

现在你不但成功添加了birthday,同时age也仍然可以正常访问。

总结

getter

get语法将对象属性绑定到查询该属性时将被调用的函数。

语法:

{get prop() { ... } }

{get [expression]() { ... } }
复制代码

getter 使用场景

  • 需要允许访问返回动态计算值的属性。
  • 需要反映内部变量的状态,而不需要使用显式方法调用。

setter

当尝试设置属性时,set语法将对象属性绑定到要调用的函数。

语法:

{set prop(val) { . . . }}
{set [expression](val) { . . . }}
复制代码

setter 使用场景

  • 试着改变一个伪属性的值。(就像我们上面示例中的fullName)

26、什么是防抖和节流?有什么区别?如何实现?

本质上是优化高频率执行代码的一种手段

如:浏览器的 resizescrollkeypressmousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能

为了优化体验,需要对这类事件进行调用次数的限制,对此我们就可以采用 防抖(debounce)节流(throttle) 的方式来减少调用频率

防抖函数原理:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时

手写简化版:

// 防抖函数
const debounce = (fn, delay) => {
  let timer = null;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
};

节流函数原理:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效

手写简化版:

// 节流函数
const throttle = (fn, delay = 500) => {
  let flag = true;
  return (...args) => {
    if (!flag) return;
    flag = false;
    setTimeout(() => {
      fn.apply(this, args);
      flag = true;
    }, delay);
  };
};
// 节流函数-传参版
let throttle = (fn, delay = 500,s) => {
  let flag = true;
  return (...args) => {
    if (!flag) return;
    flag = false;
    setTimeout(() => {
      let qr=s.concat(args);
      fn.apply(this,qr);
      flag = true;
    }, delay);
  };
}
function aaa (s,e){
  console.log(s,e)
}
window.onresize = debounce(aaa,500,["8888"])

二、区别

相同点:

  • 都可以通过使用 setTimeout 实现
  • 目的都是,降低回调执行频率。节省计算资源

不同点:

  • 函数防抖,在一段连续操作结束后,处理回调,利用clearTimeoutsetTimeout实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能
  • 函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次

例如,都设置时间频率为500ms,在2秒时间内,频繁触发函数,节流,每隔 500ms 就执行一次。防抖,则不管调动多少次方法,在2s后,只会执行一次

如下图所示:

三、应用场景

防抖在连续的事件,只需触发一次回调的场景有:

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求
  • 手机号、邮箱验证输入检测
  • 窗口大小resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。

节流在间隔一段时间执行一次回调的场景有:

  • 滚动加载,加载更多或滚到底部监听
  • 搜索框,搜索联想功能

ES6

ES6+你熟悉么,用过哪些特性?

  • 箭头函数
  • 类及引入导出和继承( class/import/export/extends)
  • 字符串模板
  • Promise
  • let,const
  • async/await
  • 默认参数/参数或变量解构装饰器
  • Array.inclueds/String.padStart|String.padEnd/Object.assign

js中的箭头函数和普通函数有什么区别

  1. 样式不同,箭头函数是 =>,普通函数是 function;
  2. 箭头函数不能作为构造函数使用,也就不能使用 new 关键字;
  3. 箭头函数不绑定 arguments,可以考虑用剩余参数代替;
  4. 箭头函数会捕获其所在上下文的 this 值,作为自己的 this 值,定义的时候就确定了;
  5. call、apply、bind 并不会影响 this 的指向;
  6. 箭头函数没有原型属性;
  7. 箭头函数不能当作 Generator 函数,不能使用 yield 关键字;

var、let、const之间的区别?

  • var

    1. var声明变量可以重复声明,而let、const不可以重复声明。但let可以重新赋值。
    2. var是不受限于块级的,而let、const是受限于块级
    3. var声明的变量存在变量提升,即变量可以在声明之前调用,值为undefined。letconst不存在变量提升,即它们所声明的变量一定要在声明后使用,否则报错。这是因为var不存在暂时性死区,letconst存在暂时性死区,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
    4. var会与window相映射(会挂一个属性),而let不与window相映射
  • let

    1. 会产生块级作用域,不会造成变量提升,无法重新声明(但可以重新赋值);
  • const

    1. 是只读的,若是基本数据类型,具有不变性(无法重新赋值改动)
    2. 引用对象可以调整内部值(可能设计的时候没有考虑周全!)--------(都知道对象和数组是引用类型,const声明的a中保存的仅是数组和对象的指针,这就是说const仅保证指针不发生改变,修改数组的值不会改变对象的指针,所以是可以改的的。也就是说const定义的引用类型只要指针不发生改变,其他的不论如何改变都是可以的。)

    const定义的值部分能改,部分不能改

说一下Set,Map的区别

  1. 简述:

    • Set 和 Map 主要的应用场景在于 数据重组 和 数据储存。
    • Set 是一种叫做集合的数据结构,Map 是一种叫做字典的数据结构。
    // 去重数组
    Array.from(new Set(arr))
    // 简化
    [...new Set(arr)]
    
  2. 区别:

    • 共同点:Set、Map 可以储存不重复的值
    • 不同点:Map 是以 [value, value]的形式储存元素,Map 是以 [key, value] 的形式储存
  3. 总结:

    • Set:
      • 成员唯一、无序且不重复。
      • [value, value],键值与键名是一致的(或者说只有键值,没有键名)。
      • 可以遍历,方法有:add、delete、has。
    • Map:
      • 本质上是键值对的集合,类似集合。
      • 可以遍历,方法很多可以跟各种数据格式转换。
    • WeakSet:
      • 成员都是对象。
      • 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存DOM节点,不容易造成内存泄漏。
      • 不能遍历,方法有add、delete、has。
    • WeakMap:
      • 只接受对象作为键名(null除外),不接受其他类型的值作为键名。
      • 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的。
      • 不能遍历,方法有get、set、has、delete。

说说常见的几种去重方式、扁平化

数组去重

  1. 利用ES6 Set去重(ES6中最常用)
  2. 利用for嵌套for,然后splice去重(ES5中最常用)双层循环,外层循环元素,内层循环时比较值。值相同时,则删去这个值。
  3. 利用indexOf去重 新建一个空的结果数组,for 循环原数组,判断结果数组是否存在当前元素,如果有相同的值则跳过,不相同则push进数组。
  4. 利用sort() 排序方法,然后根据排序后的结果进行遍历及相邻元素比对。
  5. 利用includes() 检测数组是否有某个值
  6. 利用filter() + indexOf() 当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
  7. 利用递归去重
// 1.Set + 数组复制
fuction unique1(array){
    // Array.from(),对一个可迭代对象进行浅拷贝
    return Array.from(new Set(array))
}

// 2.Set + 扩展运算符浅拷贝
function unique2(array){
    // ... 扩展运算符
    return [...new Set(array)]
}



// 3.filter,判断是不是首次出现,如果不是就过滤掉
function unique3(array){
    return array.filter((item,index) => {
        return array.indexOf(item) === index
    })
}

// 4.创建一个新数组,如果之前没加入就加入
function unique4(array){
    let res = []
    array.forEach(item => {
        if(res.indexOf(item) === -1){
            res.push(item)
        }
    })
    return res
}


数组扁平化

function flat1(array){
    // reduce(): 对数组的每一项执行归并函数,这个归并函数的返回值会作为下一次调用时的参数,即 preValue
    // concat(): 合并两个数组,并返回一个新数组
    return array.reduce((preValue,curItem) => {
        return preValue.concat(Array.isArray(curItem) ? flat1(curItem) : curItem)
    },[])
}

function flat2(array){
    let res = []
    array.forEach(item => {
        if(Array.isArray(item)){
            // res.push(...flat2(item))
        // 如果遇到一个数组,递归
            res = res.concat(flat2(item))
        }
        else{
            res.push(item)
        }
    })
    return res
}

说一下es6中的Symbol Bigint

  • SymbolSymbol是一种基本类型。Symbol 通过调用symbol函数产生,它接收一个可选的名字参数,该函数返回的symbol是唯一的

proxy

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

本质:修改的是程序默认形为,就形同于在编程语言层面上做修改,属于元编程(meta programming)

var proxy = new Proxy(target, handler)

target表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理))

handler通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为

Handler

get()

get接受三个参数,依次为目标对象、属性名和 proxy 实例本身,最后一个参数可选。

get能够对数组增删改查进行拦截,下面是试下你数组读取负数的索引

set()

set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身

deleteProperty()

deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除

应用场景

Proxy其功能非常类似于设计模式中的代理模式,常用功能如下:

  • 拦截和监视外部对对象的访问
  • 降低函数或类的复杂度
  • 在复杂操作前对操作进行校验或对所需资源进行管理

import和common js

为什么需要模块化
  • 代码抽象
  • 代码封装
  • 代码复用
  • 依赖管理

如果没有模块化,我们代码会怎样?

  • 变量和方法不容易维护,容易污染全局作用域
  • 加载资源的方式通过script标签从上到下。
  • 依赖的环境主观逻辑偏重,代码较多就会比较复杂。
  • 大型项目资源难以维护,特别是多人合作的情况下,资源的引入会让人奔溃

因此,需要一种将JavaScript程序模块化的机制,如

  • CommonJs (典型代表:node.js早期)
  • AMD (典型代表:require.js)
  • CMD (典型代表:sea.js)
AMD

Asynchronous ModuleDefinition(AMD),异步模块定义,采用异步方式加载模块。所有依赖模块的语句,都定义在一个回调函数中,等到模块加载完成之后,这个回调函数才会运行

代表库为require.js

/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
  baseUrl: "js/lib",
  paths: {
    "jquery": "jquery.min",  //实际路径为js/lib/jquery.min.js
    "underscore": "underscore.min",
  }
});
// 执行基本操作
require(["jquery","underscore"],function($,_){
  // some code here
});
CommonJs

CommonJS 是一套 Javascript 模块规范,用于服务端

// a.js
module.exports={ foo , bar}

// b.js
const { foo,bar } = require('./a.js')

其有如下特点:

  • 所有代码都运行在模块作用域,不会污染全局作用域
  • 模块是同步加载的,即只有加载完成,才能执行后面的操作
  • 模块在首次执行后就会缓存,再次加载只返回缓存结果,如果想要再次执行,可清除缓存
  • require返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个值

既然存在了AMD以及CommonJs机制,ES6Module又有什么不一样?

ES Module

ES6 在语言标准的层面上,实现了Module,即模块功能,完全可以取代 CommonJSAMD规范,成为浏览器和服务器通用的模块解决方案

CommonJSAMD 模块,都只能在运行时确定这些东西。比如,CommonJS模块就是对象,输入时必须查找对象属性

// CommonJS模块
let { stat, exists, readfile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

ES6设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量

// ES6模块
import { stat, exists, readFile } from 'fs';

上述代码,只加载3个方法,其他方法不加载,即 ES6 可以在编译时就完成模块加载

由于编译加载,使得静态分析成为可能。包括现在流行的typeScript也是依靠静态分析实现功能

js异步编程

一、前后端通信的方式

原始模型

最初加载页面的方式——你为网站发送一个请求到服务器, 只要没有出错你将会获取资源并显示网页到你的电脑上。但问题是每次请求都要加载整个页面

XMLHttpRequest

XMLHttpRequest (XHR)现在是一个相当古老的技术 - 它是在20世纪90年代后期由微软发明的,并且已经在相当长的时间内跨浏览器进行了标准化。

不能跨域

let xhr = new XMLHttpRequest(); //1. 创建xml对象
xhr.open('GET', 'https://www.easy-mock.com/mock/5f5089e9eb182d5f62995f1c/xml/getNum');//2. 初始化请求
xhr.send();//3. 发送请求
xhr.onload = function() {//4. onload表示当请求正确并成功返回数据时调用
    console.log(JSON.parse(this.response))
    let num=JSON.parse(this.response).data.number
    document.body.innerHTML=num
};

Fetch

Fetch API基本上是XHR的一个现代替代品——它是最近在浏览器中引入的,它使异步HTTP请求在JavaScript中更容易实现,对于开发人员和在Fetch之上构建的其他API来说都是如此。

fetch('https://www.easy-mock.com/mock/5f5089e9eb182d5f62995f1c/xml/getNum')
.then(response => response.json())
.then(data => {
    let num=data.data.number
    document.body.innerHTML=num
});

AJAX模型

Ajax的全称是Asynchronous JavaScript And XML,即异步JavaScript和XML

通过使用诸如 XMLHttpRequest 、 Fetch API 等来实现,这些技术允许网页直接处理对服务器上可用的特定资源的 HTTP 请求,并在显示之前根据需要对结果数据进行格式化。简而言之,Ajax模型能使用Web API作为代理来更智能地请求数据,而不仅仅是让浏览器重新加载整个页面。

为了进一步提高速度,可以在首次请求时将资源存储在用户的计算机上,这意味着在后续访问中,他们将使用本地版本,而不是在首次加载页面时下载新副本。 内容仅在更新后从服务器重新加载。

Axios

Axios 是一个基于 HTTP 库的promise对象,可以用在浏览器和 node.js 中

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
axios.get('https://www.easy-mock.com/mock/5f5089e9eb182d5f62995f1c/xml/getNum')
  .then(function (response) {
    let num=response.data.data.number
    document.body.innerHTML=num
})
</script>

可以跳转至axios中文文档| axios介绍及应用了解更多

Jq中的ajax

可以引入jquery,使用其为我们封装好的xml,可以跨域

<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js"></script>
<script>
$.ajax('https://www.easy-mock.com/mock/5f5089e9eb182d5f62995f1c/xml/getNum',{
    type:"GET",	
    success:function(res){
        let num=res.data.number
        document.body.innerHTML=num
    }
})
</script>

Jsonp

JSON with Padding,是一种借助于script 标签发送跨域请求的技巧。它本质上不是一个请求,而是通过script标签请求一个服务端的PHP文件,这个文件返回的结果是一段Js,作用是调用我们事先定义好的一个函数,从而将服务端想要给客户端发过去的数据发送给客户端。
你可以自己封装:

function jsonp(url,params,callback){
    var funcName="jsonp_"+Date.now()+Math.random().toString().substr(2)
     if(typeOf params==="object"){
        var tempArr=[]
        for(var key in params){
            var value=params[key]
            tempArr.push(key+"="+value)
            params.tempArr.join("&")
        }
    }
    
    var script=document.createElement("script")
    script.src=url+"?"+params+"&callback"+funcName
    document.body.appendChild(script)

    window[funcName]=function(data){
        callback(data)
        //调用完就删掉
        delete window[funcName]
        document.body.removeChild(script)
    }
}

jsonp('url',{id:12},function(res){})//只能是get请求

也可以通过安装一个npm包使用:

$ npm install jsonp

二、xhr

XML 简介

XML 可扩展标记语言。
XML 被设计用来传输和存储数据。
XML 和HTML 类似,不同的是HTML 中都是预定义标签,而XML 中没有预定义标签,
全都是自定义标签,用来表示一些数据。

比如说我有一个学生数据:
name = “孙悟空” ; age = 18 ; gender = “男” ;
用XML 表示:

<student>
	<name>孙悟空</name>
	<age>18</age>
	<gender>男</gender>
</student>

现在已经被JSON 取代了。

{"name":"孙悟空","age":18,"gender":"男"}

XMLHttpRequest(XHR)对象用于与服务器交互。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。这允许网页在不影响用户操作的情况下,更新页面的局部内容。XMLHttpRequest 在 AJAX 编程中被大量使用。

尽管名称如此,XMLHttpRequest 可以用于获取任何类型的数据,而不仅仅是 XML。它甚至支持 HTTP 以外的协议(包括 file:// 和 FTP),尽管可能受到更多出于安全等原因的限制。

XMLHttpRequest()

该构造函数用于初始化一个 XMLHttpRequest 实例对象。在调用下列任何其他方法之前,必须先调用该构造函数,或通过其他方式,得到一个实例对象。

三、ajax

什么是ajax,回答:

我对 ajax 的理解是,它是一种异步通信的方法,通过直接由 js 脚本向服务器发起 http 通信,然后根据服务器返回的XML文档中提取数据,更新网页的相应部分,而不用刷新整个页面的一种方法。

创建一个 ajax 有这样几个步骤

首先是创建一个 XMLHttpRequest 对象。

然后在这个对象上使用 open 方法创建一个 http 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。

在发起请求前,我们可以为这个对象添加一些信息和监听函数。比如说我们可以通过 setRequestHeader 方法来为请求添加头信息。我们还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,我们可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候我们可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候我们就可以通过 response 中的数据来对页面进行更新了。

当对象的属性和监听函数设置完成后,最后我们调用 sent 方法来向服务器发起请求,可以传入参数作为发送的数据体。

区别 一般http请求与ajax请求

  1. ajax请求 是一种特别的 http请求
  2. 对服务器端来说, 没有任何区别, 区别在浏览器端
  3. 浏览器端发请求: 只有XHR 或fetch 发出的才是ajax 请求, 其它所有的都是非ajax 请求
  4. 浏览器端接收到响应
    (1) 一般请求: 浏览器一般会直接显示响应体数据, 也就是我们常说的刷新/跳转页面
    (2) ajax请求: 浏览器不会对界面进行任何更新操作, 只是调用监视的回调函数并传入响应相关数据

具体来说,AJAX 包括以下几个步骤。

  • 1.创建 XMLHttpRequest 对象,也就是创建一个异步调用对象
  • 2.创建一个新的 HTTP 请求,并指定该 HTTP 请求的方法、URL 及验证信息
  • 3.设置响应 HTTP 请求状态变化的函数
  • 4.发送 HTTP 请求
  • 5.获取异步调用返回的数据
  • 6.使用 JavaScript 和 DOM 实现局部刷新

API总结

  • XMLHttpRequest():创建 XHR 对象的构造函数
  • status:响应状态码值,如 200、404
  • statusText:响应状态文本,如 ’ok‘、‘not found’
  • readyState:标识请求状态的只读属性 0-1-2-3-4
  • onreadystatechange:绑定 readyState 改变的监听
  • responseType:指定响应数据类型,如果是 ‘json’,得到响应后自动解析响应
  • response:响应体数据,类型取决于 responseType 的指定
  • timeout:指定请求超时时间,默认为 0 代表没有限制
  • ontimeout:绑定超时的监听
  • onerror:绑定请求网络错误的监听
  • open():初始化一个请求,参数为:(method, url[, async])
  • send(data):发送请求
  • abort():中断请求 (发出到返回之间)
  • getResponseHeader(name):获取指定名称的响应头值
  • getAllResponseHeaders():获取所有响应头组成的字符串
  • setRequestHeader(name, value):设置请求头

一般实现:

const SERVER_URL = "/server";

let xhr = new XMLHttpRequest();

// 创建 Http 请求
xhr.open("GET", SERVER_URL, true);

// 设置状态监听函数
xhr.onreadystatechange = function() {
  if (this.readyState !== 4) return;

  // 当请求成功时
  if (this.status === 200) {
    handle(this.response);
  } else {
    console.error(this.statusText);
  }
};

// 设置请求失败时的监听函数
xhr.onerror = function() {
  console.error(this.statusText);
};

// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");

// 发送 Http 请求
xhr.send(null);


使用promise封装AJAX请求

// promise 封装实现:

function sendAJAX(url) {
         return new Promise((resolve, reject) => {
                //创建对象
                const xhr = new XMLHttpRequest();
                xhr.responseType = 'json';
                //初始化
                xhr.open('GET', url);
                //发送
                xhr.send();
                //处理响应结果
                xhr.onreadystatechange = function () {
                    if (xhr.readyState === 4) {
                        if (xhr.status >= 200 && xhr.status < 300) {
                            //输出响应体
                            resolve(xhr.response);
                        } else {
                            //输出响应状态码
                            reject(xhr.status);
                        }
                    }
                }
            });
     }
      
     sendAJAX("http://poetry.apiopen.top/sentences")
     .then(value=>{
              console.log(value);
         },reason=>{
             //控制台输出警告信息
               console.warn(reason);
         })

四、谈谈你对 Promise 的理解?

简介

Promise 是在 js 中进行异步编程的新解决方案。(以前旧的方案是单纯使用回调函数)
从语法来说,promise是一个构造函数。
从功能来说,promise对象用来封装一个异步操作,并且可以获得成功或失败的返回值。

回答:

1. Promise 是一个构造函数,我们可以通过该构造函数来生成Promise的实例。Promise 构造函数是同步执行的,promise.then 中的函数是异步执行的。
2. Promise 状态有三:pending(等待)、resolved(成功)、rejected(失败)。状态改变只能是 pending->resolved 或者 pending->rejected,状态一旦改变则不能再变
3. Promise 是对回调函数的一种封装,解决对调地狱的问题,我们可以通过Promise将自己的程序以同步的方式表达出来,从而可以解决代码臃肿及可读性差的问题。
4. axios采用Promise对象,发送ajax请求,获取数据,利用async和awiat方式类同步获取数据
5. Promise虽然解决了我们项目开发中的很多问题,但我们也不能无脑的滥用。比如Promise.all,如果参数中promise有一个失败(rejected),则此实例回调必然失败(reject),就不会再执行then方法的回调了。在实际中可能只是一个不关键的数据加载失败,往往会导致其他所有的数据不会显示,使得项目的容错性大大降低。所以我个人在开发过程中只会在必须依赖这几个步骤全部加载成功后才能继续向下执行的场景中采用它,比如图片的预加载功能。

使用promise的好处:

1.指定回调函数的方式更加灵活

旧的: 必须在启动异步任务前指定

promise: 启动异步任务 => 返回promie对象 => 给promise对象绑定回调函数(甚至可以在异步任务结束后指定)

2.支持链式调用(将异步操作以同步操作的流程表达出来), 可以解决回调地狱问题

什么是回调地狱? 回调函数嵌套调用, 外部回调函数异步执行的结果是嵌套的回调函数执行的条件

回调地狱的缺点? 不便于阅读 / 不便于异常处理

promise的状态改变

promise状态表示实例对象的一个属性
【PromiseState】。包括以下值:
(1)pending 未决定的
(2)resolved 或 fullfilled 成功
(3)rejected 失败
Promise对象的值表示实例对象的另一个属性
【PromiseResult】。保存着对象【成功/失败】的结果。而其状态改变只有以下两种可能:
(1)pending 变为resolved
(2)pending 变为 rejected
注:一个promise对象只能改变一次,无论成功或失败都会有一个结果数据,成功的称为 value , 失败的称为 reason 。

api

api相关

  1. 成功调用 resolve,失败调用 reject

  2. .then 获取结果,.catch 捕获异常。捕获异常还可通过 .then 的第二个参数

  3. .finally 无论成功失败都一定会调用

  4. 多个并发的请求,用 Promise.all()

    只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

    只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

  5. Promise.allSettled() 方法返回一个在所有给定的 promise 都已经fulfilledrejected后的 promise,并带有一个对象数组,每个对象表示对应的 promise 结果。

    当您有多个彼此不依赖的异步任务成功完成时,或者您总是想知道每个promise的结果时,通常使用它。

    相比之下,Promise.all() 更适合彼此相互依赖或者在其中任何一个reject时立即结束。

  6. Promise.race()方法

    Promise.race方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

    const p = Promise.race([p1, p2, p3]);
    

    上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

    Promise.race方法的参数与Promise.all方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。

问题

1.如何改变 promise的状态?

(1) resolve(value): 如果当前是pending就会变为resolved。
(2) reject(reason): 如果当前是pending就会变为rejected。
(3)抛出异常 throw :如果当前是pending就会变为rejected。

let p1 = new Promise((resolve,reject)=>{
           //  resolve('success');
         //    reject('error');
          //   throw 'error';
      })

2.一个 promise指定多个成功/失败回调函数,都会调用吗?

当promise改变为对应状态时都会调用。

let p = new Promise((resolve,reject)=>{
            resolve('success');
      })
      // 第一次回调
      p.then(value=>{
          console.log("yes");
      })
      // 第二次回调
      p.then(value=>{
          console.log("oh yes");
      })

3.改变 promise状态和指定回调函数谁先谁后?

(1)都有可能,正常情况下是先指定回调再改变状态,但也可以先改状态再指定回调
(2)如何先改状态再指定回调?
①在执行器中直接调用resolve(/reject());
②延迟 长时间才调用then();
(3)什么时候才能得到数据?
①如果先指定的回调, 那当状态发生改变时,回调函数就会调用,得到数据
②如果先改变的状态, 那当指定回调时,回调函数就会调用,得到数据

4. promise.then()返回的新promise的结果状态由什么决定?

(1)简单表达: then()指定的回调函数执行的结果决定。
(2)详细表达:
*如果抛出异常, 新promise变为rejected, reaon为抛出的异常。
*如果返回的是非prormise的任意值,新promise变为resolved, value为返回的值。
*如果返回的是另一个新promise,此promise的结果就会成为新promise的结果

let p = new Promise((resolve,reject) => {
            // resolve('success'); 
           // reject('No'); 
          //  throw 'oh no';
      });    
      let result = p.then(value => {
           console.log(value);
      }, reason => {
           console.warn(reason);   
      });     
      console.log(result);

5. promise 如何串连多个操作任务?

(1) promise 的then()返回一个新的promise,可以开成then()的链式调用。
(2)通过then的链式调用串连多个同步/异步任务。

 let p =new Promise((resolve,reject) => {
          resolve("yes");
     })
     p.then(value => {
          return new Promise((resolve,reject)=>{
              resolve("oh yes~");
          });
     }).then(value => {
          console.log(value);
     })

输出结果:
oh yes~

let p =new Promise((resolve,reject) => {
          resolve("yes");
     })
     p.then(value => {
          return new Promise((resolve,reject)=>{
              resolve("oh yes~");
          });
     }).then(value => {
          console.log(value);
     }).then(value => {
          console.log(value);
     })

输出结果:
oh yes~
undefined

6. promise 的异常穿透。

(1)当使用promise的then链式调用时,可以在最后指定失败的回调。
(2)前面任何操作出 了异常,都会传到最后失败的回调中处理。

 let p =new Promise((resolve,reject) => {
         setTimeout(()=>{
            resolve("yes");
         },1000);
     })
     p.then(value => {
          throw 'oh No';
     }).then(value => {
          console.log("123");
     }).then(value => {
          console.log("456");
     }).catch(reason=>{
         console.warn(reason);
     })

输出结果:
oh No

7.中断 promise链。

(1)当使用promise的then链式调用时,在中间中断,不再调用后面的回调函数。
(2)办法:在回调函数中返回一个pendding状态的promise对象。 return new Promise(()=>{});
未中断:

 let p =new Promise((resolve,reject) => {
         setTimeout(()=>{
            resolve("yes");
         },1000);
     })
     p.then(value => {
          console.log("789");
     }).then(value => {
          console.log("123");
     }).then(value => {
          console.log("456");
     }).catch(reason=>{
         console.warn(reason);
     })

输出结果:
789
123
456

中断:

 let p =new Promise((resolve,reject) => {
         setTimeout(()=>{
            resolve("yes");
         },1000);
     })
     p.then(value => {
          console.log("789");
          return new Promise(()=>{});
     }).then(value => {
          console.log("123");
     }).then(value => {
          console.log("456");
     }).catch(reason=>{
         console.warn(reason);
     })

输出结果:
789

Promise的自定义封装


五、Generator

理解async函数就要先理解generator函数,因为async就是Generator函数的语法糖

Generator 函数是 ES6 提供的一种异步编程解决方案,可以先理解为一个状态机,封装了多个内部状态,执行Generator函数返回一个遍历器(迭代器)对象,通过遍历器(迭代器)对象,可以依次遍历 Generator 函数内部的每一个状态。

语法上,Generator 函数是一个普通函数,但是有两个特征。
一是,function关键字与函数名之间有一个星号;
二是,函数体内部使用yield表达式,定义不同的内部状态

function* helloGenerator() {
  yield 'hello'
  yield 'Generator'
  return 'ending'
}

let Generator = helloGenerator()

调用Generator函数后并不执行,返回的也不是函数运行结果而是一个指向内部状态的指针对象,也就是遍历器对象(Iterator Object)。
必须调用遍历器对象的next方法,使得指针移向下一个状态。

console.log(Generator.next())  //  {value: 'hello', done: false}
console.log(Generator.next())  //  {value: 'Generator', done: false}
console.log(Generator.next())  //  {value: 'ending', done: true}

第一次调用next方法,Generator函数开始执行,直到遇到yield表达式为止。next方法返回一个对象,value属性就是当前yield表达式的值hello,done属性的值false,表示遍历还没有结束。
第二次调用next方法,Generator 函数从上次yield表达式停下的地方,一直执行到下一个yield表达式
继续调用next方法直到done属性值为true或者执行到return语句(如果没有return语句就执行到函数结束),表示遍历已经结束
如果再次调用next方法,此时Generator函数已经运行完毕,next方法返回对象的value属性为undefined,done属性为true。以后再调用next方法,返回的都是这个值

yield 表达式

可以理解为暂停的标志,遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

yield表达式与return语句都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。

一个函数里面,只能执行一次return语句,但是可以执行多次yield表达式。从另一个角度看,也可以说 Generator 生成了一系列的值,这也就是它的名称的来历(英语中,generator 这个词是“生成器”的意思)。另外需要注意,yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。

next方法的参数

next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值

function* foo(x) {
  let y = yield  x + 1
  let k = yield y + 2
  yield k / 2
  return k
}

let a = foo(1)

console.log(a.next())   //  {value: 2, done: false}
console.log(a.next(3))  //  {value: 5, done: false}
console.log(a.next(8))  //  {value: 4, done: false}
console.log(a.next())   //  {value: 8, done: true}

第一次运行next方法时,返回1+1的值2;第二次调用next方法,将上一次yield表达式的值设为3,y等于3,返回y + 2的值5;第三次调用next方法,将上一次yield表达式的值设为8,k等于8,返回k/2的值4
注意,由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。

next()、throw()、return()

除了next方法还有throw()、return()两个方法,这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。
next()是将yield表达式替换成一个值。
throw()是将yield表达式替换成一个throw语句。

const g = function* (x, y) {
  let result = yield x + y;
  return result;
};

const gen = g(1, 2);

gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y 替换成 let result = throw(new Error('出错了'));

return()是将yield表达式替换成一个return语句。

gen.return(2); // {value: 2, done: true}
// 相当于将 let result = yield x + y替换成 let result = return 2;

yield* 表达式

ES6提供了yield*表达式,用来在一个Generator函数里面执行另一个Generator函数。

function* foo(x) {
  
  yield 1
  yield* bar()
  yield 4
}

function* bar() {
  yield 2
  yield 3
}

let a = foo()

console.log(a.next())   //  {value: 1, done: false}
console.log(a.next())   //  {value: 2, done: false}
console.log(a.next())   //  {value: 3, done: false}
console.log(a.next())   //  {value: 4, done: false}
console.log(a.next())   //  {value: undefined, done: true}

由于yield* bar()语句得到的值,是一个遍历器,所以要用星号表示。运行结果就是使用一个遍历器,遍历了多个Generator函数,有递归的效果。
yield*后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个for...of循环。

六、async await

Generator语法糖

ES7 中引入了 async/await,async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。async 函数的实现原理,就是将 Generator函数和自动执行器,包装在一个函数里。

上面代码async函数就是将Generator函数的星号(*)替换成async,将yield替换成await,仅此而已 async函数对 Generator 函数的改进,体现在以下四点
  1. 内置执行器
    Generator 函数的执行必须靠执行器,需要调用next方法,才能真正执行,得到最后结果。
  2. 更好的语义
    async和await,比起星号和yield,语义更加清楚。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
  3. 更广的适用性
    co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)
  4. 返回值是promise
    async函数的返回值是Promise对象,比Generator函数的返回值是Iterator对象方便多了。可以用then方法指定下一步的操作。
    async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖

看上去好像我们从Generator/yield换到async/await只需要把*都改为async,yield都改为await就可以了。 所以很多人都直接拿Generator/yield来解释async/await的行为,但这会带来如下几个问题:

1.Generator有其他的用途,而不仅仅是用来帮助你处理Promise

2.这样的解释让那些不熟悉这两者的人理解起来更困难(因为你还要去解释那些类似co的库)

Async函数始终返回一个Promise

一个async函数,无论你return 1或者throw new Error()。在调用方来讲,接收到的始终是一个Promise对象:

async function throwError () {
  throw new Error()
}
async function returnNumber () {
  return 1
}

console.log(returnNumber() instanceof Promise) // true
console.log(throwError() instanceof Promise)   // true

Await是按照顺序执行的,并不能并行执行

JavaScript是单线程的,这就意味着await一只能一次处理一个,如果你有多个Promise需要处理,则就意味着,你要等到前一个Promise处理完成才能进行下一个的处理,这就意味着,如果我们同时发送大量的请求,这样处理就会非常慢,one by one:

 const bannerImages = []

 async function getImageInfo () {
   return bannerImages.map(async banner => await getImageInfo(banner))
 }

这样的四个定时器,我们需要等待4s才能执行完毕:

function delay () {
  return new Promise(resolve => setTimeout(resolve, 1000))
}
let tasks = [1, 2, 3, 4]
async function runner (tasks) {
  for (let task of tasks) {
    await delay()
  }
}
console.time('runner')
await runner(tasks)
console.timeEnd('runner')

对于这种情况,我们可以进行如下优化:

function delay () {
  return new Promise(resolve => setTimeout(resolve, 1000))
}
let tasks = [1, 2, 3, 4]
async function runner (tasks) {
  tasks = tasks.map(delay)
  await Promise.all(tasks)
}
console.time('runner')
await runner(tasks)
console.timeEnd('runner')

Promise对象在创建时就会执行函数内部的代码,也就意味着,在我们使用map创建这个数组时,所有的Promise代码都会执行,也就是说,所有的请求都会同时发出去,然后我们通过await Promise.all来监听所有Promise的响应。

结论Generator与async function都是返回一个特定类型的对象:

Generator: 一个类似

{ value: XXX, done: true }//这样结构的Object

Async: 始终返回一个Promise,使用await或者.then()来获取返回值


七、axios相关面试题

axios 是一个轻量的 HTTP客户端

基于 XMLHttpRequest 服务来执行 HTTP 请求,支持丰富的配置,支持 Promise,支持浏览器端和 Node.js 端。自Vue2.0起,尤大宣布取消对 vue-resource 的官方推荐,转而推荐 axios。现在 axios 已经成为大部分 Vue 开发者的首选

特性

  • 从浏览器中创建 XMLHttpRequests
  • node.js 创建 http请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换JSON 数据
  • 客户端支持防御XSRF

发送请求

axios({        
  url:'xxx',    // 设置请求的地址
  method:"GET", // 设置请求方法
  params:{      // get请求使用params进行参数凭借,如果是post请求用data
    type: '',
    page: 1
  }
}).then(res => {  
  // res为后端返回的数据
  console.log(res);   
})

并发请求axios.all([])

function getUserAccount() {
    return axios.get('/user/12345');
}

function getUserPermissions() {
    return axios.get('/user/12345/permissions');
}

axios.all([getUserAccount(), getUserPermissions()])
    .then(axios.spread(function (res1, res2) { 
    // res1第一个请求的返回的内容,res2第二个请求返回的内容
    // 两个请求都执行完成才会执行
}));

发送请求

import axios from 'axios';

axios(config) // 直接传入配置
axios(url[, config]) // 传入url和配置
axios[method](url[, option]) // 直接调用请求方式方法,传入url和配置
axios[method](url[, data[, option]]) // 直接调用请求方式方法,传入data、url和配置

axios.request(option) // 调用 request 方法

const axiosInstance = axios.create(config)
// axiosInstance 也具有以上 axios 的能力

axios.all([axiosInstance1, axiosInstance2]).then(axios.spread(response1, response2))
// 调用 all 和传入 spread 回调

请求拦截器

axios.interceptors.request.use(function (config) {
    // 这里写发送请求前处理的代码
    return config;
}, function (error) {
    // 这里写发送请求错误相关的代码
    return Promise.reject(error);
});

响应拦截器

axios.interceptors.response.use(function (response) {
    // 这里写得到响应数据后处理的代码
    return response;
}, function (error) {
    // 这里写得到错误响应处理的代码
    return Promise.reject(error);
});

取消请求

// 方式一
const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('xxxx', {
  cancelToken: source.token
})
// 取消请求 (请求原因是可选的)
source.cancel('主动取消请求');

// 方式二
const CancelToken = axios.CancelToken;
let cancel;

axios.get('xxxx', {
  cancelToken: new CancelToken(function executor(c) {
    cancel = c;
  })
});
cancel('主动取消请求');

八、跨域与解决

  1. 什么是跨越?

    一个网页向另一个不同域名/不同协议/不同端口的网页请求资源,这就是跨域。

    跨域原因产生:在当前域名请求网站中,默认不允许通过ajax请求发送其他域名。

  2. 为什么会产生跨域请求?

    因为浏览器使用了同源策略

  3. 什么是同源策略?

    同源策略是Netscape提出的一个著名的安全策略,现在所有支持JavaScript的浏览器都会使用这个策略。同源策略是浏览器最核心也最基本的安全功能,如果缺少同源策略,浏览器的正常功能可能受到影响。可以说web是构建在同源策略的基础之上的,浏览器只是针对同源策略的一种实现。

    同源: 协议、域名、端口号 必须完全相同。 违背同源策略就是跨域。

  4. 为什么浏览器要使用同源策略?

    是为了保证用户的信息安全,防止恶意网站窃取数据,如果网页之间不满足同源要求,将不能:

    1、共享Cookie、LocalStorage、IndexDB
    2、获取DOM
    3、AJAX请求不能发送

  5. 跨域的五个解决方式:

    1、前端使用jsonp (不推荐使用)
    ? 2、后台Http请求转发
    ? 3、后台配置同源Cors (推荐)
    ? 4、使用SpringCloud网关
    ? 5、使用nginx做转发 (推荐)

本课程提到了其中的两种:jsonp、cors

如何解决跨域问题?

相关知识点:

    1. 通过 jsonp 跨域
    1. 跨域资源共享(CORS)
    1. nginx 代理跨域
    1. nodejs 中间件代理跨域
    1. WebSocket 协议跨域

回答:

如果是像解决 ajax 无法提交跨域请求的问题,我们可以使用 jsonp、cors、websocket 协议、服务器代理来解决问题。

(1)使用 jsonp 来实现跨域请求,它的主要原理是通过动态构建 script 标签来实现跨域请求,因为浏览器对 script 标签的引入没有跨域的访问限制 。通过在请求的 url 后指定一个回调函数,然后服务器在返回数据的时候,构建一个 json 数据的包装,这个包装就是回调函数,然后返回给前端,前端接收到数据后,因为请求的是脚本文件,所以会直接执行,这样我们先前定义好的回调函数就可以被调用,从而实现了跨域请求的处理。这种方式只能用于 get 请求。

(2)使用 CORS 的方式,CORS 是一个 W3C 标准,全称是"跨域资源共享"。CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,因此我们只需要在服务器端配置就行。浏览器将 CORS 请求分成两类:简单请求和非简单请求。对于简单请求,浏览器直接发出 CORS 请求。具体来说,就是会在头信息之中,增加一个 Origin 字段。Origin 字段用来说明本次请求来自哪个源。服务器根据这个值,决定是否同意这次请求。对于如果 Origin 指定的源,不在许可范围内,服务器会返回一个正常的 HTTP 回应。浏览器发现,这个回应的头信息没有包含 Access-Control-Allow-Origin 字段,就知道出错了,从而抛出一个错误,ajax 不会收到响应信息。如果成功的话会包含一些以 Access-Control- 开头的字段。

非简单请求,浏览器会先发出一次预检请求,来判断该域名是否在服务器的白名单中,如果收到肯定回复后才会发起请求。

(3) nginx 代理跨域 nginx就扮演着浏览器与服务器之间的代理服务器,实现与浏览器同源来进行数据的传输。

(4)使用 websocket 协议,这个协议没有同源限制。

(5)使用服务器来代理跨域的访问请求,就是有跨域的请求操作时发送请求给后端,让后端代为请求,然后最后将获取的结果发返回。

5.1 JSONP

  1. JSONP 是什么?

JSONP(JSON with Padding),是一个非官方的跨域解决方案,纯粹凭借程序员的聪明 才智开发出来,只支持 get 请求。

  1. JSONP 怎么工作的?

在网页有一些标签天生具有跨域能力,比如:img link iframe script。 JSONP 就是利用 script 标签的跨域能力来发送请求的。

5.2 CORS

  1. CORS文档链接

  2. CORS是什么?

    CORS(Cross-Origin Resource Sharing),跨域资源共享。CORS 是官方的跨域解决方 案,它的特点是不需要在客户端做任何特殊的操作,完全在服务器中进行处理,支持 get 和 post 请求。跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些 源站通过浏览器有权限访问哪些资源

  3. CORS是怎么工作的?

    CORS 是通过设置一个响应头来告诉浏览器,该请求允许跨域,浏览器收到该响应 以后就会对响应放行

九、前后端分离与 RESTful 常见面试题

什么是前后端分离?有哪些优点?

后端只负责提供数据接口,不再渲染模板,前端获取数据并呈现
1.前后端解耦,接口复用(前端和客户端公用接口),减少开发量
2.各司其职,前后端同步开发,提升工作效率。定义好接口规范。
3.更有利于调度(mock)、测试和运维部署
4.缺点是对于单页面不好做seo

什么是RESTful

Respresentational State Transfer
1.表现层状态转移,由HTTP协议的主要设计者Roy Fielding提出
2.资源(Resources),表现层(Representation),状态转化(State Transfer
3.是一种以资源为中心的web软件架构风格,可以用AjaxRESTful web服务构建应用

RESTful解释
1.Resources(资源):使用URI指向一个实体
2.Representation(表现层):资源的表现形式,比如图片、HTML文本等
3.State Transer(状态转化):GET、POST、PUT、DELETE HTTP动词来操作资源,实现资源状态的改变

RESTful的准则
1.所有事物抽象为资源(resource),资源对应唯一的标识(identifier)
2.资源通过接口进行操作实现状态转移,操作本身是无状态的
3.对uqdir操作不会改变资源的标识

什么是RESTful API
RESTful风格的API接口:
1.通过HTTP GET/POST/PUT/DELETE 获取/新建/更新/删除资源
2.一般使用JSON格式返回数据
3.一般web框架都有相应的插件支持RESTful API

十、nginx的正向代理和反向代理

前端安全

一、有哪些可能引起前端安全的的问题?

  • 跨站脚本 (Cross-Site Scripting, XSS): 一种代码注入方式, 为了与 CSS 区分所以被称作 XSS. 早期常见于网络论坛, 起因是网站没有对用户的输入进行严格的限制, 使得攻击者可以将脚本上传到帖子让其他人浏览到有恶意脚本的页面, 其注入方式很简单包括但不限于 JavaScript / VBScript / CSS / Flash 等
  • iframe的滥用: iframe中的内容是由第三方来提供的,默认情况下他们不受我们的控制,他们可以在iframe中运行JavaScirpt脚本、Flash插件、弹出对话框等等,这可能会破坏前端用户体验
  • 跨站点请求伪造(Cross-Site Request Forgeries,CSRF): 指攻击者通过设置好的陷阱,强制对已完成认证的用户进行非预期的个人信息或设定信息等某些状态更新,属于被动攻击
  • 恶意第三方库: 无论是后端服务器应用还是前端应用开发,绝大多数时候我们都是在借助开发框架和各种类库进行快速开发,一旦第三方库被植入恶意代码很容易引起安全问题

二、XSS

XSS,跨站脚本攻击,允许攻击者将恶意代码植入到提供给其它用户使用的页面中

XSS涉及到三方,即攻击者、客户端与Web应用

XSS的攻击目标是为了盗取存储在客户端的cookie或者其他网站用于识别客户端身份的敏感信息。一旦获取到合法用户的信息后,攻击者甚至可以假冒合法用户与网站进行交互

举个例子:

一个搜索页面,根据url参数决定关键词的内容

<input type="text" value="<%= getParameter("keyword") %>">
<button>搜索</button>
<div>
  您搜索的关键词是:<%= getParameter("keyword") %>
</div>

这里看似并没有问题,但是如果不按套路出牌呢?

用户输入"><script>alert('XSS');</script>,拼接到 HTML 中返回给浏览器。形成了如下的 HTML:

<input type="text" value=""><script>alert('XSS');</script>">
<button>搜索</button>
<div>
  您搜索的关键词是:"><script>alert('XSS');</script>
</div>

浏览器无法分辨出 <script>alert('XSS');</script> 是恶意代码,因而将其执行,试想一下,如果是获取cookie发送对黑客服务器呢?

外在表现上,都有哪些攻击场景?---评论区、url

  1. 存储区:恶意代码存放的位置。
  2. 插入点:由谁取得恶意代码,并插入到网页上

根据攻击的来源,XSS攻击可以分成:

  • 存储型
  • 反射型
  • DOM 型

1)存储型server

场景:常见于带有用户保存数据的网站功能,攻击者通过可输入区域来注入恶意代码,如论坛发帖、商品评论,用户私信等。

2)反射型server

存储型的恶意代码通过可输入区域,存储在数据库中,而反射型的恶意代码拼接在url上。需要用户主动打开恶意的URL才能生效。

场景:通过URL传递参数的功能,如网站搜索、跳转等。

3)DOM型server

DOM型xss攻击中,取出和执行恶意代码由浏览器端完成,属于签到JavaScript自身的安全漏洞,而其他两种xss都属于服务端的安全漏洞。

场景:通过URL传递参数的功能,如网站搜索、跳转。


**如何防范xss攻击呢? ** Security

主旨:防止攻击者提交恶意代码,防止浏览器执行恶意代码。

1)对数据进行严格的输出编码:如HTML元素的编码、CSS编码、JS编码、URL编码等。

避免拼接HTML;Vue/React技术栈,避免使用v-html/dangerouslySetInnerHTML

2)CSP HTTP Header,即Content-Security-Policy(不支持CSP的旧版浏览器可以设置X-XSS-Protection)

增加攻击难度,配置CSP(本质上建立白名单,由浏览器进行拦截)
禁止加载外域代码,防止复杂的逻辑攻击
禁止外域提交,网站被攻击后,用户的数据不会泄露到外域


三、CSRF

CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求

利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目

一个典型的CSRF攻击有着如下的流程:

  • 受害者登录a.com,并保留了登录凭证(Cookie)
  • 攻击者引诱受害者访问了b.com
  • b.com 向 a.com 发送了一个请求:a.com/act=xx。浏览器会默认携带a.com的Cookie
  • a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求
  • a.com以受害者的名义执行了act=xx
  • 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作

csrf可以通过get请求,即通过访问img的页面后,浏览器自动访问目标地址,发送请求

同样,也可以设置一个自动提交的表单发送post请求,如下:

<form action="http://bank.example/withdraw" method=POST>
    <input type="hidden" name="account" value="xiaoming" />
    <input type="hidden" name="amount" value="10000" />
    <input type="hidden" name="for" value="hacker" />
</form>
<script> document.forms[0].submit(); </script> 

访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST操作

还有一种为使用a标签的,需要用户点击链接才会触发

访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST操作

< a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">
    重磅消息!!
<a/>

CSRF的特点

  • 攻击一般发起在第三方网站,而不是被攻击的网站。被攻击的网站无法防止攻击发生
  • 攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作;而不是直接窃取数据
  • 整个过程攻击者并不能获取到受害者的登录凭证,仅仅是“冒用”
  • 跨站请求可以用各种方式:图片URL、超链接、CORS、Form提交等等。部分请求方式可以直接嵌入在第三方论坛、文章中,难以进行追踪

CSRF的攻击类型?

GET类型的CSRF

GET类型的CSRF利用非常简单,只需要一个HTTP请求,一般会这样利用:

https://awps-assets.meituan.net/mit-x/blog-images-bundle-2018b/ff0cdbee.example/withdraw?amount=10000&for=hacker

在受害者访问含有这个img的页面后,浏览器会自动向http://bank.example/withdraw?account=xiaoming&amount=10000&for=hacker发出一次HTTP请求。bank.example就会收到包含受害者登录信息的一次跨域请求。

POST类型的CSRF

这种类型的CSRF利用起来通常使用的是一个自动提交的表单,如:

<form action="http://bank.example/withdraw" method=POST>
    <input type="hidden" name="account" value="xiaoming" />
    <input type="hidden" name="amount" value="10000" />
    <input type="hidden" name="for" value="hacker" />
</form>
<script> document.forms[0].submit(); </script>

访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST操作。

POST类型的攻击通常比GET要求更加严格一点,但仍并不复杂。任何个人网站、博客,被黑客上传页面的网站都有可能是发起攻击的来源,后端接口不能将安全寄托在仅允许POST上面。

链接类型的CSRF

链接类型的CSRF并不常见,比起其他两种用户打开页面就中招的情况,这种需要用户点击链接才会触发。这种类型通常是在论坛中发布的图片中嵌入恶意链接,或者以广告的形式诱导用户中招,攻击者通常会以比较夸张的词语诱骗用户点击,例如:

<a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">
  重磅消息!!
<a/>

由于之前用户登录了信任的网站A,并且保存登录状态,只要用户主动访问上面的这个PHP页面,则表示攻击成功。


CSRF的预防

CSRF通常从第三方网站发起,被攻击的网站无法防止攻击发生,只能通过增强自己网站针对CSRF的防护能力来提升安全性

防止csrf常用方案如下:

  • 阻止不明外域的访问
    • 同源检测
    • Samesite Cookie
  • 提交时要求附加本域才能获取的信息
    • CSRF Token
    • 双重Cookie验证

这里主要讲讲token这种形式,流程如下:

  • 用户打开页面的时候,服务器需要给这个用户生成一个Token
  • 对于GET请求,Token将附在请求地址之后。对于 POST 请求来说,要在 form 的最后加上
<input type=”hidden” name=”csrftoken” value=”tokenvalue”/>
  • 当用户从客户端得到了Token,再次提交给服务器的时候,服务器需要判断Token的有效性

四、网络劫持有哪几种?

网络劫持一般分为两种:

  • DNS劫持: (输入京东被强制跳转到淘宝这就属于dns劫持)
    • DNS强制解析: 通过修改运营商的本地DNS记录,来引导用户流量到缓存服务器
    • 302跳转的方式: 通过监控网络出口的流量,分析判断哪些内容是可以进行劫持处理的,再对劫持的内存发起302跳转的回复,引导用户获取内容
  • HTTP劫持: (访问谷歌但是一直有贪玩蓝月的广告),由于http明文传输,运营商会修改你的http响应内容(即加广告)

五、如何应对网络劫持?

DNS劫持由于涉嫌违法,已经被监管起来,现在很少会有DNS劫持,而http劫持依然非常盛行.

最有效的办法就是全站HTTPS,将HTTP加密,这使得运营商无法获取明文,就无法劫持你的响应内容.

六、HTTPS一定是安全的吗?

非全站HTTPS并不安全

以国内的工商银行为例

工商银行的首页不支持HTTPS

而工商银行的网银页面是支持HTTPS的

可能有人会问,登录页面支持HTTPS不就行了,首页又没有涉及账户信息.

其实这是非常不安全的行为,黑客会利用这一点进行攻击,一般是以下流程:

  1. 用户在首页点击「登录」,页面跳转到有https的网银页面,但此时由于首页是http请求,所以是明文的,这就会被黑客劫持
  2. 黑客劫持用户的跳转请求,将https网银页面地址转换为http的地址再发送给银行

用户 <== HTTP > 黑客 < HTTPS ==> 银行 3. 此时如果用户输入账户信息,那么会被中间的黑客获取,此时的账号密码就被泄露了

好在是工商银行的网银页面应该是开启了hsts和pre load,只支持https,因此上述攻击暂时是无效的

VUE

一、源码

1、Vue实例挂载过程发生了什么

回答范例
1.挂载过程指的是(app.mout)过程,这个是个初始化过程,整体上做了两件事:初始化和建立更新机制
2.初始化会创建组件实例、初始化组件状态、创建各种响应式数据
3.建立更新机制这一步会立即执行一次组件更新函数,这会首次执行组件渲染函数(render函数,主要是生成vnode)并执行patch将前面获得vnode转换为dom(_update主要功能是递归调用patch,将vnode转换为真实DOM,并且更新到页面中);同时首次执行渲染函数会创建它内部响应式数据和组件更新函数之间的依赖关系,这使得以后数据变化时会执行对应的更新函数。

2、请描述下你对vue生命周期的理解?在created和mounted这两个生命周期中请求数据有什么区别呢

Vue生命周期钩子会自动绑定 this 上下文到实例中,因此你可以访问数据,对 property 和方法进行运算这意味着你不能使用箭头函数来定义一个生命周期方法 (例如 created: () => this.fetchTodos())

列出概念

1.每个vue组件实例被创建后都会经过一系列初始化步骤,比如,它需要数据观测,模板编译,挂载实例到dom上,以及数据变化时更新dom。这个过程中会运行叫做生命周期钩子的函数,以便用户在特定阶段有机会添加他们自己的代码。

2.vue生命周期总共可以分为8个阶段:创建前后,载入前后,更新前后,销毁前后,以及一些特殊场景的生命周 期。vue3中新增了三个用于调试和服务端渲染场景

生命周期 描述
beforeCreate 组件实例被创建之初
created 组件实例已经完全创建
beforeMount 组件挂载之前
mounted 组件挂载到实例上去之后
beforeUpdate 组件数据发生变化,更新之前
updated 组件数据更新之后
beforeDestroy / beforeUnmounted(v3) 组件实例销毁之前
destroyed / unmounted(v3) 组件实例销毁之后
activated keep-alive 缓存的组件激活时
deactivated keep-alive 缓存的组件停用时调用
errorCaptured 捕获一个来自子孙组件的错误时被调用
renderTracked(v3) 调试钩子,响应式依赖被收集时时调用
renderTriggered(v3) 调试钩子,响应式依赖被触发时时调用
serverPrefetch(v3) ssr only,组件实例在服务器上被渲染前调用

3.流程图:(如图 v3中composition api再beforeCreate前面;v2中的options api在created和beforeCreate之间)

4.结合实践:

beforeCreate:通常用于插件开发中执行一些初始化任务

created:组件初始化完毕,可以访问各种数据,获取接口数据等

mounted:dom已创建,可用于获取访问数据和dom元素;访问子组件等。

beforeUpdate:此时view层还未更新,可用于获取更新前各种状态

updated:完成view层的更新,更新后,所有状态已是最新

beforeunmounted:实例被销毁前调用,可用于一些定时器或订阅的取消

unmounted:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器

第一次页面加载会触发哪几个钩子?

beforeCreate, created, beforeMount, mounted

简述每个周期具体适合哪些场景

  • beforeCreate:在new一个vue实例后,只有一些默认的生命周期钩子和默认事件,其他的东西都还没创建。在beforeCreate生命周期执行的时候,data和methods中的数据都还没有初始化。不能在这个阶段使用data中的数据和methods中的方法
  • create:data 和 methods都已经被初始化好了,如果要调用 methods 中的方法,或者操作 data 中的数据,最早可以在这个阶段中操作
  • beforeMount:执行到这个钩子的时候,在内存中已经编译好了模板了,但是还没有挂载到页面中,此时,页面还是旧的

? 这是挂载前的阶段,在这一阶段,我们虽然依然得不到具体的DOM 元素,但vue挂载的根节点已经创建,下面vue对DOM的操作将围绕 这个根元素继续进行。beforeMount这个阶段是过渡性的,一般一个 项目只能用到一两次。

  • mounted:执行到这个钩子的时候,就表示Vue实例已经初始化完成了。此时组件脱离了创建阶段,进入到了运行阶段。 如果我们想要通过插件操作页面上的DOM节点,最早可以在和这个阶段中进行

? 这是挂载后的阶段,mounted也是平时我们使用非常非常多的函数 了,一般我们的异步请求都写在这里。在这个阶段,数据和DOM都 已被渲染出来。

  • beforeUpdate: 当执行这个钩子时,页面中的显示的数据还是旧的,data中的数据是更新后的, 页面还没有和最新的数据保持同步
  • updated:页面显示的数据和data中的数据已经保持同步了,都是最新的
  • beforeDestory:Vue实例从运行阶段进入到了销毁阶段,这个时候上所有的 data 和 methods , 指令, 过滤器 ……都是处于可用状态。还没有真正被销毁
  • destroyed: 这个时候上所有的 data 和 methods , 指令, 过滤器 ……都是处于不可用状态。组件已经被销毁了。

可能的追问

  1. setup和created谁先执行?---setup

  2. setup中为什么没有beforeCreate和created?

    ---在vue3中定义变量和方法现在需要在setup当中定义,定义完之后如果想在template当中使用,需要return出去。

    1.setup的执行时机---beforeCreated之前
    beforeCreate:组件被创建出来,组件的methods和data还没初始化好
    created:组件被创建出来,组件的methods和data已经初始化好了

2.setup注意点
&1:由于在执行setup的时候,created还没有创建好,所以在setup函数内我们是无法使用data和methods的。
&2:所以vue为了让我们避免错误的使用,直接将setup函数内的this执行指向undefined
&3:setup函数只能是同步而不能是异步

setup()里访问组件的生命周期需要在生命周期钩子前加上“on”,并且没有beforeCreate和created生命周期钩子

因为 setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写。

数据请求在created和mouted的区别

created是在组件实例一旦创建完成的时候立刻调用,这时候页面dom节点并未生成;mounted是在页面dom节点渲染完毕之后就立刻执行的。触发时机上created是比mounted要更早的,两者的相同点:都能拿到实例对象的属性和方法。

讨论这个问题本质就是触发的时机,放在mounted中的请求有可能导致页面闪动(因为此时页面dom结构已经生成),但如果在页面加载前完成请求,则不会出现此情况。建议对页面内容的改动放在created生命周期当中

3、组件通信有几种方式

  1. 子组件使用props属性接收父组件传递过来的值

  2. 子组件通过$emit 来触发父组件中的一个事件

  3. vue中,我们可以使用自定义事件实现子组件向父组件传递数据

    getChildData(data){
    	//data是子组件触发事件传递的参数
    	console.log('child data is' +data)
    }
    <child-c v-on:get-child-data='getChildData'></child-c>
    
    //事件的名称推荐使用-分割
    
  4. 非父子组件、兄弟组件之间的数据传递

    /*新建一个Vue实例作为中央事件总线*/
    let event = new Vue();
    /*监听事件*/
    event.$on('eventName', (val) => {
        //......do something
    });
    /*触发事件*/
    event.$emit('eventName', 'this is a message.')
    
  5. Vuex 数据管理

? 扩展

  1. 组件通信方式大体有以下8种:
  • props
  • $emit/$on(删)
  • $children(删)/$parent
  • $attrs/$listeners(删)
  • ref
  • $root
  • eventbus
  • vuex
  1. 根据组件之间关系讨论组件通信最为清晰有效
  • 父子组件
    • props
    • $emit/$on(派发自定义事件)
    • $parent / $children(子代用$parent访问)
    • ref(父组件用ref方法访问孩子)
    • $attrs / $listeners(爷孙之间透传属性)
  • 兄弟组件(使用中间人做桥接)
    • $parent
    • eventbus
    • vuex
  • 跨层级关系
    • provide/inject(从上而下注入的方式)
    • $root
    • eventbus
    • vuex
Vue父组件通过props向子组件传递数据时,渲染过程

把父组件的data通过props传递给子组件的时候,子组件在初次渲染的时候生命周期或者render方法,有调用data相关的props的属性, 这样子组件也被添加到父组件的data的相关属性依赖中,这样父组件的data在set的时候,就相当于触发自身和子组件的update。

// main.vue
import Vue from 'vue'
import App from './App'
 
const root = new Vue({
  data: {
    state: false
  },
  mounted() {
    setTimeout(() => {
      this.state = true
    }, 1000)
  },
  render: function(h) {
    const { state } = this // state 变化重新触发render
    let root = h(App, { props: { status: state } })
    console.log('root:', root)
    return root
  }
}).$mount('#app')
 
window.root = root
// App.vue
<script>
export default {
  props: {
    status: Boolean
  },
  render: function (h){
    const { status } = this
    let app = h('h1', ['hello world'])
    console.log('app:', app)
    return app
  }
}
</script>

截图如下:

main.jsstate *状态发生了变化,由false => true, 触发了*自身子组件的render方法。

4、说一下 Vue 子组件和父组件创建和挂载顺序

思路分析

  1. 给结论
  2. 阐述理由

回答范例

  1. 创建过程自上而下,挂载过程自下而上;即:

    • parent created
    • child created
    • child mounted
    • parent mounted
  2. 之所以会这样是因为Vue创建过程是一个递归过程,先创建父组件,有子组件就会创建子组件,因此创建时先有父组件再有子组件;

    子组件首次创建时会添加mounted钩子到队列,等到patch结束再执行它们,可见子组件的mounted钩子是先进入到队列中的,因此等到patch结束执行这些钩子时也先执行。

5、Vue 组件 data 为什么必须是函数

因为js本身的特性带来的,如果 data 是一个对象,那么由于对象本身属于引用类型,当我们修改其中的一个属性时,会影响到所有Vue实例的数据。如果将 data 作为一个函数返回一个对象,那么每一个实例的 data 属性都是独立的,不会相互影响了

上面讲到组件data必须是一个函数,不知道大家有没有思考过这是为什么呢?

在我们定义好一个组件的时候,vue最终都会通过Vue.extend()构成组件实例

这里我们模仿组件构造函数,定义data属性,采用对象的形式

function Component(){
 
}

Component.prototype.data = {
	count : 0
}

创建两个组件实例

const componentA = new Component()
const componentB = new Component()

修改componentA组件data属性的值,componentB中的值也发生了改变

console.log(componentB.data.count)  // 0
componentA.data.count = 1
console.log(componentB.data.count)  // 1

产生这样的原因这是两者共用了同一个内存地址,componentA修改的内容,同样对componentB产生了影响

如果我们采用函数的形式,则不会出现这种情况(函数返回的对象内存地址并不相同

结论

  • 根实例对象data可以是对象也可以是函数(根实例是单例),不会产生数据污染情况

  • 组件实例对象data必须为函数,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染。采用函数的形式,initData时会将其作为工厂函数都会返回全新data对象

6、说说从 template 到 render 处理过程

问我们template到render过程,其实是问vue编译器工作原理。

思路

  1. 引入vue编译器概念
  2. 说明编译器的必要性
  3. 阐述编译器工作流程

回答范例

  1. Vue中有个独特的编译器模块,称为“compiler”,它的主要作用是将用户编写的template编译为js中可执行的render函数。
  2. 之所以需要这个编译过程是为了便于前端程序员能高效的编写视图模板。相比而言,我们还是更愿意用HTML来编写视图,直观且高效。手写render函数不仅效率底下,而且失去了编译期的优化能力。
  3. 在Vue中编译器会先对template进行解析,这一步称为parse,结束之后会得到一个JS对象,我们称为抽象语法树AST,然后是对AST进行深加工的转换过程,这一步成为transform,最后将前面得到的AST生成为JS代码,也就是render函数。

知其所以然

vue3编译过程窥探:

https://github1s.com/vuejs/core/blob/HEAD/packages/compiler-core/src/compile.ts#L61-L62

测试,test-v3.html

可能的追问

  1. Vue中编译器何时执行?
  2. react有没有编译器?

7、说一下Vue的双向绑定数据的原理

vue 实现数据双向绑定主要是:采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty() 来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应监听回调,通过diff算法,改变虚拟dom,实现页面刷新

双向绑定定义:我们先从单向绑定切入单向绑定非常简单,就是把Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新

双向绑定就很容易联想到了,在单向绑定的基础上,用户更新了ViewModel的数据也自动被更新了,这种情况就是双向绑

流程:

我们还是以Vue为例,先来看看Vue中的双向绑定流程是什么的

  1. new Vue()首先执行初始化,对data执行响应化处理,这个过程发生Observe
  2. 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile
  3. 同时定义?个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
  4. 由于data的某个key在?个视图中可能出现多次,所以每个key都需要?个管家Dep来管理多个Watcher
  5. 将来data中数据?旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数

流程图如下:

vue2.0双向数据绑定存在的问题

  1. Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对property执行 getter/setter转化,所以property必须在 data对象上存在才能让 Vue 将它转换为响应式的。
  2. 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue 当你修改数组的长度时,例如:vm.items.length = newLength vue是不能检测属性变化的

对于以上两种情况也有解决方案,使用$set方法

vue3.0中proxy数据双向绑定

  • Proxy 可以直接监听对象而非属性;
  • Proxy 可以直接监听数组的变化;
  • Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;
  • Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
  • Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;

vue2与vue3的区别是什么-----详见v3模块内容

区别于vue2Object.defineProperty一次性只能给对象的一个属性添加get&set方法,而Proxy一次性给对象所有属性都设置get&set方法

8、说一说你对vue响应式理解?

答题思路:

  1. 啥是响应式?
  2. 为什么vue需要响应式?
  3. 它能给我们带来什么好处?
  4. vue的响应式是怎么实现的?有哪些优缺点?
  5. vue3中的响应式的新变化

回答范例:

  1. 所谓数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制。
  2. mvvm框架中要解决的一个核心问题是连接数据层和视图层,通过数据驱动应用,数据变化,视图更新,要做到这点的就需要对数据做响应式处理,这样一旦数据发生变化就可以立即做出更新处理。
  3. 以vue为例说明,通过数据响应式加上虚拟DOM和patch算法,可以使我们只需要操作数据,完全不用接触繁琐的dom操作,从而大大提升开发效率,降低开发难度。
  4. vue2中的数据响应式会根据数据类型来做不同处理,如果是对象则采用Object.defineProperty()的方式定义数据拦截,当数据被访问或发生变化时,我们感知并作出响应;如果是数组则通过覆盖该数组原型的方法,扩展它的7个变更方法,使这些方法可以额外的做更新通知,从而作出响应。这种机制很好的解决了数据响应化的问题,但在实际使用中也存在一些缺点:比如初始化时的递归遍历会造成性能损失;新增或删除属性时需要用户使用Vue.set/delete这样特殊的api才能生效;对于es6中新产生的Map、Set这些数据结构不支持等问题。
  5. 为了解决这些问题,vue3重新编写了这一部分的实现:利用ES6的Proxy机制代理要响应化的数据,它有很多好处,编程体验是一致的,不需要使用特殊api,初始化性能和内存消耗都得到了大幅改善;另外由于响应化的实现代码抽取为独立的reactivity包,使得我们可以更灵活的使用它,我们甚至不需要引入vue都可以体验。

9、简单说一下diff算法

整体的策略就是:深度优先,同层比较

  1. diff算法是虚拟DOM技术的必然产物:通过新旧虚拟DOM作对比(即diff),将变化的地方更新在真实DOM作上;另外,也需要diff高效的执行对比过程,从而降低时间复杂度为O(n)。
  2. vue2中为了降低Watcher检度,每个组件只有一个Watcher与之对应,只有引入diff才能精确找到发生变化的地方。
  3. vue中diff执行的时刻是组件实例执行其更新函数时,它会比对上一次渲染结果oldVnode和新的渲染结果newVnode,此过程称为patch。
  4. diff过程整体遵循深度优先、同层比较的策略;两个节点之间比较会根据它们是否拥有子节点或者文本节点做不同操作;比较两组子节点是算法的重点,首先假设头尾节点可能相同做4次比对尝试,如果没有找到相同节点才按照通用方式遍历查找,查找结束再按情况处理剩下的节点;借助key通常可以非常精确找到相同节点,因此整个patch过程非常高效。

分析

必问题目,涉及vue更新原理,比较考查理解深度。

思路

  1. diff算法是干什么的
  2. 它的必要性
  3. 它何时执行
  4. 具体执行方式
  5. 拔高:说一下vue3中的优化

回答范例

1.Vue中的diff算法称为patching算法,它由Snabbdom修改而来,虚拟DOM要想转化为真实DOM就需要通过patch方法转换。

2.最初Vue1.x视图中每个依赖均有更新函数对应,可以做到精准更新,因此并不需要虚拟DOM和patching算法支持,但是这样粒度过细导致Vue1.x无法承载较大应用;Vue 2.x中为了降低Watcher粒度,每个组件只有一个Watcher与之对应,此时就需要引入patching算法才能精确找到发生变化的地方并高效更新。

3.vue中diff执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行render函数获得最新的虚拟DOM,然后执行patch函数,并传入新旧两次虚拟DOM,通过比对两者找到变化的地方,最后将其转化为对应的DOM操作。

4.patch过程是一个递归过程,遵循深度优先、同层比较的策略;以vue3的patch为例:

  • 首先判断两个节点是否为相同同类节点,不同则删除重新创建
  • 如果双方都是文本则更新文本内容
  • 如果双方都是元素节点则递归更新子元素,同时更新元素属性
  • 更新子节点时又分了几种情况:
    • 新的子节点是文本,老的子节点是数组则清空,并设置文本;
    • 新的子节点是文本,老的子节点是文本则直接更新文本;
    • 新的子节点是数组,老的子节点是文本则清空文本,并创建新子节点数组中的子元素;
    • 新的子节点是数组,老的子节点也是数组,那么比较两组子节点,更新细节

5.vue3中引入的更新策略:编译期优化patchFlags、block等

-----vue2diff算法缺陷

10、说说你对虚拟 DOM 的理解?

现有框架几乎都引入了虚拟 DOM 来对真实 DOM 进行抽象,也就是现在大家所熟知的 VNode 和 VDOM,那么为什么需要引入虚拟 DOM 呢?围绕这个疑问来解答即可!

思路

  1. vdom是什么
  2. 引入vdom的好处
  3. vdom如何生成,又如何成为dom
  4. 在后续的diff中的作用

回答范例

  1. 虚拟dom顾名思义就是虚拟的dom对象,它本身就是一个 JavaScript 对象,只不过它是通过不同的属性去描述一个视图结构。

  2. 通过引入vdom我们可以获得如下好处:

    将真实元素节点抽象成 VNode,有效减少直接操作 dom 次数,从而提高程序性能

    • 直接操作 dom 是有限制的,比如:diff、clone 等操作,一个真实元素上有许多的内容,如果直接对其进行 diff 操作,会去额外 diff 一些没有必要的内容;同样的,如果需要进行 clone 那么需要将其全部内容进行复制,这也是没必要的。但是,如果将这些操作转移到 JavaScript 对象上,那么就会变得简单了。
    • 操作 dom 是比较昂贵的操作,频繁的dom操作容易引起页面的重绘和回流,但是通过抽象 VNode 进行中间处理,可以有效减少直接操作dom的次数,从而减少页面重绘和回流。

    方便实现跨平台

    • 同一 VNode 节点可以渲染成不同平台上的对应的内容,比如:渲染在浏览器是 dom 元素节点,渲染在 Native( iOS、Android) 变为对应的控件、可以实现 SSR 、渲染到 WebGL 中等等
    • Vue3 中允许开发者基于 VNode 实现自定义渲染器(renderer),以便于针对不同平台进行渲染。
  3. vdom如何生成?在vue中我们常常会为组件编写模板 - template, 这个模板会被编译器 - compiler编译为渲染函数,在接下来的挂载(mount)过程中会调用render函数,返回的对象就是虚拟dom。但它们还不是真正的dom,所以会在后续的patch过程中进一步转化为dom。

  4. 挂载过程结束后,vue程序进入更新流程。如果某些响应式数据发生变化,将会引起组件重新render,此时就会生成新的vdom,和上一次的渲染结果diff就能得到变化的地方,从而转换为最小量的dom操作,高效更新视图。

11、你知道vue中key的原理吗?说说你对它的理解

这是一道特别常见的问题,主要考查大家对虚拟DOM和patch细节的掌握程度,能够反映面试者理解层次。

思路分析:总分总模式

  1. 给出结论,key的作用是用于优化patch性能
  2. key的必要性
  3. 实际使用方式
  4. 总结:可从源码层面描述一下vue如何判断两个节点是否相同

回答范例:

  1. key的作用主要是为了更高效的更新虚拟DOM。
  2. vue在patch过程中判断两个节点是否是相同节点是key是一个必要条件,渲染一组列表时,key往往是唯一标识,所以如果不定义key的话,vue只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch过程比较低效,影响性能。
  3. 实际使用中在渲染一组列表时key必须设置,而且必须是唯一标识,应该避免使用数组索引作为key,这可能导致一些隐蔽的bug;vue中在使用相同标签元素过渡切换时,也会使用key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果。
  4. 从源码中可以知道,vue判断两个节点是否相同时主要判断两者的key和元素类型等,因此如果不设置key,它的值就是undefined,则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的dom更新操作,明显是不可取的。

测试代码,test.html

上面案例重现的是以下过程

不使用key

如果使用key

// 首次循环patch A
A B C D E
A B F C D E

// 第2次循环patch B
B C D E
B F C D E

// 第3次循环patch E
C D E
F C D E

// 第4次循环patch D
C D
F C D

// 第5次循环patch C
C 
F C

// oldCh全部处理结束,newCh中剩下的F,创建F并插入到C前面

二、指令

1、Vue常见修饰符

一、修饰符是什么

在程序世界里,修饰符是用于限定类型以及类型成员的声明的一种符号

Vue中,修饰符处理了许多DOM事件的细节,让我们不再需要花大量的时间去处理这些烦恼的事情,而能有更多的精力专注于程序的逻辑处理

vue中修饰符分为以下五种:

  • 表单修饰符
  • 事件修饰符
  • 鼠标按键修饰符
  • 键值修饰符
  • v-bind修饰符

二、修饰符的作用

表单修饰符

在我们填写表单的时候用得最多的是input标签,指令用得最多的是v-model

关于表单的修饰符有如下:

  • lazy
  • trim
  • number

lazy

在我们填完信息,光标离开标签的时候,才会将值赋予给value,也就是在change事件之后再进行信息同步

<input type="text" v-model.lazy="value">
<p>{{value}}</p>

trim

自动过滤用户输入的首空格字符,而中间的空格不会过滤

<input type="text" v-model.trim="value">

number

自动将用户的输入值转为数值类型,但如果这个值无法被parseFloat解析,则会返回原来的值

<input v-model.number="age" type="number">

事件修饰符

事件修饰符是对事件捕获以及目标进行了处理,有如下修饰符:

  • stop
  • prevent
  • self
  • once
  • capture
  • passive
  • native

stop

阻止了事件冒泡,相当于调用了event.stopPropagation方法

<div @click="shout(2)">
  <button @click.stop="shout(1)">ok</button>
</div>
//只输出1

prevent

阻止了事件的默认行为,相当于调用了event.preventDefault方法

<form v-on:submit.prevent="onSubmit"></form>

self

只当在 event.target 是当前元素自身时触发处理函数

<div v-on:click.self="doThat">...</div>

使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击

once

绑定了事件以后只能触发一次,第二次就不会触发

<button @click.once="shout(1)">ok</button>

capture

使事件触发从包含这个元素的顶层开始往下触发

<div @click.capture="shout(1)">
    obj1
<div @click.capture="shout(2)">
    obj2
<div @click="shout(3)">
    obj3
<div @click="shout(4)">
    obj4
</div>
</div>
</div>
</div>
// 输出结构: 1 2 4 3 

passive

在移动端,当我们在监听元素滚动事件的时候,会一直触发onscroll事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符

<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成  -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div v-on:scroll.passive="onScroll">...</div>

不要把 .passive.prevent 一起使用,因为 .prevent 将会被忽略,同时浏览器可能会向你展示一个警告。

passive 会告诉浏览器你不想阻止事件的默认行为

native

让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事件

<my-component v-on:click.native="doSomething"></my-component>

使用.native修饰符来操作普通HTML标签是会令事件失效的

鼠标按钮修饰符

鼠标按钮修饰符针对的就是左键、右键、中键点击,有如下:

  • left 左键点击
  • right 右键点击
  • middle 中键点击
<button @click.left="shout(1)">ok</button>
<button @click.right="shout(1)">ok</button>
<button @click.middle="shout(1)">ok</button>

键盘修饰符

键盘修饰符是用来修饰键盘事件(onkeyuponkeydown)的,有如下:

keyCode存在很多,但vue为我们提供了别名,分为以下两种:

  • 普通键(enter、tab、delete、space、esc、up...)
  • 系统修饰键(ctrl、alt、meta、shift...)
// 只有按键为keyCode的时候才触发
<input type="text" @keyup.keyCode="shout()">

还可以通过以下方式自定义一些全局的键盘码别名

Vue.config.keyCodes.f2 = 113

v-bind修饰符

v-bind修饰符主要是为属性进行操作,用来分别有如下:

  • async
  • prop
  • camel

async

能对props进行一个双向绑定

//父组件
<comp :myMessage.sync="bar"></comp> 
//子组件
this.$emit('update:myMessage',params);

以上这种方法相当于以下的简写

//父亲组件
<comp :myMessage="bar" @update:myMessage="func"></comp>
func(e){
 this.bar = e;
}
//子组件js
func2(){
  this.$emit('update:myMessage',params);
}

使用async需要注意以下两点:

  • 使用sync的时候,子组件传递的事件名格式必须为update:value,其中value必须与子组件中props中声明的名称完全一致
  • 注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用
  • v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的

props

设置自定义标签属性,避免暴露数据,防止污染HTML结构

<input id="uid" title="title1" value="1" :index.prop="index">

camel

将命名变为驼峰命名法,如将view-Box属性名转换为 viewBox

<svg :viewBox="viewBox"></svg>

三、应用场景

根据每一个修饰符的功能,我们可以得到以下修饰符的应用场景:

  • .stop:阻止事件冒泡
  • .native:绑定原生事件
  • .once:事件只执行一次
  • .self :将事件绑定在自身身上,相当于阻止事件冒泡
  • .prevent:阻止默认事件
  • .caption:用于事件捕获
  • .once:只触发一次
  • .keyCode:监听特定键盘按下
  • .right:右键

2、说说 vue 内置指令

  • v-once - 定义它的元素或组件只渲染一次,包括元素或组件的所有节点,首次渲染后,不再随数据的变化重新渲染,将被视为静态内容。
  • v-cloak - 这个指令保持在元素上直到关联实例结束编译 -- 解决初始化慢到页面闪动的最佳实践。
  • v-bind - 绑定属性,动态更新HTML元素上的属性。例如 v-bind:class。
  • v-on - 用于监听DOM事件。例如 v-on:click v-on:keyup
  • v-html - 赋值就是变量的innerHTML -- 注意防止xss攻击
  • v-text - 更新元素的textContent
  • v-model - 1、在普通标签。变成value和input的语法糖,并且会处理拼音输入法的问题。2、再组件上。也是处理value和input语法糖。
  • v-if / v-else / v-else-if。可以配合template使用;在render函数里面就是三元表达式。
  • v-show - 使用指令来实现 -- 最终会通过display来进行显示隐藏
  • v-for - 循环指令编译出来的结果是 -L 代表渲染列表。优先级比v-if高最好不要一起使用,尽量使用计算属性去解决。注意增加唯一key值,不要使用index作为key。
  • v-pre - 跳过这个元素以及子元素的编译过程,以此来加快整个项目的编译速度。

v-if 和 v-show 区别

共同点:都能控制元素的显示和隐藏; 不同点:实现本质方法不同,v-show本质就是通过控制css中的display设置为none,控制隐藏,只会编译一次;v-if是动态的向DOM树内添加或者删除DOM元素,若初始值为false,就不会编译了。而且v-if不停的销毁和创建比较消耗性能。

总结:如果要频繁切换某节点,使用v-show(切换开销比较小,初始开销较大)。如果不需要频繁切换某节点使用v-if(初始渲染开销较小,切换开销比较大)。

v-for为什么要加key

如果不使用key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。key 是为Vue中Vnode的唯一标识,通过这个key,我们的diff操作可以更准确、更快速。 更准确:因为带key就不是就地复用了,在sameNode函数 a.key === b.key 对比中可以避免就地复用的情况。所以更加准确。 更快速:利用key的唯一性生成map对象来获取对应节点,比遍历方式块。

对 keep-alive 的了解

keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染

3、请说出对v-model指令的理解

思路分析:

  1. 给出双绑定义
  2. 双绑带来的好处
  3. 在哪使用双绑
  4. 使用方式
  5. 扩展:使用细节、原理实现描述

回答范例:

  1. vue中双向绑定是一个指令v-model,可以绑定一个动态值到视图,同时视图中变化能改变该值。v-model是语法糖,默认情况下相当于:value和@input。
  2. 使用v-model可以减少大量繁琐的事件处理代码,提高开发效率,代码可读性也更好
  3. 通常在表单项上使用v-model
  4. 原生的表单项可以直接使用v-model,自定义组件上如果要使用它需要在组件内绑定value并处理输入事件
  5. 我做过测试,输出包含v-model模板的组件渲染函数,发现它会被转换为value属性的绑定以及一个事件监听,事件回调函数中会做相应变量更新操作,这说明神奇魔法实际上是vue的编译器完成的。

可能的追问:

  1. v-model和sync修饰符有什么区别

?

  1. 自定义组件使用v-model如果想要改变事件名或者属性名应该怎么做

4、v-if和v-for的优先级是什么?

v-ifv-for都是vue模板系统中的指令

vue模板编译的时候,会将指令系统转化成可执行的render函数

回答范例:

  1. Vue 2 中,v-for 优先于 v-if 被解析;但在 Vue 3 中,则完全相反,v-if 的优先级高于 v-for
  2. 我曾经做过实验,把它们放在一起,输出的渲染函数中可以看出会先执行循环再判断条件
  3. 实践中也不应该把它们放一起,因为哪怕我们只渲染列表中一小部分元素,也得在每次重渲染的时候遍历整个列表。
  4. 通常有两种情况下导致我们这样做:
    • 为了过滤列表中的项目 (比如 v-for="user in users" v-if="user.isActive")。此时定义一个计算属性 (比如 activeUsers),让其返回过滤后的列表即可。
    • 为了避免渲染本应该被隐藏的列表 (比如 v-for="user in users" v-if="shouldShowUsers")。此时把 v-if 移动至容器元素上 (比如 ulol)即可。
  5. 文档中明确指出永远不要把 v-ifv-for 同时用在同一个元素上,显然这是一个重要的注意事项。
  6. 看过源码里面关于代码生成的部分,

5、Vue 中怎么自定义指令

全局注册主要是通过Vue.directive方法进行注册

局部注册通过在组件options选项中设置directive属性

/*全局注册*/
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function (el) {
    // 聚焦元素
    el.focus()
  }
})
/*局部注册*/
directives: {
  focus: {
    // 指令的定义
    inserted: function (el) {
      el.focus()
    }
  }
}

使用自定义指令可以满足我们日常一些场景,这里给出几个自定义指令的案例:

  • 表单防止重复提交
  • 图片懒加载
  • 一键 Copy的功能
  • 还有很多应用场景,如:拖拽指令、页面水印、权限校验等等应用场景

6、v-bind和v-on

三、api相关、内置属性、内置标签

1、Vue中的$nextTick有什么作用?

这道题考查大家对vue异步更新队列的理解,有一定深度,如果能够很好回答此题,对面试效果有极大帮助。

答题思路:

  1. nextTick是啥?下一个定义
  2. 为什么需要它呢?用异步更新队列实现原理解释
  3. 我再什么地方用它呢?抓抓头,想想你在平时开发中使用它的地方
  4. 下面介绍一下如何使用nextTick
  5. 最后能说出源码实现就会显得你格外优秀

先看看官方定义

Vue.nextTick( [callback, context] )

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
// DOM 更新了
})

回答范例:

  1. nextTick是Vue提供的一个全局API,由于vue的异步更新策略导致我们对数据的修改不会立刻体现在dom变化上,此时如果想要立即获取更新后的dom状态,就需要使用这个方法
  2. Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。nextTick方法会在队列中加入一个回调函数,确保该函数在前面的dom操作完成后才调用。
  3. 所以当我们想在修改数据后立即看到dom执行结果就需要用到nextTick方法。
  4. 比如,我在干什么的时候就会使用nextTick,传一个回调函数进去,在里面执行dom操作即可。
  5. 我也有简单了解nextTick实现,它会在callbacks里面加入我们传入的函数,然后用timerFunc异步方式调用它们,首选的异步方式会是Promise。这让我明白了为什么可以在nextTick中看到dom操作结果。

2、说说你对vue的mixin的理解,有什么应用场景?

Mixin是面向对象程序设计语言中的类,提供了方法的实现。其他类可以访问mixin类的方法而不必成为其子类

Mixin类通常作为功能模块使用,在需要该功能时“混入”,有利于代码复用又避免了多继承的复杂

Vue中的mixin

先来看一下官方定义

mixin(混入),提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。

本质其实就是一个js对象,它可以包含我们组件中任意功能选项,如datacomponentsmethodscreatedcomputed等等

我们只要将共用的功能以对象的方式传入 mixins选项中,当组件使用 mixins对象时所有mixins对象的选项都将被混入该组件本身的选项中来

Vue中我们可以局部混入全局混入

局部混入

定义一个mixin对象,有组件optionsdatamethods属性

var myMixin = {
  created: function () {
    this.hello()
  },
  methods: {
    hello: function () {
      console.log('hello from mixin!')
    }
  }
}

组件通过mixins属性调用mixin对象

Vue.component('componentA',{
  mixins: [myMixin]
})

该组件在使用的时候,混合了mixin里面的方法,在自动执行created生命钩子,执行hello方法

全局混入

通过Vue.mixin()进行全局的混入

Vue.mixin({
  created: function () {
      console.log("全局混入")
    }
})

使用全局混入需要特别注意,因为它会影响到每一个组件实例(包括第三方组件)

PS:全局混入常用于插件的编写

注意事项:

当组件存在与mixin对象相同的选项的时候,进行递归合并的时候组件的选项会覆盖mixin的选项

但是如果相同选项为生命周期钩子的时候,会合并成一个数组,先执行mixin的钩子,再执行组件的钩子

二、使用场景

在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立

这时,可以通过Vuemixin功能将相同或者相似的代码提出来

举个例子

定义一个modal弹窗组件,内部通过isShowing来控制显示

const Modal = {
  template: '#modal',
  data() {
    return {
      isShowing: false
    }
  },
  methods: {
    toggleShow() {
      this.isShowing = !this.isShowing;
    }
  }
}

定义一个tooltip提示框,内部通过isShowing来控制显示

const Tooltip = {
  template: '#tooltip',
  data() {
    return {
      isShowing: false
    }
  },
  methods: {
    toggleShow() {
      this.isShowing = !this.isShowing;
    }
  }
}

通过观察上面两个组件,发现两者的逻辑是相同,代码控制显示也是相同的,这时候mixin就派上用场了

首先抽出共同代码,编写一个mixin

const toggle = {
  data() {
    return {
      isShowing: false
    }
  },
  methods: {
    toggleShow() {
      this.isShowing = !this.isShowing;
    }
  }
}

两个组件在使用上,只需要引入mixin

const Modal = {
  template: '#modal',
  mixins: [toggle]
};
 
const Tooltip = {
  template: '#tooltip',
  mixins: [toggle]
}

通过上面小小的例子,让我们知道了Mixin对于封装一些可复用的功能如此有趣、方便、实用

另外extends

要继承的“基类”组件。

  • 使一个组件可以继承另一个组件的组件选项。

    • 从实现角度来看,extends 几乎和 mixins 相同。通过 extends 指定的组件将会当作第一个 mixin 来处理。

    • 然而,extendsmixins 表达的是不同的目标。mixins 选项基本用于组合功能,而 extends 则一般更关注继承关系。

    mixins 一样,所有选项都将使用相关的策略进行合并。

  • 示例:

    const CompA = { ... }
    
    const CompB = {
      extends: CompA,
      ...
    }
    

3、说说你对slot的理解?slot使用场景有哪些?

Slot 艺名插槽,花名“占坑”,我们可以理解为solt在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot位置),作为承载分发内容的出口。

通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理

如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情

通过slot插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用

比如布局组件、表格列、下拉选、弹框显示内容等

详细讲一下antd-v中插槽的使用:

。。。

默认插槽

子组件用<slot>标签来确定渲染的位置,标签里面可以放DOM结构,当父组件使用的时候没有往插槽传入内容,标签内DOM结构就会显示在页面

父组件在使用的时候,直接在子组件的标签内写入内容即可

子组件Child.vue

<template>
    <slot>
      <p>插槽后备的内容</p>
    </slot>
</template>

父组件

<Child>
  <div>默认插槽</div>  
</Child>

具名插槽

子组件用name属性来表示插槽的名字,不传为默认插槽

父组件中在使用时在默认插槽的基础上加上slot属性,值为子组件插槽name属性值

子组件Child.vue

<template>
    <slot>插槽后备的内容</slot>
  <slot name="content">插槽后备的内容</slot>
</template>

父组件

<child>
    <template v-slot:default>具名插槽</template>
    <!-- 具名插槽?插槽名做参数 -->
    <template v-slot:content>内容...</template>
</child>

作用域插槽

子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件v-slot接受的对象上

父组件中在使用时通过v-slot:(简写:#)获取子组件的信息,在内容中使用

子组件Child.vue

<template> 
  <slot name="footer" testProps="子组件的值">
          <h3>没传footer插槽</h3>
    </slot>
</template>

父组件

<child> 
    <!-- 把v-slot的值指定为作?域上下?对象 -->
    <template v-slot:default="slotProps">
      来??组件数据:{{slotProps.testProps}}
    </template>
    <template #default="slotProps">
      来??组件数据:{{slotProps.testProps}}
    </template>
</child>

小结:

  • v-slot属性只能在<template>上使用,但在只有默认插槽时可以在组件标签上使用
  • 默认插槽名为default,可以省略default直接写v-slot
  • 缩写为#时不能不写参数,写成#default
  • 可以通过解构获取v-slot={user},还可以重命名v-slot="{user: newName}"和定义默认值v-slot="{user = '默认值'}"

4、watch和computed的区别以及选择?---扩展一下

两个重要API,反应应聘者熟练程度。

思路分析

  1. 先看两者定义,列举使用上的差异
  2. 列举使用场景上的差异,如何选择
  3. 使用细节、注意事项
  4. vue3变化

回答范例

  1. 计算属性可以从组件数据派生出新数据,最常见的使用方式是设置一个函数,返回计算之后的结果,computed和methods的差异是它具备缓存性,如果依赖项不变时不会重新计算。侦听器可以侦测某个响应式数据的变化并执行副作用,常见用法是传递一个函数,执行副作用,watch没有返回值,但可以执行异步操作等复杂逻辑。
  2. 计算属性常用场景是简化行内模板中的复杂表达式,模板中出现太多逻辑会是模板变得臃肿不易维护。侦听器常用场景是状态变化之后做一些额外的DOM操作或者异步操作。选择采用何用方案时首先看是否需要派生出新值,基本能用计算属性实现的方式首选计算属性。
  3. 使用过程中有一些细节,比如计算属性也是可以传递对象,成为既可读又可写的计算属性。watch可以传递对象,设置deep、immediate等选项。
  4. vue3中watch选项发生了一些变化,例如不再能侦测一个点操作符之外的字符串形式的表达式; reactivity API中新出现了watch、watchEffect可以完全替代目前的watch选项,且功能更加强大。

可能追问

  1. watch会不会立即执行?

  2. watch 和 watchEffect有什么差异

5、怎么缓存当前的组件?缓存后怎么更新?说说你对keep-alive的理解

Keep-alive 是什么

keep-alive`是`vue`中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染`DOM

keep-alive 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们

keep-alive可以设置以下props属性:

  • include - 字符串或正则表达式。只有名称匹配的组件会被缓存
  • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存
  • max - 数字。最多可以缓存多少组件实例

关于keep-alive的基本用法:

<keep-alive>
  <component :is="view"></component>
</keep-alive>

使用includesexclude

<keep-alive include="a,b">
  <component :is="view"></component>
</keep-alive>

<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
  <component :is="view"></component>
</keep-alive>

<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
  <component :is="view"></component>
</keep-alive>

匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值),匿名组件不能被匹配

设置了 keep-alive 缓存的组件,会多出两个生命周期钩子(activateddeactivated):

  • 首次进入组件时:beforeRouteEnter > beforeCreate > created> mounted > activated > ... ... > beforeRouteLeave > deactivated
  • 再次进入组件时:beforeRouteEnter >activated > ... ... > beforeRouteLeave > deactivated

使用场景

使用原则:当我们在某些场景下不需要让页面重新加载时我们可以使用keepalive

举个栗子:

当我们从首页–>列表页–>商详页–>再返回,这时候列表页应该是需要keep-alive

首页–>列表页–>商详页–>返回到列表页(需要缓存)–>返回到首页(需要缓存)–>再次进入列表页(不需要缓存),这时候可以按需来控制页面的keep-alive

在路由中设置keepAlive属性判断是否需要缓存

{
  path: 'list',
  name: 'itemList', // 列表页
  component (resolve) {
    require(['@/pages/item/list'], resolve)
 },
 meta: {
  keepAlive: true,
  title: '列表页'
 }
}

使用<keep-alive>

<div id="app" class='wrapper'>
    <keep-alive>
        <!-- 需要缓存的视图组件 --> 
        <router-view v-if="$route.meta.keepAlive"></router-view>
     </keep-alive>
      <!-- 不需要缓存的视图组件 -->
     <router-view v-if="!$route.meta.keepAlive"></router-view>
</div>

原理分析

keep-alivevue中内置的一个组件

源码位置:src/core/components/keep-alive.js

export default {
  name: 'keep-alive',
  abstract: true,

  props: {
    include: [String, RegExp, Array],
    exclude: [String, RegExp, Array],
    max: [String, Number]
  },

  created () {
    this.cache = Object.create(null)
    this.keys = []
  },

  destroyed () {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted () {
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  render() {
    /* 获取默认插槽中的第一个组件节点 */
    const slot = this.$slots.default
    const vnode = getFirstComponentChild(slot)
    /* 获取该组件节点的componentOptions */
    const componentOptions = vnode && vnode.componentOptions

    if (componentOptions) {
      /* 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag */
      const name = getComponentName(componentOptions)

      const { include, exclude } = this
      /* 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode */
      if (
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      /* 获取组件的key值 */
      const key = vnode.key == null
        // same constructor may get registered as different local components
        // so cid alone is not enough (#3269)
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
     /*  拿到key值后去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存 */
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        remove(keys, key)
        keys.push(key)
      }
        /* 如果没有命中缓存,则将其设置进缓存 */
        else {
        cache[key] = vnode
        keys.push(key)
        // prune oldest entry
        /* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }

      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}

可以看到该组件没有template,而是用了render,在组件渲染的时候会自动执行render函数

this.cache是一个对象,用来存储需要缓存的组件,它将以如下形式存储:

this.cache = {
    'key1':'组件1',
    'key2':'组件2',
    // ...
}

在组件销毁的时候执行pruneCacheEntry函数

function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
  const cached = cache[key]
  /* 判断当前没有处于被渲染状态的组件,将其销毁*/
  if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}

mounted钩子函数中观测 includeexclude 的变化,如下:

mounted () {
    this.$watch('include', val => {
        pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
        pruneCache(this, name => !matches(val, name))
    })
}

如果includeexclude 发生了变化,即表示定义需要缓存的组件的规则或者不需要缓存的组件的规则发生了变化,那么就执行pruneCache函数,函数如下:

function pruneCache (keepAliveInstance, filter) {
  const { cache, keys, _vnode } = keepAliveInstance
  for (const key in cache) {
    const cachedNode = cache[key]
    if (cachedNode) {
      const name = getComponentName(cachedNode.componentOptions)
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}

在该函数内对this.cache对象进行遍历,取出每一项的name值,用其与新的缓存规则进行匹配,如果匹配不上,则表示在新的缓存规则下该组件已经不需要被缓存,则调用pruneCacheEntry函数将其从this.cache对象剔除即可

关于keep-alive的最强大缓存功能是在render函数中实现

首先获取组件的key值:

const key = vnode.key == null? 
componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key

拿到key值后去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存,如下:

/* 如果命中缓存,则直接从缓存中拿 vnode 的组件实例 */
if (cache[key]) {
    vnode.componentInstance = cache[key].componentInstance
    /* 调整该组件key的顺序,将其从原来的地方删掉并重新放在最后一个 */
    remove(keys, key)
    keys.push(key)
} 

直接从缓存中拿 vnode 的组件实例,此时重新调整该组件key的顺序,将其从原来的地方删掉并重新放在this.keys中最后一个

this.cache对象中没有该key值的情况,如下:

/* 如果没有命中缓存,则将其设置进缓存 */
else {
    cache[key] = vnode
    keys.push(key)
    /* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */
    if (this.max && keys.length > parseInt(this.max)) {
        pruneCacheEntry(cache, keys[0], keys, this._vnode)
    }
}

表明该组件还没有被缓存过,则以该组件的key为键,组件vnode为值,将其存入this.cache中,并且把key存入this.keys

此时再判断this.keys中缓存组件的数量是否超过了设置的最大缓存数量值this.max,如果超过了,则把第一个缓存组件删掉

思考题:缓存后如何获取数据

解决方案可以有以下两种:

  • beforeRouteEnter
  • actived

beforeRouteEnter

每次组件渲染的时候,都会执行beforeRouteEnter

beforeRouteEnter(to, from, next){
    next(vm=>{
        console.log(vm)
        // 每次进入路由执行
        vm.getData()  // 获取数据
    })
},

actived

keep-alive缓存的组件被激活的时候,都会执行actived钩子

activated(){
   this.getData() // 获取数据
},

注意:服务器端渲染期间avtived不被调用

6、vue如何实现异步加载和动态加载

异步组件是什么?使用场景有哪些?

分析

因为异步路由的存在,我们使用异步组件的次数比较少,因此还是有必要两者的不同。

体验

大型应用中,我们需要分割应用为更小的块,并且在需要组件时再加载它们。

import { defineAsyncComponent } from 'vue'
// defineAsyncComponent定义异步组件
const AsyncComp = defineAsyncComponent(() => {
  // 加载函数返回Promise
  return new Promise((resolve, reject) => {
    // ...可以从服务器加载组件
    resolve(/* loaded component */)
  })
})
// 借助打包工具实现ES模块动态导入
const AsyncComp = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
)

思路

  1. 异步组件作用
  2. 何时使用异步组件
  3. 使用细节
  4. 和路由懒加载的不同

范例

  1. 在大型应用中,我们需要分割应用为更小的块,并且在需要组件时再加载它们。
  2. 我们不仅可以在路由切换时懒加载组件,还可以在页面组件中继续使用异步组件,从而实现更细的分割粒度。
  3. 使用异步组件最简单的方式是直接给defineAsyncComponent指定一个loader函数,结合ES模块动态导入函数import可以快速实现。我们甚至可以指定loadingComponent和errorComponent选项从而给用户一个很好的加载反馈。另外Vue3中还可以结合Suspense组件使用异步组件。
  4. 异步组件容易和路由懒加载混淆,实际上不是一个东西。异步组件不能被用于定义懒加载路由上,处理它的是vue框架,处理路由组件加载的是vue-router。但是可以在懒加载的路由组件中使用异步组件。

知其所以然

defineAsyncComponent定义了一个高阶组件,返回一个包装组件。包装组件根据加载器的状态决定渲染什么内容。

https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/apiAsyncComponent.ts#L43-L44


动态组件

1、什么是动态组件?

动态组件指的是动态切换组件的显示与隐藏

2、如何实现动态组件渲染?

vue提供了一个内置的组件,专门用来实现动态组件的渲染:通过 is 属性动态指定要渲染的组件

因为要渲染的组件是不确定的,所以要通过data申明一个变量用来接收组件的名称,用 :is 动态绑定这个变量组件中,通过按钮添加事件改变变量的值来动态切换组件。

data(){
	//1.当前要渲染的组件名称
	return {  comName:'left'  }
}

<!-- 2.通过is属性,动态指定要渲染的组件 -->
<component :is="comName"></component>

<!-- 3.点击按钮,动态切换组件名称 -->
<button @click=" comName = 'Left' ">展示Left组件</button>
<button @click=" comName = 'Right' ">展示Right组件</button>

3、使用 keep-alive 保持状态

默认情况下,切换动态组件时无法保持组件的状态,此时可以使用vue内置的组件保持动态组件的状态

<keep-alive>
	<component :is="comName"></component>
</keep-alive>

4、keep-alive对应的生命周期函数

当组件被缓存时,会自动触发组件的 deactivated 生命周期函数。

当组件被激活时,会自动触发组件的 activated 生命周期函数。

5、keep-alive的include属性

(include*注意是keep-alive的属性 is 是 组件的属性*)

include 属性用来指定:只有名称匹配的组件会被缓存。多个组件名之间使用英文的逗号分隔

<keep-alive include="MyLeft,MyRight">
	<component :is="comName"></component>
</keep-alive>

四、库相关

1、Vuex常见面试题

1.Vuex有哪几种属性?

有五种,分别是 State、 Getter、Mutation 、Action、 Module state => 基本数据(数据源存放地) getters => 从基本数据派生出来的数据 mutations => 提交更改数据的方法,同步! actions => 像一个装饰器,包裹mutations,使之可以异步。 modules => 模块化Vuex

2.Vue.js中ajax请求代码应该写在组件的methods中还是vuex的actions中?

如果请求来的数据是不是要被其他组件公用,仅仅在请求的组件内使用,就不需要放入vuex 的state里。 如果被其他地方复用,这个很大几率上是需要的,如果需要,请将请求放入action里,方便复用。

3.vuex的理解

思路

  1. 给定义
  2. 必要性阐述
  3. 何时使用
  4. 拓展:一些个人思考、实践经验等

回答范例

  1. Vuex 是一个专为 Vue.js 应用开发的状态管理模式 + 库。它采用集中式存储,管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
  2. 我们期待以一种简单的“单向数据流”的方式管理应用,即状态 -> 视图 -> 操作单向循环的方式。但当我们的应用遇到多个组件共享状态时,比如:多个视图依赖于同一状态或者来自不同视图的行为需要变更同一状态。此时单向数据流的简洁性很容易被破坏。因此,我们有必要把组件的共享状态抽取出来,以一个全局单例模式管理。通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。这是vuex存在的必要性,它和react生态中的redux之类是一个概念。
  3. Vuex 解决状态管理的同时引入了不少概念:例如state、mutation、action等,是否需要引入还需要根据应用的实际情况衡量一下:如果不打算开发大型单页应用,使用 Vuex 反而是繁琐冗余的,一个简单的 store 模式就足够了。但是,如果要构建一个中大型单页应用,Vuex 基本是标配。
  4. 我在使用vuex过程中感受到一些blabla

可能的追问

  1. vuex有什么缺点吗?你在开发过程中有遇到什么问题吗?

  2. action和mutation的区别是什么?为什么要区分它们?

4.vuex中actions和mutations有什么区别?

题目分析

mutationsactionsvuex带来的两个独特的概念。新手程序员容易混淆,所以面试官喜欢问。

我们只需记住修改状态只能是mutationsactions只能通过提交mutation修改状态即可。


体验

看下面例子可知,Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。
const store = createStore({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

答题思路

  1. 给出两者概念说明区别
  2. 举例说明应用场景
  3. 使用细节不同
  4. 简单阐述实现上差异

回答范例

  1. 官方文档说:更改 Vuex 的 store 中的状态的唯一方法是提交 mutationmutation 非常类似于事件:每个 mutation 都有一个字符串的类型 (type)*和一个*回调函数 (handler)Action 类似于 mutation,不同在于:Action可以包含任意异步操作,但它不能修改状态, 需要提交mutation才能变更状态。
  2. 因此,开发时,包含异步操作或者复杂业务组合时使用action;需要直接修改状态则提交mutation。但由于dispatch和commit是两个API,容易引起混淆,实践中也会采用统一使用dispatch action的方式。
  3. 调用dispatch和commit两个API时几乎完全一样,但是定义两者时却不甚相同,mutation的回调函数接收参数是state对象。action则是与Store实例具有相同方法和属性的上下文context对象,因此一般会解构它为{commit, dispatch, state},从而方便编码。另外dispatch会返回Promise实例便于处理内部异步结果。
  4. 实现上commit(type)方法相当于调用options.mutations[type](state)dispatch(type)方法相当于调用options.actions[type](store),这样就很容易理解两者使用上的不同了。

知其所以然

我们可以像下面这样简单实现commitdispatch,从而辨别两者不同:

class Store {
    constructor(options) {
        this.state = reactive(options.state)
        this.options = options
    }
    commit(type, payload) {
        // 传入上下文和参数1都是state对象
        this.options.mutations[type].call(this.state, this.state, payload)
    }
    dispatch(type, payload) {
        // 传入上下文和参数1都是store本身
        this.options.actions[type].call(this, this, payload)
    }
}

5.你有使用过vuex的module吗?

这是基本应用能力考察,稍微上点规模的项目都要拆分vuex模块便于维护。


体验

https://vuex.vuejs.org/zh/guide/modules.html

const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}
const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}
const store = createStore({
  modules: {
    a: moduleA,
    b: moduleB
  }
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
store.getters.c // -> moduleA里的getters
store.commit('d') // -> 能同时触发子模块中同名mutation
store.dispatch('e') // -> 能同时触发子模块中同名action

思路

  1. 概念和必要性
  2. 怎么拆
  3. 使用细节
  4. 优缺点

范例

  1. 用过module,项目规模变大之后,单独一个store对象会过于庞大臃肿,通过模块方式可以拆分开来便于维护
  2. 可以按之前规则单独编写子模块代码,然后在主文件中通过modules选项组织起来:createStore({modules:{...}})
  3. 不过使用时要注意访问子模块状态时需要加上注册时模块名:store.state.a.xxx,但同时gettersmutationsactions又在全局空间中,使用方式和之前一样。如果要做到完全拆分,需要在子模块加上namespace选项,此时再访问它们就要加上命名空间前缀。
  4. 很显然,模块的方式可以拆分代码,但是缺点也很明显,就是使用起来比较繁琐复杂,容易出错。而且类型系统支持很差,不能给我们带来帮助。pinia显然在这方面有了很大改进,是时候切换过去了。

用过pinia吗?都做了哪些改善?

。。。

2、vue-router常见面试题

  1. 什么是vue-router

    vue-router的原理
    在单页面应用中,不同组件之间的切换需要通过前端路由来实现。

    前端路由
    1.key是路径,value是组件,用于展示页面内容
    2.工作过程:当浏览器的路径改变时,对应的组件就会显示。
    vue-router的路由作用:将组件映射到路由, 然后渲染出来

    主要就是

    如何改变URL却不引起页面刷新
    如何检测到URL变化了

  2. vue-router的两种模式

    1.hash模式:即地址栏 URL 中的 # 符号

    路由的hash模式:

    hash 是 URL 中 # 及后面的那部分,#后的url不会发送到服务器,所以改变 URL 中的 hash 部分不会引起页面刷新

    window可以监听onhashchange事件变化。当hash变化时,读取#后的内容,根据信息进行路由规则匹配,通过改变 window.location.hash 改变页面路由。

    改变URL的三种方式:

    • 通过浏览器前进后退改变 URL
    • 通过标签改变 URL
    • 通过window.location改变URL

    优点:

    • 只需要前端配置路由表, 不需要后端的参与
    • 兼容性好, 浏览器都能支持
    • hash值改变不会向后端发送请求, 完全属于前端路由

    缺点:

    • hash值前面需要加#, 不符合url规范,也不美观

    2.history模式:window.history对象打印出来可以看到里边提供的方法和记录长度。利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。(需要特定浏览器支持)

    html5 的history Interface 中新增的pushState() 和 replaceState() 方法,用来在浏览历史中添加和修改记录,改变页面路径,使URL跳转不会重新加载页面。

    类似hashchange 事件的 popstate 事件,但 popstate 事件有些不同:

    只有在做出浏览器的行为才会调用 popState,用户点击浏览器倒退按钮和前进按钮,或者使用 JavaScript 调用History.back()、History.forward()、History.go()方法时才会触发。

    优点:

    • 符合url地址规范, 不需要#, 使用起来比较美观

    缺点:

    • 在用户手动输入地址或刷新页面时会发起url请求, 后端需要配置index.html页面用户匹配不到静态资源的情况, 否则会出现404错误
    • 兼容性比较差, 是利用了 HTML5 History对象中新增的 pushState() 和 replaceState() 方法,需要特定浏览器的支持
  3. $route和$router的区别

    1. $router为VueRouter实例,想要导航到不同URL,则使用router为VueRouter实例,想要导航到不同URL,则使用router.push()
    2. $route 为当前 router 跳转对象里面可以获取 name 、 path 、 query 、 params 等
  4. vue-router 使用params与query传参有什么区别

    • params 是路由的一部分,必须要有。query 是拼接在 url 后面的参数,没有也没关系
    • params 不设置的时候,刷新页面或者返回参数会丢,query 则不会有这个问题

    vue-router参数丢失的问题

    params参数传递的时候,传递了设置占位符接收的参数,地址栏不会显示并且刷新会丢失。

    解决办法:可以通过this.$route.params获取参数保存在本地

  5. vue-router 有哪几种导航钩子

    1. 全局导航钩子:router.beforeEach(to,from,next),作用:跳转前进行判断拦截。
    2. 组件内的钩子 beforeRouteEnter(to,from,next)
    3. 单独路由独享组件 beforeEnter(to,from,next)

    完整的导航解析流程

    1.导航被触发。
    2.在失活的组件里调用 beforeRouteLeave 守卫。
    3.调用全局的 beforeEach 守卫。
    4.在重用的组件里调用 beforeRouteUpdate 守卫。
    5.在路由配置里调用 beforeEnter。
    6.解析异步路由组件。
    7.在被激活的组件里调用 beforeRouteEnter。
    8.调用全局的 beforeResolve 守卫。
    9.导航被确认。
    10.调用全局的 afterEach 钩子。
    11.触发 DOM 更新。
    12.调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

  6. router-link和router-view是如何起作用的?

    分析

    vue-router中两个重要组件router-linkrouter-view,分别起到导航作用和内容渲染作用,但是回答如何生效还真有一定难度哪!

    思路

    • 两者作用
    • 阐述使用方式
    • 原理说明

    回答范例

    • vue-router中两个重要组件router-linkrouter-view,分别起到路由导航作用和组件内容渲染作用
    • 使用中router-link默认生成一个a标签,设置to属性定义跳转path。实际上也可以通过custom和插槽自定义最终的展现形式。router-view是要显示组件的占位组件,可以嵌套,对应路由配置的嵌套关系,配合name可以显示具名组件,起到更强的布局作用。
    • router-link组件内部根据custom属性判断如何渲染最终生成节点,内部提供导航方法navigate,用户点击之后实际调用的是该方法,此方法最终会修改响应式的路由变量,然后重新去routes匹配出数组结果,router-view则根据其所处深度deep在匹配数组结果中找到对应的路由并获取组件,最终将其渲染出来。
  7. 预加载和懒加载

    1、怎么实现路由懒加载呢?

    分析

    这是一道应用题。当打包应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问时才加载对应组件,这样就会更加高效。

    // 将
    // import UserDetails from './views/UserDetails'
    // 替换为
    const UserDetails = () => import('./views/UserDetails')
    
    const router = createRouter({
    // ...
    routes: [{ path: '/users/:id', component: UserDetails }],
    })
    

    参考https://router.vuejs.org/zh/guide/advanced/lazy-loading.html


    思路

    1. 必要性
    2. 何时用
    3. 怎么用
    4. 使用细节

    回答范例

    1. 当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。利用路由懒加载我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样会更加高效,是一种优化手段。

    2. 一般来说,对所有的路由都使用动态导入是个好主意。

    3. component选项配置一个返回 Promise 组件的函数就可以定义懒加载路由。例如:

      { path: '/users/:id', component: () => import('./views/UserDetails') }

    4. 结合注释() => import(/* webpackChunkName: "group-user" */ './UserDetails.vue')可以做webpack代码分块

      vite中结合rollupOptions定义分块

    5. 路由中不能使用异步组件


    知其所以然

    component (和 components) 配置如果接收一个返回 Promise 组件的函数,Vue Router 只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。

3、Axios二次封装

封装的同时,你需要和 后端协商好一些约定,请求头,状态码,请求超时时间.......

设置接口请求前缀:根据开发、测试、生产环境的不同,前缀需要加以区分

请求头 : 来实现一些具体的业务,必须携带一些参数才可以请求(例如:会员业务)

状态码: 根据接口返回的不同status , 来执行不同的业务,这块需要和后端约定好

请求方法:根据getpost等方法进行一个再次封装,使用起来更为方便

请求拦截器: 根据请求的请求头设定,来决定哪些请求可以访问

响应拦截器: 这块就是根据 后端`返回来的状态码判定执行不同业务

设置接口请求前缀

利用node环境变量来作判断,用来区分开发、测试、生产环境

if (process.env.NODE_ENV === 'development') {
axios.defaults.baseURL = 'http://dev.xxx.com'
} else if (process.env.NODE_ENV === 'production') {
axios.defaults.baseURL = 'http://prod.xxx.com'
}

在本地调试的时候,还需要在vue.config.js文件中配置devServer实现代理转发,从而实现跨域

devServer: {
 proxy: {
   '/proxyApi': {
     target: 'http://dev.xxx.com',
     changeOrigin: true,
     pathRewrite: {
       '/proxyApi': ''
     }
   }
 }
}

设置请求头与超时时间

大部分情况下,请求头都是固定的,只有少部分情况下,会需要一些特殊的请求头,这里将普适性的请求头作为基础配置。当需要特殊请求头时,将特殊请求头作为参数传入,覆盖基础配置

const service = axios.create({
 ...
 timeout: 30000,  // 请求 30s 超时
	  headers: {
     get: {
       'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
       // 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进来
     },
     post: {
       'Content-Type': 'application/json;charset=utf-8'
       // 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进来
     }
},
})

封装请求方法

先引入封装好的方法,在要调用的接口重新封装成一个方法暴露出去

// get 请求
export function httpGet({
url,
params = {}
}) {
return new Promise((resolve, reject) => {
 axios.get(url, {
   params
 }).then((res) => {
   resolve(res.data)
 }).catch(err => {
   reject(err)
 })
})
}

// post
// post请求
export function httpPost({
url,
data = {},
params = {}
}) {
return new Promise((resolve, reject) => {
 axios({
   url,
   method: 'post',
   transformRequest: [function (data) {
     let ret = ''
     for (let it in data) {
       ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
     }
     return ret
   }],
   // 发送的数据
   data,
   // url参数
   params

 }).then(res => {
   resolve(res.data)
 })
})
}

把封装的方法放在一个api.js文件中

import { httpGet, httpPost } from './http'
export const getorglist = (params = {}) => httpGet({ url: 'apps/api/org/list', params })

页面中就能直接调用

// .vue
import { getorglist } from '@/assets/js/api'

getorglist({ id: 200 }).then(res => {
console.log(res)
})

这样可以把api统一管理起来,以后维护修改只需要在api.js文件操作即可

请求拦截器

请求拦截器可以在每个请求里加上token,做了统一处理后维护起来也方便

// 请求拦截器
axios.interceptors.request.use(
config => {
 // 每次发送请求之前判断是否存在token
 // 如果存在,则统一在http请求的header都加上token,这样后台根据token判断你的登录情况,此处token一般是用户完成登录后储存到localstorage里的
 token && (config.headers.Authorization = token)
 return config
},
error => {
 return Promise.error(error)
})

响应拦截器

响应拦截器可以在接收到响应后先做一层操作,如根据状态码判断登录状态、授权

// 响应拦截器
axios.interceptors.response.use(response => {
// 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据
// 否则的话抛出错误
if (response.status === 200) {
 if (response.data.code === 511) {
   // 未授权调取授权接口
 } else if (response.data.code === 510) {
   // 未登录跳转登录页
 } else {
   return Promise.resolve(response)
 }
} else {
 return Promise.reject(response)
}
}, error => {
// 我们可以在这里对异常状态作统一处理
if (error.response.status) {
 // 处理请求失败的情况
 // 对不同返回码对相应处理
 return Promise.reject(error.response)
}
})

小结

  • 封装是编程中很有意义的手段,简单的axios封装,就可以让我们可以领略到它的魅力
  • 封装 axios 没有一个绝对的标准,只要你的封装可以满足你的项目需求,并且用起来方便,那就是一个好的封装方案

五、VUE3

1、vue2和vue3的区别?------重点

在我们更新(和重写)Vue 的主要版本时,主要考虑两点因素:首先是新的 JavaScript 语言特性在主流浏览器中的受支持水平;其次是当前代码库中随时间推移而逐渐暴露出来的一些设计和架构问题」

简要就是:

  • 利用新的语言特性(es6)
  • 解决架构问题

我们可以概览Vue3的新特性,如下:

  • 速度更快
  • 体积减少
  • 更易维护
  • 更接近原生
  • 更易使用

速度更快

  • 重写了虚拟Dom实现
  • 编译模板的优化
  • 更高效的组件初始化
  • undate性能提高1.3~2倍
  • SSR速度提高了2~3倍

体积更小

通过webpacktree-shaking功能,可以将无用模块“剪辑”,仅打包需要的

能够tree-shaking,有两大好处:

  • 对开发人员,能够对vue实现更多其他的功能,而不必担忧整体体积过大
  • 对使用者,打包出来的包体积变小了

vue可以开发出更多其他的功能,而不必担忧vue打包出来的整体体积过多

更易维护

compositon Api
  • 可与现有的Options API一起使用
  • 灵活的逻辑组合与复用
  • Vue3模块可以和其他框架搭配使用
更好的Typescript支持

VUE3是基于typescipt编写的,可以享受到自动的类型定义提示

编译器重写

更接近原生

可以自定义渲染 API

更易使用

响应式 Api 暴露出来

概览

一、新特性

1、组合式API---setup

2、ref创建响应式数据

3、Teleport---“传送门”

4、多根节点

5、style中使用变量

二、差异

1、v-if和v-for的优先级

2、.sync修饰符

3、全局API

4、Vue.prototype 替换为 config.globalProperties

5、生命周期

6、key在template和v-if上的使用

7、$listeners被移除

8、this

9、typescript支持

详情

一、vue3新特性
1、组合式API---setup
  • 在vue2中:生命周期、methods、data等只能定义一次,并把相关的变量、方法等集中写在一处地方,这导致我们在代码编写及维护的过程中,不可避免的要反复横跳来查找对应的变量、方法等,这种碎片化的编码方式既不利于理解,也不利于维护
  • 在vue3中:setup是一个接收 props 和 content 的函数,允许我们在其内部通过API的方式(如:onMounted(getUserRepositories) // 在 mounted 时调用 getUserRepositories)多次使用一个相同的生命周期等。
2、ref创建响应式数据
  • 在vue2中:响应式数据的创建必须是写在 data 中的,在vue内部通过Object.defineproperty() 方法 以及 重写Array的方法来达到数据响应式的目的,这种方式是“主动”的,无论此变量有没有改变都会在vue初始化时设置为响应式(效率差,支持性差:你经常会用到$set这个方法)
  • 在vue3中:通过 ref 函数实现数据响应式(vue3中数据响应式的实现是通过proxy代理的方式,这种方式是“被动”的,只有当你去访问改变变量时,才会被处理为响应式数据,并且不需要对数组、对象等进行特殊处理,它是“一视同仁的”,还支持 Set 等较新的数据类型)
3、Teleport---“传送门”
  • 允许我们将指定内容渲染在指定的html标签上
4、多根节点
  • 在vue2中:一旦根节点有多个,vue会发出警告
  • 在vue3中:支持多根节点(减少了DOM元素的嵌套层级)
5、style中使用变量
  • <script setup>
    const theme = {
      color: 'red'
    }
    </script>
    <template>
      <p>hello</p>
    </template>
    
二、区别
1、v-if和v-for的优先级
  • 在vue2中:当v-if和v-for同时使用时,v-for的优先级高于v-if(因此我们通常需要计算属性先对数据进行加工处理,以达到性能优化的目的)
  • 在vue3中:当v-if和v-for同时使用时,v-if的优先级高于v-for
2、.sync修饰符
  • vue2中:由于vue中是单向数据流,父子组件在传值时想要实现v-model的效果就要用到.sync修饰符来实现“双向绑定”
  • vue3中:对v-model进行了改造,不再需要 .sync 修饰符即可达到数据双向绑定的效果。在vue3中支持多个 v-model属性,默认使用 modelValue 作为 prop,update:modelValue作为事件,当多个v-model绑定时,书写为例:v-model:title="title",此时 title 作为prop,update:title 作为事件
3、全局API
  • vue2中:有许多的全局API,如:Vue.directive、Vue.component、Vue.config、Vue.mixin等
  • vue3中:提供的是实例API,通过createApp创建vue实例,原来在Vue原型上的API都被挂载到了vue实例上,如:app.directive、app.component、app.config、app.mixin等
4、Vue.prototype 替换为 config.globalProperties
  • vue2中:绑定全局的变量、方法等:Vue.prototype.$ajax = xxxx
  • vue3中:const app = createApp({}); app.config.globalProperties.$ajax = xxxx
5、生命周期
vue2 vue3
beforeCreate setup()
created setup()
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeDestroy onBeforeUnmount
destroyed onUnmounted
6、key在template和v-if上的使用
  • vue2中:在使用v-if、vi-else、v-else-if时,为了保证dom节点渲染的正确性,通常需要在对应的节点添加不同的key,以确保vue在进行虚拟dom对比时是准确的;vue2中template在v-for循环时是不能设置key的,否则会产生警告(需要给子节点设置key)。
  • vue3中:在使用v-if、vi-else、v-else-if时,不用提供唯一的key对dom节点进行区分,因为vue内部会自动生成唯一的key,如果你提供了key,那你就要保证它的唯一性;vue3中template在v-for循环时,key应该设置在template标签上
7、$listeners被移除
  • vue2中:使用$attrs访问传递给组件的属性,使用$listeners访问传递给组件的事件(需要结合inheritAttrs:false)。
  • vue3中:虚拟dom中,事件监听器仅仅是以on为前缀的属性
8、this
  • vue2中:无时无刻都要使用this
  • vue3中:因为setup函数的存在,所有的props、data等都不需要用this进行访问(vue3对vue2绝大多数是兼容的,如果你用了vue2相关的东西,那你还是需要像vue2一样书写)
9、typescript支持
  • vue2中:默认是不支持typescript的。
  • vue3中:支持使用typescript,使用typescript在构建大型项目时,能够很好的提高项目开发的质量。

2、Object.definePropety和Proxy区别

一、bject.definePropety

定义:Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

为什么能实现响应式

通过defineProperty 两个属性,getset

  • get

属性的 getter 函数,当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值

  • set

属性的 setter 函数,当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined

下面通过代码展示:

定义一个响应式函数defineReactive

function update() {
    app.innerText = obj.foo
}

function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            console.log(`get ${key}:${val}`);
            return val
        },
        set(newVal) {
            if (newVal !== val) {
                val = newVal
                update()
            }
        }
    })
}

调用defineReactive,数据发生变化触发update方法,实现数据响应式

const obj = {}
defineReactive(obj, 'foo', '')
setTimeout(()=>{
    obj.foo = new Date().toLocaleTimeString()
},1000)

在对象存在多个key情况下,需要进行遍历

function observe(obj) {
    if (typeof obj !== 'object' || obj == null) {
        return
    }
    Object.keys(obj).forEach(key => {
        defineReactive(obj, key, obj[key])
    })
}

如果存在嵌套对象的情况,还需要在defineReactive中进行递归

function defineReactive(obj, key, val) {
    observe(val)
    Object.defineProperty(obj, key, {
        get() {
            console.log(`get ${key}:${val}`);
            return val
        },
        set(newVal) {
            if (newVal !== val) {
                val = newVal
                update()
            }
        }
    })
}

当给key赋值为对象的时候,还需要在set属性中进行递归

set(newVal) {
    if (newVal !== val) {
        observe(newVal) // 新值是对象的情况
        notifyUpdate()
    }
}

上述例子能够实现对一个对象的基本响应式,但仍然存在诸多问题

现在对一个对象进行删除与添加属性操作,无法劫持到

const obj = {
    foo: "foo",
    bar: "bar"
}
observe(obj)
delete obj.foo // no ok
obj.jar = 'xxx' // no ok

当我们对一个数组进行监听的时候,并不那么好使了

const arrData = [1,2,3,4,5];
arrData.forEach((val,index)=>{
    defineProperty(arrData,index,val)
})
arrData.push() // no ok
arrData.pop()  // no ok
arrDate[0] = 99 // ok

可以看到数据的api无法劫持到,从而无法实现数据响应式,

所以在Vue2中,增加了setdelete API,并且对数组api方法进行一个重写

还有一个问题则是,如果存在深层的嵌套对象关系,需要深层的进行监听,造成了性能的极大问题

小结

  • 检测不到对象属性的添加和删除
  • 数组API方法无法监听到
  • 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题

二、proxy

Proxy的监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作,这就完全可以代理所有属性了

ES6系列中,我们详细讲解过Proxy的使用,就不再述说了

下面通过代码进行展示:

定义一个响应式方法reactive

function reactive(obj) {
    if (typeof obj !== 'object' && obj != null) {
        return obj
    }
    // Proxy相当于在对象外层加拦截
    const observed = new Proxy(obj, {
        get(target, key, receiver) {
            const res = Reflect.get(target, key, receiver)
            console.log(`获取${key}:${res}`)
            return res
        },
        set(target, key, value, receiver) {
            const res = Reflect.set(target, key, value, receiver)
            console.log(`设置${key}:${value}`)
            return res
        },
        deleteProperty(target, key) {
            const res = Reflect.deleteProperty(target, key)
            console.log(`删除${key}:${res}`)
            return res
        }
    })
    return observed
}

测试一下简单数据的操作,发现都能劫持

const state = reactive({
    foo: 'foo'
})
// 1.获取
state.foo // ok
// 2.设置已存在属性
state.foo = 'fooooooo' // ok
// 3.设置不存在属性
state.dong = 'dong' // ok
// 4.删除属性
delete state.dong // ok

再测试嵌套对象情况,这时候发现就不那么 OK 了

const state = reactive({
    bar: { a: 1 }
})

// 设置嵌套对象属性
state.bar.a = 10 // no ok

如果要解决,需要在get之上再进行一层代理

function reactive(obj) {
    if (typeof obj !== 'object' && obj != null) {
        return obj
    }
    // Proxy相当于在对象外层加拦截
    const observed = new Proxy(obj, {
        get(target, key, receiver) {
            const res = Reflect.get(target, key, receiver)
            console.log(`获取${key}:${res}`)
            return isObject(res) ? reactive(res) : res
        },
    return observed
}

总结

Object.defineProperty只能遍历对象属性进行劫持

function observe(obj) {
    if (typeof obj !== 'object' || obj == null) {
        return
    }
    Object.keys(obj).forEach(key => {
        defineReactive(obj, key, obj[key])
    })
}

Proxy直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的

function reactive(obj) {
    if (typeof obj !== 'object' && obj != null) {
        return obj
    }
    // Proxy相当于在对象外层加拦截
    const observed = new Proxy(obj, {
        get(target, key, receiver) {
            const res = Reflect.get(target, key, receiver)
            console.log(`获取${key}:${res}`)
            return res
        },
        set(target, key, value, receiver) {
            const res = Reflect.set(target, key, value, receiver)
            console.log(`设置${key}:${value}`)
            return res
        },
        deleteProperty(target, key) {
            const res = Reflect.deleteProperty(target, key)
            console.log(`删除${key}:${res}`)
            return res
        }
    })
    return observed
}

Proxy可以直接监听数组的变化(pushshiftsplice

const obj = [1,2,3]
const proxtObj = reactive(obj)
obj.psuh(4) // ok

Proxy有多达13种拦截方法,不限于applyownKeysdeletePropertyhas等等,这是Object.defineProperty不具备的

正因为defineProperty自身的缺陷,导致Vue2在实现响应式过程需要实现其他的方法辅助(如重写数组方法、增加额外setdelete方法)

// 数组重写
const originalProto = Array.prototype
const arrayProto = Object.create(originalProto)
['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(method => {
  arrayProto[method] = function () {
    originalProto[method].apply(this.arguments)
    dep.notice()
  }
});

// set、delete
Vue.set(obj,'bar','newbar')
Vue.delete(obj),'bar')

Proxy 不兼容IE,也没有 polyfill, defineProperty 能支持到IE9

简答:

回答范例
■JS中做属性拦截常见的方式有三::defineProperty,getter/setters和Proxies
■Vue2中使用defineProperty的原因是,2013年时只能用这种方式由于该AP存在一些局限性,比如对于
数组的拦截有问题,为此vue需要专门为数组响应式做一套实现。另外不能截那些新增、删除属性;最后
defineProperty,方案在初始化时需要深度递归遍历待处理的对象才能对它进行完全拦截,明显增加了初始
化的时间。
■以上两点在Proxy出现之后迎刃而解,不仅可以对数组实现拦截,还能对Map、Set实现拦截:另外Proxy的挡
截也是懒处理行为,如果用户没有访问嵌套对象,那么也不会实施拦截,这就让初始化的速度和内存占用都改
善了。
当然Proxy是有兼容性问题的,IE完全不支持,所以如果需要E兼容就不合适

3、Vue3.0 所采用的 Composition Api 与 Vue2.x 使用的 Options Api 有什么不同?、

Composition API 可以说是Vue3的最大特点,那么为什么要推出Composition Api,解决了什么问题?

通常使用Vue2开发的项目,普遍会存在以下问题:

  • 代码的可读性随着组件变大而变差
  • 每一种代码复用的方式,都存在缺点
  • TypeScript支持有限

以上通过使用Composition Api都能迎刃而解

一、Options Api

Options API,即大家常说的选项API,即以vue为后缀的文件,通过定义methodscomputedwatchdata等属性与方法,共同处理页面逻辑

如下图:

可以看到Options代码编写方式,如果是组件状态,则写在data属性上,如果是方法,则写在methods属性上...

用组件的选项 (datacomputedmethodswatch) 组织逻辑在大多数情况下都有效

然而,当组件变得复杂,导致对应属性的列表也会增长,这可能会导致组件难以阅读和理解

二、Composition Api

在 Vue3 Composition API 中,组件根据逻辑功能来组织的,一个功能所定义的所有 API 会放在一起(更加的高内聚,低耦合)

即使项目很大,功能很多,我们都能快速的定位到这个功能所用到的所有 API

对比

下面对Composition ApiOptions Api进行两大方面的比较

  • 逻辑组织
  • 逻辑复用

逻辑组织

Options API

假设一个组件是一个大型组件,其内部有很多处理逻辑关注点(对应下图不用颜色)

可以看到,这种碎片化使得理解和维护复杂组件变得困难

选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块

Compostion API

Compositon API正是解决上述问题,将某个逻辑关注点相关的代码全都放在一个函数里,这样当需要修改一个功能时,就不再需要在文件中跳来跳去

下面举个简单例子,将处理count属性相关的代码放在同一个函数了

function useCount() {
    let count = ref(10);
    let double = computed(() => {
        return count.value * 2;
    });

    const handleConut = () => {
        count.value = count.value * 2;
    };

    console.log(count);

    return {
        count,
        double,
        handleConut,
    };
}

组件上中使用count

export default defineComponent({
    setup() {
        const { count, double, handleConut } = useCount();
        return {
            count,
            double,
            handleConut
        }
    },
});

再来一张图进行对比,可以很直观地感受到 Composition API在逻辑组织方面的优势,以后修改一个属性功能的时候,只需要跳到控制该属性的方法中即可

逻辑复用

Vue2中,我们是用过mixin去复用相同的逻辑

下面举个例子,我们会另起一个mixin.js文件

export const MoveMixin = {
  data() {
    return {
      x: 0,
      y: 0,
    };
  },

  methods: {
    handleKeyup(e) {
      console.log(e.code);
      // 上下左右 x y
      switch (e.code) {
        case "ArrowUp":
          this.y--;
          break;
        case "ArrowDown":
          this.y++;
          break;
        case "ArrowLeft":
          this.x--;
          break;
        case "ArrowRight":
          this.x++;
          break;
      }
    },
  },

  mounted() {
    window.addEventListener("keyup", this.handleKeyup);
  },

  unmounted() {
    window.removeEventListener("keyup", this.handleKeyup);
  },
};

然后在组件中使用

<template>
  <div>
    Mouse position: x {{ x }} / y {{ y }}
  </div>
</template>
<script>
import mousePositionMixin from './mouse'
export default {
  mixins: [mousePositionMixin]
}
</script>

使用单个mixin似乎问题不大,但是当我们一个组件混入大量不同的 mixins 的时候

mixins: [mousePositionMixin, fooMixin, barMixin, otherMixin]

会存在两个非常明显的问题:

  • 命名冲突
  • 数据来源不清晰

现在通过Compositon API这种方式改写上面的代码

import { onMounted, onUnmounted, reactive } from "vue";
export function useMove() {
  const position = reactive({
    x: 0,
    y: 0,
  });

  const handleKeyup = (e) => {
    console.log(e.code);
    // 上下左右 x y
    switch (e.code) {
      case "ArrowUp":
        // y.value--;
        position.y--;
        break;
      case "ArrowDown":
        // y.value++;
        position.y++;
        break;
      case "ArrowLeft":
        // x.value--;
        position.x--;
        break;
      case "ArrowRight":
        // x.value++;
        position.x++;
        break;
    }
  };

  onMounted(() => {
    window.addEventListener("keyup", handleKeyup);
  });

  onUnmounted(() => {
    window.removeEventListener("keyup", handleKeyup);
  });

  return { position };
}

在组件中使用

<template>
  <div>
    Mouse position: x {{ x }} / y {{ y }}
  </div>
</template>

<script>
import { useMove } from "./useMove";
import { toRefs } from "vue";
export default {
  setup() {
    const { position } = useMove();
    const { x, y } = toRefs(position);
    return {
      x,
      y,
    };

  },
};
</script>

可以看到,整个数据来源清晰了,即使去编写更多的 hook 函数,也不会出现命名冲突的问题

小结

  • 在逻辑组织和逻辑复用方面,Composition API是优于Options API
  • 因为Composition API几乎是函数,会有更好的类型推断。
  • Composition APItree-shaking 友好,代码也更容易压缩
  • Composition API中见不到this的使用,减少了this指向不明的情况
  • 如果是小型组件,可以继续使用Options API,也是十分友好的

4、ref和reactive异同

这是Vue3数据响应式中非常重要的两个概念,自然的,跟我们写代码关系也很大。

ref:https://vuejs.org/api/reactivity-core.html#ref

const count = ref(0)
console.log(count.value) // 0

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

reactive:https://vuejs.org/api/reactivity-core.html#reactive

const obj = reactive({ count: 0 })
obj.count++

回答思路

  1. 两者概念
  2. 两者使用场景
  3. 两者异同
  4. 使用细节
  5. 原理

回答范例

  1. ref接收内部值(inner value)返回响应式Ref对象,reactive返回响应式代理对象
  2. 从定义上看ref通常用于处理单值的响应式,reactive用于处理对象类型的数据响应式
  3. 两者均是用于构造响应式数据,但是ref主要解决原始值的响应式问题
  4. ref返回的响应式数据在JS中使用需要加上.value才能访问其值,在视图中使用会自动脱ref,不需要.value;ref可以接收对象或数组等非原始值,但内部依然是reactive实现响应式;reactive内部如果接收Ref对象会自动脱ref;使用展开运算符(...)展开reactive返回的响应式对象会使其失去响应性,可以结合toRefs()将值转换为Ref对象之后再展开。
  5. reactive内部使用Proxy代理传入对象并拦截该对象各种操作(trap),从而实现响应式。ref内部封装一个RefImpl类,并设置get value/set value,拦截用户对值的访问,从而实现响应式。

5、watch和watchEffect异同

我们经常性需要侦测响应式数据的变化,vue3中除了watch之外又出现了watchEffect,不少同学会混淆这两个api。


体验

watchEffect立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数。

Runs a function immediately while reactively tracking its dependencies and re-runs it whenever the dependencies are changed.

const count = ref(0)

watchEffect(() => console.log(count.value))
// -> logs 0

count.value++
// -> logs 1

watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数。

Watches one or more reactive data sources and invokes a callback function when the sources change.

const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

思路

  1. 给出两者定义
  2. 给出场景上的不同
  3. 给出使用方式和细节
  4. 原理阐述

范例

  1. watchEffect立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数。watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数。
  2. watchEffect(effect)是一种特殊watch,传入的函数既是依赖收集的数据源,也是回调函数。如果我们不关心响应式数据变化前后的值,只是想拿这些数据做些事情,那么watchEffect就是我们需要的。watch更底层,可以接收多种数据源,包括用于依赖收集的getter函数,因此它完全可以实现watchEffect的功能,同时由于可以指定getter函数,依赖可以控制的更精确,还能获取数据变化前后的值,因此如果需要这些时我们会使用watch。
  3. watchEffect在使用时,传入的函数会立刻执行一次。watch默认情况下并不会执行回调函数,除非我们手动设置immediate选项。
  4. 从实现上来说,watchEffect(fn)相当于watch(fn,fn,{immediate:true})

很明显watchEffect就是一种特殊的watch实现。

六、项目

1、说下你的vue项目的目录结构,如果是大型项目你该怎么划分结构和划分组件呢?

2、vue要做权限管理该怎么做?如果控制到按钮级别的权限怎么做?

七、扩展

1、SPA首屏加载速度慢的怎么解决?

csr

2、SPA和SSR

我们现在编写的Vue、React和Angular应用大多数情况下都会在一个页面中,点击链接跳转页面通常是内容切换而非页面跳转,由于良好的用户体验逐渐成为主流的开发模式。但同时也会有首屏加载时间长,SEO不友好的问题,因此有了SSR,这也是为什么面试中会问到两者的区别。

思路分析

  1. 两者概念
  2. 两者优缺点分析
  3. 使用场景差异
  4. 其他选择

回答范例

  1. SPA(Single Page Application)即单页面应用。一般也称为 客户端渲染(Client Side Render), 简称 CSR。SSR(Server Side Render)即 服务端渲染。一般也称为 多页面应用(Mulpile Page Application),简称 MPA。
  2. SPA应用只会首次请求html文件,后续只需要请求JSON数据即可,因此用户体验更好,节约流量,服务端压力也较小。但是首屏加载的时间会变长,而且SEO不友好。为了解决以上缺点,就有了SSR方案,由于HTML内容在服务器一次性生成出来,首屏加载快,搜索引擎也可以很方便的抓取页面信息。但同时SSR方案也会有性能,开发受限等问题。
  3. 在选择上,如果我们的应用存在首屏加载优化需求,SEO需求时,就可以考虑SSR。
  4. 但并不是只有这一种替代方案,比如对一些不常变化的静态网站,SSR反而浪费资源,我们可以考虑预渲染(prerender)方案。另外nuxt.js/next.js中给我们提供了SSG(Static Site Generate)静态网站生成方案也是很好的静态站点解决方案,结合一些CI手段,可以起到很好的优化效果,且能节约服务器资源。

知其所以然

内容生成上的区别:

SSR


SPA


部署上的区别

3、使用vue渲染大量数据时应该怎么优化?说下你的思路!

分析

企业级项目中渲染大量数据的情况比较常见,因此这是一道非常好的综合实践题目。

思路

  1. 描述大数据量带来的问题
  2. 分不同情况做不同处理
  3. 总结一下

回答

  1. 在大型企业级项目中经常需要渲染大量数据,此时很容易出现卡顿的情况。比如大数据量的表格、树。
  2. 处理时要根据情况做不通处理:
    • 可以采取分页的方式获取,避免渲染大量数据
    • vue-virtual-scroller等虚拟滚动方案,只渲染视口范围内的数据
    • 如果不需要更新,可以使用v-once方式只渲染一次
    • 通过v-memo可以缓存结果,结合v-for使用,避免数据变化时不必要的VNode创建
    • 可以采用懒加载方式,在用户需要的时候再加载数据,比如tree组件子树的懒加载
  3. 总之,还是要看具体需求,首先从设计上避免大数据获取和渲染;实在需要这样做可以采用虚表的方式优化渲染;最后优化更新,如果不需要更新可以v-once处理,需要更新可以v-memo进一步优化大数据更新性能。其他可以采用的是交互方式优化,无线滚动、懒加载等方案。

4、你了解哪些Vue性能优化方法?

分析

这是一道综合实践题目,写过一定数量的代码之后小伙伴们自然会开始关注一些优化方法,答得越多肯定实践经验也越丰富,是很好的题目。

答题思路:

根据题目描述,这里主要探讨Vue代码层面的优化


回答范例

  • 我这里主要从Vue代码编写层面说一些优化手段,例如:代码分割、服务端渲染、组件缓存、长列表优化等

  • 最常见的路由懒加载:有效拆分App尺寸,访问时才异步加载

    const router = createRouter({
      routes: [
        // 借助webpack的import()实现异步组件
        { path: '/foo', component: () => import('./Foo.vue') }
      ]
    })
    

  • keep-alive缓存页面:避免重复创建组件实例,且能保留缓存组件状态

    <router-view v-slot="{ Component }">
    	<keep-alive>
      	<component :is="Component"></component>
      </keep-alive>
    </router-view>
    

  • 使用v-show复用DOM:避免重复创建组件

    <template>
      <div class="cell">
        <!-- 这种情况用v-show复用DOM,比v-if效果好 -->
        <div v-show="value" class="on">
          <Heavy :n="10000"/>
        </div>
        <section v-show="!value" class="off">
          <Heavy :n="10000"/>
        </section>
      </div>
    </template>
    

  • v-for 遍历避免同时使用 v-if:实际上在Vue3中已经是个错误写法

    <template>
        <ul>
          <li
            v-for="user in activeUsers"
            <!-- 避免同时使用,vue3中会报错 -->
            <!-- v-if="user.isActive" -->
            :key="user.id">
            {{ user.name }}
          </li>
        </ul>
    </template>
    <script>
      export default {
        computed: {
          activeUsers: function () {
            return this.users.filter(user => user.isActive)
          }
        }
      }
    </script>
    

  • v-once和v-memo:不再变化的数据使用v-once

    <!-- single element -->
    <span v-once>This will never change: {{msg}}</span>
    <!-- the element have children -->
    <div v-once>
      <h1>comment</h1>
      <p>{{msg}}</p>
    </div>
    <!-- component -->
    <my-component v-once :comment="msg"></my-component>
    <!-- `v-for` directive -->
    <ul>
      <li v-for="i in list" v-once>{{i}}</li>
    </ul>
    

    按条件跳过更新时使用v-momo:下面这个列表只会更新选中状态变化项

    <div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
      <p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
      <p>...more child nodes</p>
    </div>
    

    https://vuejs.org/api/built-in-directives.html#v-memo


  • 长列表性能优化:如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容

    <recycle-scroller
      class="items"
      :items="items"
      :item-size="24"
    >
      <template v-slot="{ item }">
        <FetchItemView
          :item="item"
          @vote="voteItem(item)"
        />
      </template>
    </recycle-scroller>
    

    一些开源库:

    • vue-virtual-scroller
    • vue-virtual-scroll-grid

  • 事件的销毁:Vue 组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。

    export default {
      created() {
        this.timer = setInterval(this.refresh, 2000)
      },
      beforeUnmount() {
        clearInterval(this.timer)
      }
    }
    

  • 图片懒加载

    对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。

    <img v-lazy="/static/img/1.png">
    

    参考项目:vue-lazyload


  • 第三方插件按需引入

    element-plus这样的第三方组件库可以按需引入避免体积太大。

    import { createApp } from 'vue';
    import { Button, Select } from 'element-plus';
    
    const app = createApp()
    app.use(Button)
    app.use(Select)
    

  • 子组件分割策略:较重的状态组件适合拆分

    <template>
      <div>
        <ChildComp/>
      </div>
    </template>
    
    <script>
    export default {
      components: {
        ChildComp: {
          methods: {
            heavy () { /* 耗时任务 */ }
          },
          render (h) {
            return h('div', this.heavy())
          }
        }
      }
    }
    </script>
    

    但同时也不宜过度拆分组件,尤其是为了所谓组件抽象将一些不需要渲染的组件特意抽出来,组件实例消耗远大于纯dom节点。参考:https://vuejs.org/guide/best-practices/performance.html#avoid-unnecessary-component-abstractions


  • 服务端渲染/静态网站生成:SSR/SSG

    如果SPA应用有首屏渲染慢的问题,可以考虑SSR、SSG方案优化。参考SSR Guide

计算机网络

一、基础

1、如何理解OSI七层模型

OSI (Open System Interconnect)模型全称为开放式通信系统互连参考模型,是国际标准化组织 ( ISO ) 提出的一个试图使各种计算机在世界范围内互连为网络的标准框架

OSI将计算机网络体系结构划分为七层,每一层实现各自的功能和协议,并完成与相邻层的接口通信。即每一层扮演固定的角色,互不打扰

划分

OSI主要划分了七层,如下图所示:

应用层

应用层位于 OSI 参考模型的第七层,其作用是通过应用程序间的交互来完成特定的网络应用

该层协议定义了应用进程之间的交互规则,通过不同的应用层协议为不同的网络应用提供服务。例如域名系统 DNS,支持万维网应用的 HTTP 协议,电子邮件系统采用的 SMTP协议等

在应用层交互的数据单元我们称之为报文

表示层

表示层的作用是使通信的应用程序能够解释交换数据的含义,其位于 OSI参考模型的第六层,向上为应用层提供服务,向下接收来自会话层的服务

该层提供的服务主要包括数据压缩,数据加密以及数据描述,使应用程序不必担心在各台计算机中表示和存储的内部格式差异

会话层

会话层就是负责建立、管理和终止表示层实体之间的通信会话

该层提供了数据交换的定界和同步功能,包括了建立检查点和恢复方案的方法

传输层

传输层的主要任务是为两台主机进程之间的通信提供服务,处理数据包错误、数据包次序,以及其他一些关键传输问题

传输层向高层屏蔽了下层数据通信的细节。因此,它是计算机通信体系结构中关键的一层

其中,主要的传输层协议是TCPUDP

网络层

两台计算机之间传送数据时其通信链路往往不止一条,所传输的信息甚至可能经过很多通信子网

网络层的主要任务就是选择合适的网间路由和交换节点,确保数据按时成功传送

在发送数据时,网络层把传输层产生的报文或用户数据报封装成分组和包,向下传输到数据链路层

在网络层使用的协议是无连接的网际协议(Internet Protocol)和许多路由协议,因此我们通常把该层简单地称为 IP 层

数据链路层

数据链路层通常也叫做链路层,在物理层和网络层之间。两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层协议

在两个相邻节点之间传送数据时,数据链路层将网络层交下来的 IP数据报组装成帧,在两个相邻节点间的链路上传送帧

每一帧的数据可以分成:报头head和数据data两部分:

  • head 标明数据发送者、接受者、数据类型,如 MAC地址
  • data 存储了计算机之间交互的数据

通过控制信息我们可以知道一个帧的起止比特位置,此外,也能使接收端检测出所收到的帧有无差错,如果发现差错,数据链路层能够简单的丢弃掉这个帧,以避免继续占用网络资源

物理层

作为OSI 参考模型中最低的一层,物理层的作用是实现计算机节点之间比特流的透明传送

该层的主要任务是确定与传输媒体的接口的一些特性(机械特性、电气特性、功能特性,过程特性)

该层主要是和硬件有关,与软件关系不大

数据在各层之间的传输如下图所示:

  • 应用层报文被传送到运输层
  • 在最简单的情况下,运输层收取到报文并附上附加信息,该首部将被接收端的运输层使用
  • 应用层报文和运输层首部信息一道构成了运输层报文段。附加的信息可能包括:允许接收端运输层向上向适当的应用程序交付报文的信息以及差错检测位信息。该信息让接收端能够判断报文中的比特是否在途中已被改变
  • 运输层则向网络层传递该报文段,网络层增加了如源和目的端系统地址等网络层首部信息,生成了网络层数据包
  • 网络层数据包接下来被传递给链路层,在数据链路层数据包添加发送端 MAC 地址和接收端 MAC 地址后被封装成数据帧
  • 在物理层数据帧被封装成比特流,之后通过传输介质传送到对端
  • 对端再一步步解开封装,获取到传送的数据

2、如何理解TCP/IP协议?

TCP/IP,传输控制协议/网际协议,是指能够在多个不同网络间实现信息传输的协议簇

  • TCP(传输控制协议)

一种面向连接的、可靠的、基于字节流的传输层通信协议

  • IP(网际协议)

用于封包交换数据网络的协议

TCP/IP协议不仅仅指的是TCPIP两个协议,而是指一个由FTPSMTPTCPUDPIP等协议构成的协议簇,

只是因为在TCP/IP协议中TCP协议和IP协议最具代表性,所以通称为TCP/IP协议簇

TCP/IP协议族按层次分别了五层体系或者四层体系

3、不同层的设备和协议

设备

  • 物理层设备
    • ? 光纤 同轴电缆 网线 中继器 集线器 --------------------------------物理层就是关心怎么传输的 局域网
  • 链路层设备
    • ? 交换机(局域网通信)
  • 网络层设备
    • ? 路由器 路由器有wan口可以充当网关进行上网,没有wan口的路由器可以看成交换机默认两个不同的网络不能相互通信想让两个不同的区域的设备来通信要经历网关

协议

协议就是约定和规范 (在7模型中只有三层以上的才能称之为协议)

  • 应用层 (HTTP DNS) DHCP协议
  • 传输层协议 (TCP UDP)
  • 网络层 IP协议 ARP协议 ------------- 下层为上层提供服务的

ARP是有歧义的(核心价值在于将ip地址转化成mac地址故即可算链路层也可算网络层)----局域网协议

DHCP 动态主机配置协议 自动分配ip 基于udp

DNS 因为用户很难记住ip地址 用域名替代ip,ip替代mac

4、IP地址和MAC地址

“IP地址”是指互联网协议地址(InternetProtocolAddress),是IPAddress的缩写。IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。

“MAC地址”又称为物理地址、硬件地址,用来定义网络设备的位置。网卡的物理地址通常是由网卡生产厂家烧入网卡的,具有全球唯一性。MAC地址用于在网络中唯一标示一个网卡,一台电脑会有一或多个网卡,每个网卡都需要有一个唯一的MAC地址。

IP地址和MAC地址的区别主要有:

1.两者地址使用不同。IP地址是指Internet协议使用的地址,而MAC地址是Ethernet协议使用的地址。当存在一个附加层的地址寻址时,设备更易于移动和维修。

2.分配依据不同。IP地址的分配是基于网络拓扑,MAC地址的分配是基于制造商。IP地址是可以自动分配的,MAC地址在每个网卡出场的时候就有一个全球唯一的MAC地址,所以很多的验证软件就是验证mac地址的。

3.地址能否更改不同。IP是可以更改的,mac地址虽然也可以更改,但是一般用不上,除非要用来绕过一些验证软件的。网卡在通讯的时候通过mac地址相互识别。

4.长度不同。IP地址为32位,MAC地址为48位。

5.寻址协议层不同。IP地址应用于OSI第三层,即网络层,而MAC地址应用在OSI第二层,即数据链路层。

二、应用层

1、DNS域名系统采用的是TCP协议还是UDP协议?为啥?

UDP协议,因为对于用户来说,相应时间越快越好,而TCP连接建立时间比较慢,所以采用UDP协议来实 现。

2、讲讲DNS解析过程?

DNS解析,就是将域名解析成具体的地址的过程。会一步步向上解析,知道解析出结果(从右向左,递归解析)。

DNS(Domain Names System),域名系统,是互联网一项服务,是进行域名和与之相对应的 IP 地址进行转换的服务器

简单来讲,DNS相当于一个翻译官,负责将域名翻译成ip地址

  • IP 地址:一长串能够唯一地标记网络上的计算机的数字
  • 域名:是由一串用点分隔的名字组成的 Internet 上某一台计算机或计算机组的名称,用于在数据传输时对计算机的定位标识

域名

域名是一个具有层次的结构,从上到下一次为根域名、顶级域名、二级域名、三级域名...(几个点就是几级域名)

例如www.xxx.comwww为三级域名、xxx为二级域名、com为顶级域名,系统为用户做了兼容,域名末尾的根域名.一般不需要输入

在域名的每一层都会有一个域名服务器,如下图:

除此之外,还有电脑默认的本地域名服务器

查询方式

DNS 查询的方式有两种:

  • 递归查询:如果 A 请求 B,那么 B 作为请求的接收者一定要给 A 想要的答案
  • 迭代查询:如果接收者 B 没有请求者 A 所需要的准确内容,接收者 B 将告诉请求者 A,如何去获得这个内容,但是自己并不去发出请求

域名缓存

在域名服务器解析的时候,使用缓存保存域名和IP地址的映射

计算机中DNS的记录也分成了两种缓存方式:

  • 浏览器缓存:浏览器在获取网站域名的实际 IP 地址后会对其进行缓存,减少网络请求的损耗
  • 操作系统缓存:操作系统的缓存其实是用户自己配置的 hosts 文件

查询过程

解析域名的过程如下:

  • 首先搜索浏览器的 DNS 缓存,缓存中维护一张域名与 IP 地址的对应表
  • 若没有命中,则继续搜索操作系统的 DNS 缓存
  • 若仍然没有命中,则操作系统将域名发送至本地域名服务器,本地域名服务器采用递归查询自己的 DNS 缓存,查找成功则返回结果(路由器的dns)
  • 若本地域名服务器的 DNS 缓存没有命中,则本地域名服务器向上级域名服务器进行迭代查询
    • 首先本地域名服务器向根域名服务器发起请求,根域名服务器返回顶级域名服务器的地址给本地服务器
    • 本地域名服务器拿到这个顶级域名服务器的地址后,就向其发起请求,获取权限域名服务器的地址
    • 本地域名服务器根据权限域名服务器的地址向其发起请求,最终得到该域名对应的 IP 地址
  • 本地域名服务器将得到的 IP 地址返回给操作系统,同时自己将 IP 地址缓存起来
  • 操作系统将 IP 地址返回给浏览器,同时自己也将 IP 地址缓存起
  • 至此,浏览器就得到了域名对应的 IP 地址,并将 IP 地址缓存起

流程如下图所示:

3、什么是HTTP

HTTP 是一个在计算机世界里专门在「两点」之间「传输」文字、图片、音频、视频等「超文本」数据的「约定和规范」。(不一定是服务器和本地浏览器,也可能是服务器到服务器)

4、HTTP 常见的状态码有哪些?

1xx 类状态码属于提示信息,是协议处理中的一种中间状态,实际用到的比较少。

100-continue 该状态码说明服务器收到了请求的初始部分,并请客户端继续发送。在服务器发送了 100 Continue 状态码之后,如果收到客户端的请求,则必须进行响应

101 Switching Protocol(协议切换)状态码表示服务器应客户端升级协议的请求对协议进行切换。

102 Processing是由WebDAV(RFC 2518)扩展的状态码,代表处理将被继续执行。

2xx 类状态码表示服务器成功处理了客户端的请求,也是我们最愿意看到的状态。

  • 200 OK」是最常见的成功状态码,表示一切正常。如果是非 HEAD 请求,服务器返回的响应头都会有 body 数据。
  • 204 No Content」也是常见的成功状态码,与 200 OK 基本相同,但响应头没有 body 数据。
  • 206 Partial Content」是应用于 HTTP 分块下载或断点续传,表示响应返回的 body 数据并不是资源的全部,而是其中的一部分,也是服务器处理成功的状态。

3xx 类状态码表示客户端请求的资源发生了变动,需要客户端用新的 URL 重新发送请求获取资源,也就是重定向

  • 301 Moved Permanently」表示永久重定向,说明请求的资源已经不存在了,需改用新的 URL 再次访问。
  • 302 Found」表示临时重定向,说明请求的资源还在,但暂时需要用另一个 URL 来访问。

301 和 302 都会在响应头里使用字段 Location,指明后续要跳转的 URL,浏览器会自动重定向新的 URL。

  • 304 Not Modified」不具有跳转的含义,表示资源未修改,重定向已存在的缓冲文件,也称缓存重定向,也就是告诉客户端可以继续使用缓存资源,用于缓存控制。

4xx 类状态码表示客户端发送的报文有误,服务器无法处理,也就是错误码的含义。

  • 400 Bad Request」表示客户端请求的报文有错误,但只是个笼统的错误。
  • [401 Unauthorized] 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
  • 403 Forbidden」表示服务器禁止访问资源,并不是客户端的请求出错。
  • 404 Not Found」表示请求的资源在服务器上不存在或未找到,所以无法提供给客户端。

5xx 类状态码表示客户端请求报文正确,但是服务器处理时内部发生了错误,属于服务器端的错误码。

  • 500 Internal Server Error」与 400 类型,是个笼统通用的错误码,服务器发生了什么错误,我们并不知道。
  • 501 Not Implemented」表示客户端请求的功能还不支持,类似“即将开业,敬请期待”的意思。
  • 502 Bad Gateway」通常是服务器作为网关或代理时返回的错误码,表示服务器自身工作正常,访问后端服务器发生了错误。
  • 503 Service Unavailable」表示服务器当前很忙,暂时无法响应客户端,类似“网络服务正忙,请稍后重试”的意思。

5、HTTP 常见字段有哪些?

响应体:一个 HTTP 响应代表服务器向客户端回送的数据,它包括:一个状态行、若干消息头(响应头)、实体内容 (响应体)

客户端连上服务器后,向服务器请求某个 web 资源,称之为客户端向服务器发送了一个 HTTP 请求

一个完整的HTTP请求包括如下内容:

A、请求行
B、若干消息头
C、实体内容 (get请求时没有,post请求时有)

一、application/x-www-form-urlencoded

  这个是默认是数据类型,如果没有进行说明,那么默认提交的数据格式。提交的内容按照key1=value1&key2=value2的方式进行编码

二、multipart/form-data

  这也是一种常见的数据格式,一般用于form表单上传文件的场景。

  在postman中测试,Body选择form-data即可,如将上面格式改为multipart/form-data格式

、application/json

  以JSON格式传入参数,现在越来越多的请求数据为次格式,字典键值对的形式,告诉服务器数据是序列化后的JSON格式。如将上面请求改application/json格式

二进制数据、图片、xml等


Host 字段

客户端发送请求时,用来指定服务器的域名。

Host: www.A.com

有了 Host 字段,就可以将请求发往「同一台」服务器上的不同网站。

Content-Length 字段

服务器在返回数据时,会有 Content-Length 字段,表明本次回应的数据长度。

Content-Length: 1000

如上面则是告诉浏览器,本次服务器回应的数据长度是 1000 个字节,后面的字节就属于下一个回应了。

Connection 字段

Connection 字段最常用于客户端要求服务器使用 TCP 持久连接,以便其他请求复用。

HTTP/1.1 版本的默认连接都是持久连接,但为了兼容老版本的 HTTP,需要指定 Connection 首部字段的值为 Keep-Alive

Connection: keep-alive

一个可以复用的 TCP 连接就建立了,直到客户端或服务器主动关闭连接。但是,这不是标准字段。

Content-Type 字段

Content-Type 字段用于服务器回应时,告诉客户端,本次数据是什么格式。

Content-Type: text/html; charset=utf-8

上面的类型表明,发送的是网页,而且编码是UTF-8。

客户端请求的时候,可以使用 Accept 字段声明自己可以接受哪些数据格式。

Accept: */*

上面代码中,客户端声明自己可以接受任何格式的数据。

Content-Encoding 字段

Content-Encoding 字段说明数据的压缩方法。表示服务器返回的数据使用了什么压缩格式

Content-Encoding: gzip

上面表示服务器返回的数据采用了 gzip 方式压缩,告知客户端需要用此方式解压。

客户端在请求时,用 Accept-Encoding 字段说明自己可以接受哪些压缩方法。

Accept-Encoding: gzip, deflate

6、说一下 GET 和 POST 的区别?

表面的区别

  1. GET在浏览器回退时是无害的,而POST会再次提交请求。
  2. GET产生的URL地址可以被Bookmark,而POST不可以。
  3. GET请求会被浏览器主动cache,而POST不会,除非手动设置。
  4. GET请求只能进行url编码,而POST支持多种编码方式。
  5. GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
  6. get方式提交数据的大小(一般来说1024字节),http协议并没有硬性限制,而是与浏览器、服务器、操作系统有关,而POST理论上来说没有大小限制,http协议规范也没有进行大小限制,但实际上post所能传递的数据量根据取决于服务器的设置和内存大小。
  7. 对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
  8. GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
  9. GET参数通过URL传递,POST放在Request body中。

没有区别的回答

  1. GET和POST是HTTP协议中的两种发送请求的方法。
  2. HTTP是基于TCP/IP的关于数据如何在万维网中如何通信的协议。
  3. HTTP的底层是TCP/IP。所以GET和POST的底层也是TCP/IP,也就是说,GET/POST都是TCP链接。GET和POST能做的事情是一样一样的。你要给GET加上request body,给POST带上url参数,技术上是完全行的通的。
  4. GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。

重大区别

  • GET产生一个TCP数据包;POST产生两个TCP数据包。
  • 对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);
  • 而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。
  • 因为POST需要两步,时间上消耗的要多一点,看起来GET比POST更有效。因此Yahoo团队有推荐用GET替换POST来优化网站性能。但这是一个坑!跳入需谨慎。

为什么不能混用

  1. GET与POST都有自己的语义,不能随便混用。
  2. 据研究,在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。
  3. 并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。

在你做项目的时候要注意:

get一般是用来获取数据,post提交数据

post其实是有大小限制的,只不过是取决于服务器的设置和内存大小。

还有更深入的区别:

GET是用来向获取服务器信息的,请求报文传输的信息只是用于描述所需资源的参数,返回的信息才是数据本身;POST是用来向服务器传递数据的,其请求报文传递的信息就是数据本身,返回的报文只是操作的结果。

7、HTTP缓存技术

避免重复发送 HTTP 请求的方法就是通过缓存技术。HTTP 缓存有两种实现方式,分别是强制缓存和协商缓存

1.强制缓存

强缓存指的是只要浏览器判断缓存没有过期,则直接使用浏览器的本地缓存,决定是否使用缓存的主动性在于浏览器这边。

如下图中,返回的是 200 状态码,但在 size 项中标识的是 from disk cache,就是使用了强制缓存。

强缓存是利用下面这两个 HTTP 响应头部(Response Header)字段实现的,它们都用来表示资源在客户端缓存的有效期:

  • Cache-Control, 是一个相对时间;
  • Expires,是一个绝对时间;

如果 HTTP 响应头部同时有 Cache-Control 和 Expires 字段的话,Cache-Control的优先级高于 Expires

Cache-control 选项更多一些,设置更加精细,所以建议使用 Cache-Control 来实现强缓存。具体的实现流程如下:

  • 当浏览器第一次请求访问服务器资源时,服务器会在返回这个资源的同时,在 Response 头部加上 Cache-Control,Cache-Control 中设置了过期时间大小;
  • 浏览器再次请求访问服务器中的该资源时,会先通过请求资源的时间与 Cache-Control 中设置的过期时间大小,来计算出该资源是否过期,如果没有,则使用该缓存,否则重新请求服务器;
  • 服务器再次收到请求后,会再次更新 Response 头部的 Cache-Control。

2.协商缓存

当我们在浏览器使用开发者工具的时候,你可能会看到过某些请求的响应码是 304,这个是告诉浏览器可以使用本地缓存的资源,通常这种通过服务端告知客户端是否可以使用缓存的方式被称为协商缓存。

上图就是一个协商缓存的过程,所以协商缓存就是与服务端协商之后,通过协商结果来判断是否使用本地缓存

协商缓存可以基于两种头部来实现。

第一种:请求头部中的 If-Modified-Since 字段与响应头部中的 Last-Modified 字段实现,这两个字段的意思是:

  • 响应头部中的 Last-Modified:标示这个响应资源的最后修改时间;
  • 请求头部中的 If-Modified-Since:当资源过期了,发现响应头中具有 Last-Modified 声明,则再次发起请求的时候带上 Last-Modified 的时间,服务器收到请求后发现有 If-Modified-Since 则与被请求资源的最后修改时间进行对比(Last-Modified),如果最后修改时间较新(大),说明资源又被改过,则返回最新资源,HTTP 200 OK;如果最后修改时间较旧(小),说明资源无新修改,响应 HTTP 304 走缓存。

第二种:请求头部中的 If-None-Match 字段与响应头部中的 ETag 字段,这两个字段的意思是:

  • 响应头部中 Etag:唯一标识响应资源;
  • 请求头部中的 If-None-Match:当资源过期时,浏览器发现响应头里有 Etag,则再次向服务器发起请求时,会将请求头If-None-Match 值设置为 Etag 的值。服务器收到请求后进行比对,如果资源没有变化返回 304,如果资源变化了返回 200。

第一种实现方式是基于时间实现的,第二种实现方式是基于一个唯一标识实现的,相对来说后者可以更加准确地判断文件内容是否被修改,避免由于时间篡改导致的不可靠问题。

如果 HTTP 响应头部同时有 Etag 和 Last-Modified 字段的时候, Etag 的优先级更高,也就是先会判断 Etag 是否变化了,如果 Etag 没有变化,然后再看 Last-Modified。

注意,协商缓存这两个字段都需要配合强制缓存中 Cache-control 字段来使用,只有在未能命中强制缓存的时候,才能发起带有协商缓存字段的请求

使用 ETag 字段实现的协商缓存的过程如下;

  • 当浏览器第一次请求访问服务器资源时,服务器会在返回这个资源的同时,在 Response 头部加上 ETag 唯一标识,这个唯一标识的值是根据当前请求的资源生成的;

  • 当浏览器再次请求访问服务器中的该资源时,首先会先检查强制缓存是否过期,如果没有过期,则直接使用本地缓存;如果缓存过期了,会在 Request 头部加上 If-None-Match 字段,该字段的值就是 ETag 唯一标识;

  • 服务器再次收到请求后,

    会根据请求中的 If-None-Match 值与当前请求的资源生成的唯一标识进行比较:

    • 如果值相等,则返回 304 Not Modified,不会返回资源
    • 如果不相等,则返回 200 状态码和返回资源,并在 Response 头部加上新的 ETag 唯一标识;
  • 如果浏览器收到 304 的请求响应状态码,则会从本地缓存中加载资源,否则更新资源。

8、HTTP优缺点

HTTP 最突出的优点是「简单、灵活和易于扩展、应用广泛和跨平台」。

HTTP 协议里有优缺点一体的双刃剑,分别是「无状态、明文传输」,同时还有一大缺点「不安全」

1. 无状态双刃剑

无状态的好处,因为服务器不会去记忆 HTTP 的状态,所以不需要额外的资源来记录状态信息,这能减轻服务器的负担,能够把更多的 CPU 和内存用来对外提供服务。

无状态的坏处,既然服务器没有记忆能力,它在完成有关联性的操作时会非常麻烦。

对于无状态的问题,解法方案有很多种,其中比较简单的方式用 Cookie 技术。

Cookie 通过在请求和响应报文中写入 Cookie 信息来控制客户端的状态。

相当于,在客户端第一次请求后,服务器会下发一个装有客户信息的「小贴纸」,后续客户端请求服务器的时候,带上「小贴纸」,服务器就能认得了了

2. 明文传输双刃剑

明文意味着在传输过程中的信息,是可方便阅读的,通过浏览器的 F12 控制台或 Wireshark 抓包都可以直接肉眼查看,为我们调试工作带了极大的便利性。

但是这正是这样,HTTP 的所有信息都暴露在了光天化日下,相当于信息裸奔

3. 不安全

HTTP 比较严重的缺点就是不安全:

  • 通信使用明文(不加密),内容可能会被窃听。比如,账号信息容易泄漏,那你号没了。
  • 不验证通信方的身份,因此有可能遭遇伪装。比如,访问假的淘宝、拼多多,那你钱没了。
  • 无法证明报文的完整性,所以有可能已遭篡改。比如,网页上植入垃圾广告,视觉污染,眼没了。

9、http和https区别

  • 区别
  1. HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。
  2. http是超文本传输协议,信息是明文传输,HTTPS 则解决 HTTP 不安全的缺陷,在 TCP 和 HTTP 网络层之间加入了 SSL/TLS 安全协议,使得报文能够加密传输。
  3. http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。(这个只是默认端口不一样,实际上端口是可以改的)
  4. HTTP 连接建立相对简单, TCP 三次握手之后便可进行 HTTP 的报文传输。而 HTTPS 在 TCP 三次握手之后,还需进行 SSL/TLS 的握手过程,才可进入加密报文传输。

10、HTTPS 是如何解决HTTP的窃听、篡改、冒充风险的?

  • 混合加密的方式实现信息的机密性,解决了窃听的风险。
  • 摘要算法的方式来实现完整性,它能够为数据生成独一无二的「指纹」,指纹用于校验数据的完整性,解决了篡改的风险。
  • 将服务器公钥放入到数字证书中,解决了冒充的风险。

11、什么是数字签名

? 。。。

12、HTTPS 是如何建立连接的?其间交互了什么?

SSL/TLS 协议基本流程:

  • 客户端向服务器索要并验证服务器的公钥。
  • 双方协商生产「会话秘钥」。
  • 双方采用「会话秘钥」进行加密通信。

前两步也就是 SSL/TLS 的建立过程,也就是 TLS 握手阶段。

SSL/TLS 的「握手阶段」涉及四次通信, 基于 RSA 握手过程的 HTTPS (opens new window)见下图:

SSL/TLS 协议建立的详细流程:

1. ClientHello

首先,由客户端向服务器发起加密通信请求,也就是 ClientHello 请求。

在这一步,客户端主要向服务器发送以下信息:

(1)客户端支持的 SSL/TLS 协议版本,如 TLS 1.2 版本。

(2)客户端生产的随机数(Client Random),后面用于生成「会话秘钥」条件之一。

(3)客户端支持的密码套件列表,如 RSA 加密算法。

2. SeverHello

服务器收到客户端请求后,向客户端发出响应,也就是 SeverHello。服务器回应的内容有如下内容:

(1)确认 SSL/ TLS 协议版本,如果浏览器不支持,则关闭加密通信。

(2)服务器生产的随机数(Server Random),也是后面用于生产「会话秘钥」条件之一。

(3)确认的密码套件列表,如 RSA 加密算法。

(4)服务器的数字证书。

3.客户端回应

客户端收到服务器的回应之后,首先通过浏览器或者操作系统中的 CA 公钥,确认服务器的数字证书的真实性。

如果证书没有问题,客户端会从数字证书中取出服务器的公钥,然后使用它加密报文,向服务器发送如下信息:

(1)一个随机数(pre-master key)。该随机数会被服务器公钥加密。

(2)加密通信算法改变通知,表示随后的信息都将用「会话秘钥」加密通信。

(3)客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要,用来供服务端校验。

上面第一项的随机数是整个握手阶段的第三个随机数,会发给服务端,所以这个随机数客户端和服务端都是一样的。

服务器和客户端有了这三个随机数(Client Random、Server Random、pre-master key),接着就用双方协商的加密算法,各自生成本次通信的「会话秘钥」

4. 服务器的最后回应

服务器收到客户端的第三个随机数(pre-master key)之后,通过协商的加密算法,计算出本次通信的「会话秘钥」。

然后,向客户端发送最后的信息:

(1)加密通信算法改变通知,表示随后的信息都将用「会话秘钥」加密通信。

(2)服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要,用来供客户端校验。

至此,整个 SSL/TLS 的握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的 HTTP 协议,只不过用「会话秘钥」加密内容。

13、简述https握手

  1. 首先客户端发起请求到服务端,服务端处理后发送一个公钥给客户端
  2. 客户端进行验证公钥,看公钥是否有效和是否过期
  3. 客户端验证通过会产生随机值key,然后用公钥进行加密回传给服务端
  4. 服务端用私钥解密后获得客户端的随机值key
  5. 利用随机值key加密数据后传输给客户端
  6. 客户端利用key值进行解密数据
  7. 客户端获取真正的数据

14、http1/2/3的区别

HTTP1.0:

  • 浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接

HTTP1.1:

  • 新增了一些请求方法

  • 新增了一些请求头和响应头

  • 引入了长连接,持久连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。即TCP连接默认不关闭,可以被多个请求复用

  • 采取了管道网络传输的方法,即在同一个TCP连接里面,客户端可以同时发送多个请求,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。

    缺点:

  • 虽然允许复用TCP连接,但是同一个TCP连接里面,所有的数据通信是按次序进行的,服务器只有处理完一个请求,才会接着处理下一个请求。如果前面的处理特别慢,后面就会有许多请求排队等着

  • HTTP/1.1 管道解决了请求的队头阻塞,但是没有解决响应的队头阻塞

HTTP2.0:

  • HTTP/2 协议是基于 HTTPS 的,所以 HTTP/2 的安全性也是有保障的。

? 相比于1.1的进步

  • 采用二进制格式而非文本格式,增加了数据传输的效率
  • 完全多路复用,一个连接中并发多个请求或回应,而不用按照顺序一一对应降低了延迟,大幅度提高了连接的利用率
  • 使用报头压缩,HTTP/2 会压缩头(Header)如果你同时发出多个请求,他们的头是一样的或是相似的,那么,协议会帮你消除重复的部分。降低开销
  • 服务器推送,改善了传统的「请求 - 应答」工作模式,服务端不再是被动地响应,可以主动向客户端发送消息。
  • 数据流。在 HTTP/2 中每个请求或响应的所有数据包,称为一个数据流(Stream)。每个数据流都标记着一个独一无二的编号(Stream ID),不同 Stream 的帧是可以乱序发送的(因此可以并发不同的 Stream )

? 缺陷:HTTP/2 虽然通过多个请求复用一个 TCP 连接解决了 HTTP 的队头阻塞 ,但是一旦发生丢包,就会阻塞住所有的 HTTP 请求,这属于 TCP 层队头阻塞。

HTTP3.0:

  • UDP 发送是不管顺序,也不管丢包的,所以不会出现像 HTTP/2 队头阻塞的问题。大家都知道 UDP 是不可靠传输的,但基于 UDP 的 QUIC 协议 可以实现类似 TCP 的可靠性传输。

三、传输层

1、谈谈你对tcp/udp的理解

UDP

UDP(User Datagram Protocol),用户数据包协议,是一个简单的面向数据报的通信协议,即对应用层交下来的报文,不合并,不拆分,只是在其上面加上首部后就交给了下面的网络层

也就是说无论应用层交给UDP多长的报文,它统统发送,一次发送一个报文

而对接收方,接到后直接去除首部,交给上面的应用层就完成任务

UDP报头包括4个字段,每个字段占用2个字节(即16个二进制位),标题短,开销小

特点如下:

  • UDP 不提供复杂的控制机制,利用 IP 提供面向无连接的通信服务
  • 传输途中出现丢包,UDP 也不负责重发
  • 当包的到达顺序出现乱序时,UDP没有纠正的功能。
  • 并且它是将应用程序发来的数据在收到的那一刻,立即按照原样发送到网络上的一种机制。即使是出现网络拥堵的情况,UDP 也无法进行流量控制等避免网络拥塞行为

TCP

TCP(Transmission Control Protocol),传输控制协议,是一种可靠、面向字节流的通信协议,把上面应用层交下来的数据看成无结构的字节流来发送

可以想象成流水形式的,发送方TCP会将数据放入“蓄水池”(缓存区),等到可以发送的时候就发送,不能发送就等着,TCP会根据当前网络的拥塞状态来确定每个报文段的大小

TCP报文首部有20个字节(不包括可选项和数据--HTTP报文),额外开销大

  • 源端口号、目标端口号,指代的是发送方随机端口,目标端对应的端口。
  • 序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。
  • 确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。
  • 4位首部长度:单位是字节,4位最大能表示15,所以头部长度最大为60
  • 6位保留:
  • 控制位:
    • URG:紧急信号
    • ACK:确认信号-------该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1
    • PSH:是否立即从TCP缓存区读走数据
    • RST:复位标识-------该位为 1 时,表示 TCP 连接中出现异常必须强制断开重新连接。
    • SYN:同步序列号标识(TCP连接时使用)-------- 该位为 1 时,表示希望建立连接(握手),并在其「序列号」的字段进行序列号初始值的设定。
    • FIN:结束序列号标识(TCP断开时使用)-------- 该位为 1 时,表示今后不会再有数据发送(已经完成),希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。
  • 16位窗口:不是滑动窗口的大小,滑动窗口的大小是固定的,他是用来记录接受缓冲区的大小,如果大小为0,就不会发送数据了,以此来达到流量控制。

特点如下:

  • TCP充分地实现了数据传输时各种控制功能,可以进行丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。而这些在 UDP 中都没有。
  • 此外,TCP 作为一种面向有连接的协议,只有在确认通信对端存在时才会发送数据,从而可以控制通信流量的浪费。
  • 根据 TCP 的这些机制,在 IP 这种无连接的网络上也能够实现高可靠性的通信( 主要通过检验和、序列号、确认应答、重发控制、连接管理以及窗口控制等机制实现)

区别

UDPTCP两者的都位于传输层,如下图所示:

两者区别如下表所示:

TCP UDP
可靠性 可靠 不可靠
连接性 面向连接 无连接
报文 面向字节流 面向报文
效率 传输效率低 传输效率高
双共性 全双工 一对一、一对多、多对一、多对多
流量控制 滑动窗口
拥塞控制 慢开始、拥塞避免、快重传、快恢复
传输效率
  • TCP 是面向连接的协议,建立连接3次握手、断开连接四次挥手,UDP是面向无连接,数据传输前后不连接连接,发送端只负责将数据发送到网络,接收端从消息队列读取
  • TCP 提供可靠的服务,传输过程采用流量控制、编号与确认、计时器等手段确保数据无差错,不丢失。UDP 则尽可能传递数据,但不保证传递交付给对方
  • TCP 面向字节流,将应用层报文看成一串无结构的字节流,分解为多个TCP报文段传输后,在目的站重新装配。UDP协议面向报文,不拆分应用层报文,只保留报文边界,一次发送一个报文,接收方去除报文首部后,原封不动将报文交给上层应用
  • TCP 只能点对点全双工通信。UDP 支持一对一、一对多、多对一和多对多的交互通信

两者应用场景如下图:

可以看到,TCP 应用场景适用于对效率要求低,对准确性要求高或者要求有链接的场景,而UDP 适用场景为对效率要求高,对准确性要求低的场景

2、说说TCP为什么需要三次握手和四次挥手?

三次握手

三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包

主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备

上述每一次握手的作用如下:tcp是全双工的------seq序列号、小写ack确认应答号

  • 第一次握手:(客户端和服务端主动握手)客户端发送网络包,服务端收到了 这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。---------------客户端会随机初始化序号(client_isn、如图seq初始化为x),将tcp首部的syn标志为1,表示syn报文,并传给服务端,服务端变成listen状态,客户端处于syn-sent状态
  • 第二次握手:(服务端应答后和客户端握手)服务端发包,客户端收到了 这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常-----------------服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn,如图seq设为y),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」(如图ack)字段填入 client_isn + 1, 接着把 SYNACK 标志位置为 1。最后把该报文(SYN+ACK报文)发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态
  • 第三次握手:客户端发包,服务端收到了。 这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常---------------------客户端收到服务端报文后,还要向服务端回应最后一个应答报文(ACK报文),首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务器的数据,之后客户端处于 ESTABLISHED 状态,服务器收到客户端的应答报文后,也进入 ESTABLISHED 状态。

第三次握手是可以携带数据的,前两次握手是不可以携带数据的--------------面试常问

通过三次握手,就能确定双方的接收和发送能力是正常的。之后就可以正常通信了

为什么不是两次握手、四次?

TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。

如果是两次握手,无法可靠的同步双方序列号,发送端可以确定自己发送的信息能对方能收到,也能确定对方发的包自己能收到,但接收端只能确定对方发的包自己能收到 无法确定自己发的包对方能收到

并且两次握手的话, 客户端有可能因为网络阻塞等原因会发送多个请求报文,延时到达的请求又会与服务器建立连接,浪费掉许多服务器的资源

无需四次握手,三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。

四次挥手

tcp终止一个连接,需要经过四次挥手

过程如下:

  • 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号seq。此时客户端处于 FIN_WAIT1 状态,停止发送数据,等待服务端的确认
  • 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的确认序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT状态(不能合并的原因是服务端接受到FIN后,可能还有需要发送的消息)
  • 第三次挥手:如果服务端如果也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号seq。此时服务端处于 LAST_ACK 的状态
  • 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态(不会立即断开,一般客户端在经过 2MSL 一段时间后,自动进入 CLOSED 状态,至此客户端也完成连接的关闭),服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态

这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。

为什么 TIME_WAIT 等待的时间是 2MSL?

MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。

TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。可以看到 2MSL时长 这其实是相当于至少允许报文丢失一次,让服务端超时重传并接收

为什么需要四次挥手

  • 关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
  • 服务器收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。

从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACKFIN 一般都会分开发送,从而比三次握手导致多了一次。

简述三次握手

第一次握手:建立连接时,客户端发送syn包(syn=x)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

简述四次挥手

第一次挥手:客户端A发送一个FIN.用来关闭客户A到服务器B的数据传送
第二次挥手:服务器B收到这个FIN. 它发回一个ACK,确认序号为收到的序号+1。和SYN一样,一个FIN将占用一个序号
第三次挥手:服务器B关闭与客户端A的连接,发送一个FIN给客户端A
第四次挥手:客户端A发回ACK报文确认,并将确认序号设置为序号加1

3、讲一讲tcp中的滑动窗口

TCP 巨复杂,它为了保证可靠性,用了巨多的机制来保证。为了实现可靠性传输,需要考虑很多事情,例如数据的破坏、丢包、重复以及分片顺序混乱等问题。如不能解决这些问题,也就无从谈起可靠传输。那么,TCP 是通过序列号、确认应答、重发控制、连接管理以及窗口控制等机制实现可靠性传输的。

  • 由于TCP是双工的,客户端有自己的缓冲区,服务端也有自己的缓冲区,要发送的数据都放到发送端的缓冲区,发送窗口就是发送缓冲区的一部分,会根据网络状况调整发送数据的多少。

  • 我们发送数据的时候是乱序发送的,当我们收到某个包的时候,可能前面的包没有收到,此时需要等待前面的序号的包收到了才可以(队头阻塞)

  • 服务端会和客户端说明发送数据的个数

  • 如果某个数据丢包了,那需要重新发送(超时重传 ROT)

  • 当接受方的窗口大小收满了,每隔一段时间,发送方会发送一个探测包,来询问是否能够调整窗口大小。上层协议消耗掉了接收方的数据,接收方也会通知发送方调整窗口,继续发送数据。

  • 滑动窗口是一种流量控制,控制发送方的频率:在建立连接时 . 接收端会告诉发送端自己的窗口大小 (rwnd),每次接收端收到数据后都会再次确认 (rwnd) 大小, 如果值为0 停止发送数据. (并发送窗口探测包,持续监测窗口大小)

  • swnd ≈ rwnd

4、怎么让发送方避免发送小数据呢?(粘包)------解决糊涂窗口综合症

使用 Nagle 算法,该算法的思路是延时处理,只有满足下面两个条件中的任意一个条件,才能可以发送数据:

条件一:要等到窗口大小 >= MSS 或是 数据大小 >= MSS;(此值就是帧的大小1500字节-ip头-tcp头=1460字节 ---理论值)

条件二:收到之前发送数据的 ack 回包;

只要上面两个条件都不满足,发送方一直在囤积数据,直到满足上面的发送条件。

5、TCP如何进行拥塞处理

TCP 不能忽略网络上发生的事,受带宽的影响,它被设计成一个无私的协议,当网络发送拥塞时,TCP 会自我牺牲,降低发送的数据量。

于是,就有了拥塞控制,控制的目的就是避免「发送方」的数据填满整个网络。

当发生超时重传时就认为网络出现了拥塞

1、慢启动算法

可以看出慢启动算法,发包的个数是指数性的增长。

有一个叫慢启动门限 ssthresh (slow start threshold)状态变量。

当 cwnd < ssthresh 时,使用慢启动算法。

当 cwnd >= ssthresh 时,就会使用「拥塞避免算法」。

一般来说 ssthresh 的大小是 65535 字节。

2、拥塞避免

那么进入拥塞避免算法后,它的规则是:每当收到一个 ACK 时,cwnd 增加 1/cwnd。

接上前面的慢启动的栗子,现假定 ssthresh 为 8:

当 8 个 ACK 应答确认到来时,每个确认增加 1/8,8 个 ACK 确认 cwnd 一共增加 1,于是这一次能够发送 9 个 MSS 大小的数据,变成了线性增长。

就这么一直增长着后,网络就会慢慢进入了拥塞的状况了,于是就会出现丢包现象,这时就需要对丢失的数据包进行重传。

当触发了重传机制,也就进入了「拥塞发生算法」。

3、超时重传,拥塞发生算法

当发生了「超时重传」,则就会使用拥塞发生算法。

这个时候,ssthresh 和 cwnd 的值会发生变化:

ssthresh 设为 cwnd/2

cwnd 重置为 1 (是恢复为 cwnd 初始化值,我这里假定 cwnd 初始化值 1)

4、快速地重传

当接收方发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。

TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthresh 和 cwnd 变化如下:

cwnd = cwnd/2 ,也就是设置为原来的一半;

ssthresh = cwnd;

进入快速恢复算法

拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了);

重传丢失的数据包;

如果再收到重复的 ACK,那么 cwnd 增加 1;

如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;

6、TCP 的 Keepalive 和 HTTP 的 Keep-Alive 是一个东西吗?

  • HTTP 的 Keep-Alive,是由应用层(用户态) 实现的,称为 HTTP 长连接;
  • TCP 的 Keepalive,是由 TCP 层(内核态) 实现的,称为 TCP 保活机制;

7、可以谈谈对CDN网络架构的理解吗?

CDN (全称 Content Delivery Network),即内容分发网络

构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN 的关键技术主要有内容存储和分发技术

简单来讲,CDN就是根据用户位置分配最近的资源

原理分析

在没有应用CDN时,我们使用域名访问某一个站点时的路径为

用户提交域名→浏览器对域名进行解释→DNS 解析得到目的主机的IP地址→根据IP地址访问发出请求→得到请求数据并回复

应用CDN后,DNS 返回的不再是 IP 地址,而是一个CNAME(Canonical Name ) 别名记录,指向CDN的全局负载均衡

CNAME实际上在域名解析的过程中承担了中间人(或者说代理)的角色,这是CDN实现的关键

负载均衡系统

由于没有返回IP地址,于是本地DNS会向负载均衡系统再发送请求 ,则进入到CDN的全局负载均衡系统进行智能调度:

  • 看用户的 IP 地址,查表得知地理位置,找相对最近的边缘节点
  • 看用户所在的运营商网络,找相同网络的边缘节点
  • 检查边缘节点的负载情况,找负载较轻的节点
  • 其他,比如节点的“健康状况”、服务能力、带宽、响应时间等

结合上面的因素,得到最合适的边缘节点,然后把这个节点返回给用户,用户就能够就近访问CDN的缓存代理

整体流程如下图:

缓存代理

缓存系统是 CDN的另一个关键组成部分,缓存系统会有选择地缓存那些最常用的那些资源

其中有两个衡量CDN服务质量的指标:

  • 命中率:用户访问的资源恰好在缓存系统里,可以直接返回给用户,命中次数与所有访问次数之比
  • 回源率:缓存里没有,必须用代理的方式回源站取,回源次数与所有访问次数之比

缓存系统也可以划分出层次,分成一级缓存节点和二级缓存节点。一级缓存配置高一些,直连源站,二级缓存配置低一些,直连用户

回源的时候二级缓存只找一级缓存,一级缓存没有才回源站,可以有效地减少真正的回源

现在的商业 CDN命中率都在 90% 以上,相当于把源站的服务能力放大了 10 倍以上

资源上传cdn之后,当用户访问cdn的资源地址之后会经历下面的步骤:

  1. 首先经过本地的dns解析,请求cname指向的那台cdn专用的dns服务器。
  2. dns服务器返回全局负载均衡的服务器ip给用户
  3. 用户请求全局负载均衡服务器,服务器根据ip返回所在区域的负载均衡服务器ip给用户
  4. 用户请求区域负载均衡服务器,负载均衡服务器根据用户ip选择距离近的,并且存在用户所需内容的,负载比较合适的一台缓存服务器ip给用户。当没有对应内容的时候,会去上一级缓存服务器去找,直到找到资源所在的源站服务器,并且缓存在缓存服务器中。用户下一次在请求该资源,就可以就近拿缓存了

四、一个TCP连接可以发送多少个HTTP请求?就这这个问题,我们聊聊TCP、HTTP以及浏览器之间的关系和对请求处理的优化。

TCP与HTTP的渊源

我们知道TCP协议对应于传输层,HTTP协议对应于应用层。WEB项目中,HTTP协议是建立在TCP的基础上的。

最初浏览器从服务器加载一个网页,会发起一个HTTP请求,这时需要先建立一个TCP连接。当本次数据请求完毕之后,会立刻断开TCP连接。

但随着时间的推理,HTML网页内容越来越复杂,不仅有内容,还有JS、CSS和图片资源,每个资源的请求都建立一次TCP连接,效率就会很低。

这时,Keep-Alive就被提出用来了,专门用于解决效率低的问题。

本文关于TCP连接能够发送多少个HTTP请求,本质上就是围绕着解决通信的低效问题的。

下面我们通过几个常见的面试问题,来逐步揭开这其中包含的知识点。

问题一:浏览器建立TCP连接之后,完成一次HTTP请求,是否会断开?

HTTP协议Header中的Connection属性决定了连接是否持久,不同HTTP协议版本有所不同。

HTTP/1.0中Connection默认为close,即每次请求都会重新建立和断开TCP连接。缺点:建立和断开TCP连接,代价过大。

HTTP/1.1中Connection默认为keep-alive,即连接可以复用,不用每次都重新建立和断开TCP连接。超时之后没有连接则主动断开。可以通过声明Connection为close进行关闭。

优点:TCP连接可被重复利用,减少建立连接的损耗,SSL的开销也可以避免。刷新页面时也可以复用,从而不再建立SSL连接等。

结论:默认情况下(HTTP/1.1)建立TCP连接不会断开,只有在请求报头中声明Connection: close才会请求完成之后关闭连接。不断开的最终目的是减少建立连接所导致的性能损耗。

问题二:一个TCP连接可以对应几个HTTP请求?

如果Connection为close,则一个TCP连接只对应一个HTTP请求。

如果Connection为Keep-alive,则一个TCP连接可对应一个到多个HTTP请求。

问题三:一个TCP连接中,可以同时发送多个HTTP请求吗?

HTTP/1.1中单个TCP连接在同一时刻只能处理一个请求。HTTP/1.1在RFC 2616中规定了Pipelining来解决这个问题,但浏览器默认是关闭的。

RFC 2616中规定:一个支持持久连接的客户端可以在一个连接中发送多个请求(不需要等待任意请求的响应)。收到请求的服务器必须按照请求收到的顺序发送响应。

Pipelining本身存在一些问题,比如代理服务器不能正确处理HTTP Pipelining、Head-of-line Blocking连接头阻塞(首个请求耗时过长,阻塞其他请求)。所以,浏览器默认关闭该功能。

HTTP/2.0提供了多路复用技术Multiplexing,一个TCP可以并发多个HTTP请求(理论无上限,但是一般浏览器会有TCP并发数的限制)。

HTTP/1.1中为了提升性能,通常会采用连接复用和同时建立多个TCP连接的方式提升性能。

结论:HTTP/1.1中存在Pipelining技术支持一个连接发送多个请求,但存在弊端,浏览器默认关闭。HTTP/2.0中通过多路复用技术支持一个TCP连接中并发请求HTTP。

问题四:浏览器对同一Host建立TCP连接的数量有没限制?

不同浏览器限制不同,比如Chrome最多允许同一个Host可建立6个TCP连接。

如果服务器只支持HTTP/1.1,浏览器会采用在同一个Host下建立多个TCP连接来进行效率提升。如果是基于HTTPS传输,在SSL握手之后,还会尝试协商是否可以采用HTTP/2.0的Multiplexing功能。

问题五:keep-alive使用场景及优缺点

开启keep-alive对内存要求高,关闭keep-alive对CPU要求高;如果内存和CPU都足够,开启和关闭keep-alive对性能影响不大;如果考虑服务器压力,如果是静态页面,大量的调用js或者图片的话,建议开启keep-alive;如果是动态网页,建议关闭keep-alive。

注意事项:如果需要使用keep-alive功能,服务器端如果使用nginx中keepalive_timeout值要大于0。

四、说说地址栏输入 URL 敲下回车后发生了什么?

简单的分析,从输入 URL到回车后发生的行为如下:

  • URL解析
  • DNS 查询
  • TCP 连接
  • HTTP 请求
  • 响应请求
  • 页面渲染

详细分析

URL解析

首先判断你输入的是一个合法的URL 还是一个待搜索的关键词,并且根据你输入的内容进行对应操作

URL的解析第过程中的第一步,一个url的结构解析如下:

DNS查询

在之前文章中讲过DNS的查询,这里就不再讲述了

整个查询过程如下图所示:

最终,获取到了域名对应的目标服务器IP地址

TCP连接

在之前文章中,了解到tcp是一种面向有连接的传输层协议

在确定目标服务器服务器的IP地址后,则经历三次握手建立TCP连接,流程如下:

发送 http 请求

当建立tcp连接之后,就可以在这基础上进行通信,浏览器发送 http 请求到目标服务器

请求的内容包括:

  • 请求行
  • 请求头
  • 请求主体

响应请求

当服务器接收到浏览器的请求之后,就会进行逻辑操作,处理完成之后返回一个HTTP响应消息,包括:

  • 状态行
  • 响应头
  • 响应正文

在服务器响应之后,由于现在http默认开始长连接keep-alive,当页面关闭之后,tcp链接则会经过四次挥手完成断开

页面渲染

当浏览器接收到服务器响应的资源后,首先会对资源进行解析:

  • 查看响应头的信息,根据不同的指示做对应处理,比如重定向,存储cookie,解压gzip,缓存资源等等
  • 查看响应头的 Content-Type的值,根据不同的资源类型采用不同的解析方式

关于页面的渲染过程如下:

  • 解析HTML,构建 DOM 树
  • 解析 CSS ,生成 CSS 规则树
  • 合并 DOM 树和 CSS 规则,生成 render 树
  • 布局 render 树( Layout / reflow ),负责各元素尺寸、位置的计算
  • 绘制 render 树( paint ),绘制页面像素信息
  • 浏览器会将各层的信息发送给 GPU,GPU 会将各层合成( composite ),显示在屏幕上

五、说说对WebSocket的理解?应用场景?

WebSocket,是一种网络传输协议,位于OSI模型的应用层。可在单个TCP连接上进行全双工通信,能更好的节省服务器资源和带宽并达到实时通迅

客户端和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输

从上图可见,websocket服务器与客户端通过握手连接,连接成功后,两者都能主动的向对方发送或接受数据

而在websocket出现之前,开发实时web应用的方式为轮询

不停地向服务器发送 HTTP 请求,问有没有数据,有数据的话服务器就用响应报文回应。如果轮询的频率比较高,那么就可以近似地实现“实时通信”的效果

轮询的缺点也很明显,反复发送无效查询请求耗费了大量的带宽和 CPU资源

一、特点

全双工

通信允许数据在两个方向上同时传输,它在能力上相当于两个单工通信方式的结合

例如指 A→B 的同时 B→A ,是瞬时同步的

二进制帧

采用了二进制帧结构,语法、语义与 HTTP 完全不兼容,相比http/2WebSocket更侧重于“实时通信”,而HTTP/2 更侧重于提高传输效率,所以两者的帧结构也有很大的区别

不像 HTTP/2 那样定义流,也就不存在多路复用、优先级等特性

自身就是全双工,也不需要服务器推送

协议名

引入wswss分别代表明文和密文的websocket协议,且默认端口使用80或443,几乎与http一致

ws://www.chrono.com
ws://www.chrono.com:8080/srv
wss://www.chrono.com:445/im?user_id=xxx

握手

WebSocket也要有一个握手过程,然后才能正式收发数据

客户端发送数据格式如下:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
  • Connection:必须设置Upgrade,表示客户端希望连接升级
  • Upgrade:必须设置Websocket,表示希望升级到Websocket协议
  • Sec-WebSocket-Key:客户端发送的一个 base64 编码的密文,用于简单的认证秘钥。要求服务端必须返回一个对应加密的“Sec-WebSocket-Accept应答,否则客户端会抛出错误,并关闭连接
  • Sec-WebSocket-Version :表示支持的Websocket版本

服务端返回的数据格式:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Sec-WebSocket-Protocol: chat
  • HTTP/1.1 101 Switching Protocols:表示服务端接受 WebSocket 协议的客户端连接
  • Sec-WebSocket-Accep:验证客户端请求报文,同样也是为了防止误连接。具体做法是把请求头里“Sec-WebSocket-Key”的值,加上一个专用的 UUID,再计算摘要

优点

  • 较少的控制开销:数据包头部协议较小,不同于http每次请求需要携带完整的头部
  • 更强的实时性:相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少
  • 保持创连接状态:创建通信后,可省略状态信息,不同于HTTP每次请求需要携带身份验证
  • 更好的二进制支持:定义了二进制帧,更好处理二进制内容
  • 支持扩展:用户可以扩展websocket协议、实现部分自定义的子协议
  • 更好的压缩效果:Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率

二、应用场景

基于websocket的事实通信的特点,其存在的应用场景大概有:

  • 弹幕
  • 媒体聊天
  • 协同编辑
  • 基于位置的应用
  • 体育实况更新
  • 股票基金报价实时更新

浏览器

一、Javascript本地存储

什么是cookie?

由于HTTP是一种无状态的协议,服务器单从网络连接上是无法知道客户身份的。这时候服务器就需要给客户端颁发一个cookie,用来确认用户的身份。

简单的说,cookie就是客户端保存用户信息的一种机制,用来记录用户的一些信息

原理:web服务器通过在http响应消息头增加Set-Cookie响应头字段将Cookie信息发送给浏览器,浏览器则通过在http请求消息中增加Cookie请求头字段将Cookie回传给web服务器。

cookie的构成

服务器端向客户端发送Cookie是通过HTTP响应报文实现的,在Set-Cookie中设置需要向客户端发送的cookie,cookie格式如下:

Set-Cookie: "name=value;domain=.domain.com;path=/;expires=Sat, 11 Jun 2019 11:29:42 GMT;HttpOnly;secure"
复制代码

其中name=value是必选项,其它都是可选项。Cookie的主要构成如下:

  • name:一个唯一确定的cookie名称。通常来讲cookie的名称是不区分大小写的。
  • value:存储在cookie中的字符串值。最好为cookie的name和value进行url编码
  • domain:cookie对于哪个域是有效的。所有向该域发送的请求中都会包含这个cookie信息。这个值可以包含子域(如:e.baidu.com),也可以不包含它(如:.baidu.com,则对于baidu.com的所有子域都有效)。
  • path: 表示这个cookie影响到的路径,浏览器跟会根据这项配置,像指定域中匹配的路径发送cookie。
  • expires:失效时间,表示cookie何时应该被删除的时间戳(也就是,何时应该停止向服务器发送这个cookie)。如果不设置这个时间戳,浏览器会在页面关闭时即将删除所有cookie;不过也可以自己设置删除时间。这个值是GMT时间格式。如果客户端和服务器端时间不一致,使用expires就会存在偏差。并且如果给cookie设置一个过去的时间,浏览器会立即删除该cookie
  • max-age: 与expires作用相同,用来告诉浏览器此cookie多久过期(单位是秒),而不是一个固定的时间点。正常情况下,max-age的优先级高于expires。
  • HttpOnly: 告知浏览器不允许通过脚本document.cookie去更改这个值,同样这个值在document.cookie中也不可见。但在http请求张仍然会携带这个cookie。注意这个值虽然在脚本中不可获取,但仍然在浏览器安装目录中以文件形式存在。这项设置通常在服务器端设置。
  • secure: 安全标志,指定后,只有在使用SSL链接时候才能发送到服务器,如果是http链接则不会传递该信息。HttpOnly和secure一定程度上控制xxs攻击。

这里强调一点,是Cookie的不可跨域名性
很多网站都会使用Cookie,不同浏览器采用不同的方式保存Cookie,而且每个网站的Cookie只能够被对应的网站使用。意思就是说当浏览器访问baidu时,只会带baidu的Cookie,而不会带其他网站的Cookie,这就是Cookie的不可跨域名性 。 Cookie在客户端是由浏览器来管理的。浏览器可以保证各个网站只能操作各个网站的Cookie,从而保证用户的隐私安全。

cookie的特点

Cookie并不提供修改、删除操作

如果要修改某个Cookie,只需要新建一个同名的Cookie,添加到response中覆盖原来的Cookie。

如果要删除某个Cookie,只需要新建一个同名的Cookie,并将maxAge设置为0,并添加到response中覆盖原来的Cookie。注意是0而不是负数。负数代表其他的意义。

注意:修改、删除Cookie时,新建的Cookie除value、maxAge之外的所有属性,例如name、path、domain等,都要与原Cookie完全一样。否则,浏览器将视为两个不同的Cookie不予覆盖,导致修改、删除失败。

session

什么是session?

Session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而Session保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。
客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了

session的工作步骤

因为HTTP协议是无状态的,Session不能依据HTTP连接来判断是否为同一个用户。于是服务器向用户的浏览器发送了一个名为JESSIONID的Cookie,它的值是Session的id值。这个id可以让Session依据Cookie来识别是否是同一个用户。

简单来说:Session 之所以可以识别不同的用户,依靠的就是Cookie,所以说session是基于Cookie的

该Cookie是服务器自动颁发给浏览器的,不用我们手工创建的。该Cookie的maxAge值默认是-1,也就是说仅当前浏览器使用,不将该Cookie存在硬盘中,并且各浏览器窗口间不共享,关闭浏览器就会失效。

工作步骤:
将客户端称为 client,服务端称为 server

  1. 产生 sessionID:session 是基于 cookie 的一种方案,所以,首先要产生 cookie。client 第一次访问 server,server 生成一个随机数,命名为 sessionID,并将其放在响应头里,以 cookie 的形式返回给 client,client 以处理其他 cookie 的方式处理这段 cookie。大概是这样:cookie:sessionID=135165432165
  2. 保存 sessionID: server 将要保存的数据保存在相对应的 sessionID 之下,再将 sessionID 保存到服务器端的特定的保存 session 的内存中(如 一个叫 session 的哈希表)
  3. 使用 session: client 再次访问 server,会带上首次访问时获得的 值为 sessionID 的cookie,server 读取 cookie 中的 sessionID,根据 sessionID 到保存 session 的内存寻找与 sessionID 匹配的数据,若寻找成功就将数据返回给 client。

session的有效期

Session保存在服务器端。为了获得更高的存取速度,服务器一般把Session放在内存里。每个用户都会有一个独立的Session。如果Session内容过于复杂,当大量客户访问服务器时可能会导致内存溢出。因此,Session里的信息应该尽量精简。

Session生成后,只要用户继续访问,服务器就会更新Session的最后访问时间,并维护该Session。用户每访问服务器一次,无论是否读写Session,服务器都认为该用户的Session“活跃(active)”了一次。

由于会有越来越多的用户访问服务器,因此Session也会越来越多。为防止内存溢出,服务器会把长时间内没有活跃的Session从内存删除。这个时间就是Session的超时时间。如果超过了超时时间没访问过服务器,Session就自动失效了。

cookie与session的区别

  • Cookie数据存放在客户端,Session数据放在服务器端
  • Cookie的安全性一般,他人可通过分析存放在本地的Cookie并进行Cookie欺骗。在安全性第一的前提下,选择Session更优。重要交互信息比如权限等就要放在Session中,一般的信息记录放Cookie中
  • 单个Cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个Cookie,而Session原则上没有限制
  • Session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能考虑到减轻服务器性能方面,应当使用Cookie。
  • Session 的运行依赖Session ID,而 Session ID 是存在 Cookie 中的,也就是说,如果浏览器禁用了 Cookie,Session 也会失效(但是可以通过其它方式实现,比如在 url 中传递 Session ID,也就是地址重写)

webStorage---localStorage、sessionStorage

什么是localStorage?

localStorage 是 HTML5 提供的一个 API,他本质上是一个hash(哈希表),是一个存在于浏览器上的 hash(哈希表)。

localStorage生命周期是永久,这意味着除非用户显示在浏览器提供的UI上清除localStorage信息,否则这些信息将永远存在。存放数据大小为一般为5MB,而且它仅在客户端(即浏览器)中保存,不参与和服务器的通信。

localStorage使用方法

localStorage和sessionStorage使用时使用相同的API:

localStorage.setItem("key","value");	//以“key”为名称存储一个值“value”

localStorage.getItem("key");	//获取名称为“key”的值

localStorage.removeItem("key");	//删除名称为“key”的信息。

localStorage.clear();	//清空localStorage中所有信息
复制代码

localStorage 是一个保存于客户端的哈希表,可以用来保存本地的一些数据。并且不会因为刷新而释放,所以,可以使用 localStorage 来实现变量的持久化存储

localStorage的特点

  • localStorage 与 HTTP 没有任何关系,所以在HTTP请求时不会带上 localStorage 的值
  • 只有相同域名的页面才能互相读取 localStorage,同源策略与 cookie 一致
  • 不同的浏览器,对每个域名 localStorage 的最大存储量的规定不一样,超出存储量会被拒绝。最大存5M 超过5M的数据就会丢失。而 Chrome 10MB 左右
  • 常用来记录一些不敏感的信息
  • localStorage 理论上永久有效,除非用户清理缓存

sessionStorage

sessionStorage 的所有性质基本上与 localStorage 一致,唯一的不同区别在于:
sessionStorage 的有效期是页面会话持续,如果页面会话(session)结束(关闭窗口或标签页),sessionStorage 就会消失。而 localStorage 则会一直存在。

localStorage与sessionStorage的区别

  • localStorage生命周期是永久的,除非被清除,否则永久保存,而sessionStorage仅在当前会话下有效,关闭页面或浏览器后被清除

相同点可以参考localStorage的特点
这里再强调一下,这两个存储方式用来存放数据大小一般为5MB,并且仅在客户端(即浏览器)中保存,不参与和服务器的通信。

扩展的前端存储方式

indexedDB是一种低级API,用于客户端存储大量结构化数据(包括, 文件/ blobs)。该API使用索引来实现对该数据的高性能搜索

虽然 Web Storage对于存储较少量的数据很有用,但对于存储更大量的结构化数据来说,这种方法不太有用。IndexedDB提供了一个解决方案

优点:

  • 储存量理论上没有上限
  • 所有操作都是异步的,相比 LocalStorage 同步操作性能更高,尤其是数据量较大时
  • 原生支持储存JS的对象
  • 是个正经的数据库,意味着数据库能干的事它都能干

缺点:

  • 操作非常繁琐
  • 本身有一定门槛

二、描述下cookie,sessionStorage,localStorage的差异

  1. 都是保存在浏览器端、且同源的
  2. cookie大小4KB 左右,跟随请求(请求头),会占用带宽资源,但是若是用来判断用户是否在线这些挺方便,sessionStorage和localStorage虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大
  3. 数据有效期不同,sessionStorage:仅在当前浏览器窗口关闭之前有效;localStorage:始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;cookie:只在设置的cookie过期时间之前有效,即使窗口关闭或浏览器关闭
  4. 作用域不同,sessionStorage不在不同的浏览器窗口中共享,即使是同一个页面;localstorage在所有同源窗口中都是共享的;cookie也是在所有同源窗口中都是共享的

Cookie

  1. Cookie 的本职工作并非本地存储,而是“维持状态”。
  2. Cookie 不够大
  3. 同一个域名下的所有请求,都会携带 Cookie

Local Storage,Session Storage

  1. 存储容量大: Web Storage 根据浏览器的不同,存储容量可以达到 5-10M 之间
  2. 仅位于浏览器端,不与服务端发生通信。

生命周期:Local Storage 是持久化的本地存储,存储在其中的数据是永远不会过期的,使其消失的唯一办法是手动删除;而 Session Storage 是临时性的本地存储,它是会话级别的存储,当会话结束(页面被关闭)时,存储内容也随之被释放。

作用域:Local Storage、Session Storage 和 Cookie 都遵循同源策略。但 Session Storage 特别的一点在于,即便是相同域名下的两个页面,只要它们不在同一个浏览器窗口中打开,那么它们的 Session Storage 内容便无法共享。

应用场景

Local Storage

  1. 理论上 Cookie 无法胜任的、可以用简单的键值对来存取的数据存储任务,都可以交给 Local Storage 来做。
  2. 存储一些内容稳定的资源。比如图片内容丰富的电商网站会用它来存储 Base64 格式的图片字符串
  3. 存储一些不经常更新的 CSS、JS 等静态资源

Session Storage

微博的 Session Storage 就主要是存储你本次会话的浏览足迹

IndexedDB

  1. IndexedDB 是没有存储上限的(一般来说不会小于 250M)
  2. IndexedDB 可以看做是 LocalStorage 的一个升级,当数据的复杂度和规模上升到了 LocalStorage 无法解决的程度,我们毫无疑问可以请出 IndexedDB 来帮忙。
  3. 富文本

三、进程和线程

进程和线程的概念

首先我们要先了解进程和线程的概念,在计算机原理中:

1.进程是CPU资源分配的最小单位(是能拥有资源和独立运行的最小单位,进程之间不会共享资源)
2.线程是CPU调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程,多个线程之间共享进程的资源)
3.不同进程之间也可以通信,但是代价会比较大

而这里进程和线程的关系就像是工厂和工厂的工人,不同的工厂有自己的资源,空间。而工厂里面也会有很多的工人,工人是没有自己的资源和空间的,但是他们可以使用和共享工厂的资源和空间

浏览器中的进程

在理解了进程和线程以后,我们再来对浏览器进行一定程度上的认识。

1.浏览器是多进程
2.浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)
3.浏览器每新开一个页签,系统相当于创建了一个独立的进程

如何验证呢?最简单的方式,就是直接打开浏览器的任务管理器,会发现每一个新开的页签,都会在任务管理器中新增一个进程,浏览器会给页签分配内存和cpu

另外一种验证方式,就是写一个死循环,我们会发现当前这个页签的死循环了,但是其他的页签并不会受影响,而在本文一开始,我们就提到了,线程是共享资源的,进程是资源独立的。所以页签如果是线程,也会受到影响。

浏览器中有哪些进程

知道了浏览器是多进程,那么浏览器主要有哪些进程呢?

1.浏览器进程(Browser进程):浏览器的主进程(负责协调,主控),只有一个

1)负责浏览器的界面界面显示,与用户交互,网址栏输入、前进、后退等
2)负责管理各个页面,创建和销毁进程
3)将页面内容(位图)写入到浏览器内存中,最后将图像显示在屏幕上
4)文件存储等功能

2.渲染进程(浏览器内核,Renderer进程,内部是多线程的):默认一个tab页面一个渲染进程(特殊情况下:渲染进程不一定每个tab就一个),主要的作用为页面渲染,脚本执行,事件处理等

3.GPU进程:用于3D绘制等,将开启了3D绘制的元素的渲染由CPU转向GPU,也就是开启GPU加速。最多一个

4.网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面,现在独立开来,成为一个单独的进程

5.插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建

6.音频进程:浏览器音频管理

浏览器多进程的好处

相比于单进程浏览器,多进程有几点好处:

1.避免单个页面崩溃影响整个浏览器
2.避免第三方插件崩溃时影响整个浏览器
3.多进程充分利用多核优势
4.方便使用沙盒模型隔离插件等进程,提高浏览器稳定性

整体来说就是利用空间换时间,牺牲内存

浏览器的线程

上面一直在说浏览器的进程,那么我们的线程存在于哪里了?

线程主要存在于渲染进程里面,也就是我们常说的浏览器内核里面,浏览器内核中的几种引擎便是我们的主要使用到的线程,那么接下来看看渲染进程中主要都包含了哪些线程。

1.GUI渲染线程

1)负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制
2)当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
3)与JS引擎互斥,当执行JS引擎线程时,GUI会pending,当任务队列空闲时,才会继续执行GUI

2.JS引擎线程

1)也称为JS内核,负责处理javascript脚本程序
2)JS引擎线程负责解析Javascript脚本,运行代码
3)JS引擎一直等待任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JS线程在运行JS程序
4)同样注意,GUI渲染线程与JS引擎线程时互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。

3.事件触发线程

1)事件触发线程归属于浏览器而不是JS引擎(辅助JS引擎),用来控制事件循环(存在一个事件队列)
2)当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击,Ajax异步请求等),会将对应的任务添加到事件线程中
3)当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
4)注意,由于JS的单线程关系,所以这些待处理队列的事件都得排队等待JS引擎的处理(当JS引擎空闲时才会去执行)

4.定时触发器线程

1)setInterval、setTimeOut所在线程
2)浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎时单线程的,如果处于阻塞线程状态就会影响计时的准确)
3)因此通过单独线程来计时并触发(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
4)注意,W3C在HTML标准中规定要求setTimeOut中低于4ms的时间间隔为4ms

5.异步HTTP请求线程(IO线程)

1)在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
2)将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中(放入事件触发线程中)。再由JavaScript引擎执行。

从上面的概念我们可以得到几点总结:

  1. 浏览器是多进程的。
  2. js执行的主线程为JS引擎,并且无论何时都只有一个JS线程在运行,所以是单线程执行。
  3. GUI渲染线程和JS引擎线程是互斥的,并且JS会阻塞页面的加载和渲染。
  4. 定时器(setInterval,setTimeout)会在定时器触发器线程中进行计时。
  5. 定时触发器线程计时结束后需要执行的事件和异步HTTP请求线程的回调事件都会进入到事件触发线程的任务队列中等待JS引擎的执行

四、浏览器渲染流程

首先,我们要知道浏览器中是谁在把代码渲染成是视图的?我们要了解其中具体的工作流程要从哪里开始?

浏览器内核

浏览器的内核是指支持浏览器运行的最核心的程序,分为两个部分的,一是渲染引擎,另一个是JS引擎。渲染引擎在不同的浏览器中也不是都相同的。比如在 Firefox 中叫做 Gecko,在 Chrome 和 Safari 中都是基于 WebKit 开发的。我们主要介绍关于 WebKit 的这部分渲染引擎内容以及几个相关的问题。

浏览器工作流程

  1. 文件解析

    • 解析HTML/SVG/XHTML,生成DOM Tree
    • 解析CSS,生成CSS Rule Tree
    • 解析javascript,通过DOM API和CSSOM API来操作DOM TREE 和CSS Rule Tree
  2. 通过DOM Tree 和CSS Rule Tree来构建

    Rendering Tree

    • Rendering Tree 并不同于DOM树,因为一些display:none 的东西没有必要放在Rendering Tree
    • CSS Rule Tree主要是为了完成匹配并把CSS Rule附加上Rendering Tree上的每个Element。也就是DOM节点。
    • 然后,也就是每个Element的位置,这又叫layoutreflow过程
  3. 调用操作系统的GUI 进行绘制

现在,我们已经知道了浏览器从获取到HTML文件流到渲染视图到页面的大体过程,那下面我们来了解一下其中具体的过程实现。

构建DOM Tree

浏览器会遵守一套步骤将HTML 文件转换为 DOM 树。宏观上,可以分为几个步骤:

字节数据=> 字符串 => Token =>Node => DOM

  • 浏览器从磁盘或网络读取HTML的原始字节,并根据文件的指定编码(例如 UTF-8)将它们转换成字符串。

  • 将字符串转换成Token,例如:<html><body>等。Token中会标识出当前Token是“开始标签”或是“结束标签”亦或是“文本”等信息

  • 生成节点对象并构建DOM

    事实上,构建DOM的过程中,不是等所有Token都转换完成后再去生成节点对象,而是一边生成Token一边消耗Token来生成节点对象。换句话说,每个Token被生成后,会立刻消耗这个Token创建出节点对象。注意:带有结束标签标识的Token不会创建节点对象。

构建CSSOM

构建CSSOM的过程与构建DOM的过程非常相似,当浏览器接收到一段CSS,浏览器首先要做的是识别出Token,然后构建节点并生成CSSOM。

在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。因为样式你可以自行设置给某个节点,也可以通过继承获得。在这一过程中,浏览器得递归 CSSOM 树,然后确定具体的元素到底是什么样式。

注意:CSS匹配HTML元素是一个相当复杂和有性能问题的事情。所以,DOM树要小,CSS尽量用id和class,千万不要过渡层叠下去

构建渲染树

当我们生成 DOM 树和 CSSOM 树以后,就需要将这两棵树组合为渲染树。在这一过程中,不是简单的将两者合并就行了。渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是 display: none的,那么就不会在渲染树中显示。

布局与绘制

当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流reflow)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。

布局流程的输出是一个“盒模型”,它会精确地捕获每个元素在视口内的确切位置和尺寸,所有相对测量值都将转换为屏幕上的绝对像素。

布局完成后,浏览器会立即发出“Paint Setup”和“Paint”事件,将渲染树转换成屏幕上的像素。

我们了解了浏览器解析到绘制DOM的详细过程,现在我们来重点关注一下其中几个相关的重点:
一、渲染 DOM 和CSSOM 时遇到js文件怎么处理?

在构建DOM时,HTML解析器若遇到了JavaScript,那么它会暂停构建DOM,将控制权移交给JavaScript引擎,等JavaScript引擎运行完毕,浏览器再从中断的地方恢复DOM构建。JS文件不只是阻塞DOM的构建,它会导致CSSOM也阻塞DOM的构建。

原本DOM和CSSOM的构建是互不影响,井水不犯河水,但是一旦引入了JavaScript,CSSOM也开始阻塞DOM的构建,只有CSSOM构建完毕后,DOM再恢复DOM构建。

这是因为JavaScript不只是可以改DOM,它还可以更改样式,也就是它可以更改CSSOM。我们知道,不完整的CSSOM是无法使用的,但JavaScript中想访问CSSOM并更改它,那么在执行JavaScript时,必须要能拿到完整的CSSOM。所以就导致了一个现象,如果浏览器尚未完成CSSOM的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟脚本执行和DOM构建,直至其完成CSSOM的下载和构建。也就是说,在这种情况下,浏览器会先下载和构建CSSOM,然后再执行JavaScript,最后在继续构建DOM。也就是优先级如下:

js文件下载 > 构建CSSOM > 构建DOM > 执行js

二、 认识回流和重绘

重绘:当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观、风格,而不会影响布局的,比如background-color。

回流::当render tree中的一部分(或全部)因为元素的规模尺寸、布局、隐藏等改变而需要重新构建

回流必定会发生重绘,重绘不一定会引发回流。重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。

常见引起回流属性和方法

任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发回流,

  • 添加或者删除可见的DOM元素;
  • 元素尺寸改变——边距、填充、边框、宽度和高度
  • 内容变化,比如用户在input框中输入文字
  • 浏览器窗口尺寸改变——resize事件发生时
  • 计算 offsetWidth 和 offsetHeight 属性
  • 设置 style 属性的值

常见引起重绘属性和方法

  • color等颜色类
  • background等背景类
  • visibility
三、如何减少回流和重绘
  • 使用 transform 替代 top
  • 避免使用CSS表达式calc()
  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
  • CSS 选择符从右往左匹配查找,避免节点层级过多
  • 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。
  • 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
  • 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中
  • 也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。
  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
四、async和defer的作用是什么?二者有什么区别?

如上图中,其中蓝色线代表JavaScript加载;红色线代表JavaScript执行;绿色线代表 HTML 解析。

我们可以看到,渲染引擎读取到script标签就会把控制权交给js引擎,js引擎会加载文件,然后执行js代码,如果script中定义了defer属性,那么js文件的下载会异步执行,并不影响DOM和CSSOM解析,等js下载完成且HTML和CSS解析完成再执行js代码。

而设置了async属性的script标签,与 defer 的区别在于,如果js文件加载好,就会立即开始执行——无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发之后。需要注意的是,这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,但一定在 load 触发之前执行。

五、为什么js操作DOM影响性能?

因为 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们通过 JS 操作 DOM 的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗。操作 DOM 次数一多,也就等同于一直在进行线程之间的通信,并且操作 DOM 可能还会带来重绘回流的情况,所以也就导致了性能上的问题。

六、渲染页面常发生哪些问题?

FOUC:由于浏览器渲染机制(比如firefox),再CSS加载之前,先呈现了HTML,就会导致展示出无样式内容,然后样式突然呈现的现象;

白屏:有些浏览器渲染机制(比如chrome)要先构建DOM树和CSSOM树,构建完成后再进行渲染,如果CSS部分放在HTML尾部,由于CSS未加载完成,浏览器迟迟未渲染,从而导致白屏;也可能是把js文件放在头部,脚本会阻塞后面内容的呈现,脚本会阻塞其后组件的下载,出现白屏问题。

前端设计模式

介绍一下单一职责原则和开放封闭原则

  • 单一职责原则:一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
  • 开放封闭原则:核心的思想是软件实体(类、模块、函数等)是可扩展的、但不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。

接手项目越来越复杂的时候,有时写完一段代码,总感觉代码还有优化的空间,却不知道从何处去下手。设计模式主要目的是提升代码可扩展性以及可阅读性。

设计模式是对软件设计开发过程中反复出现的某类问题的通用解决方案。设计模式更多的是指导思想和方法论,而不是现成的代码,当然每种设计模式都有每种语言中的具体实现方式。学习设计模式更多的是理解各种模式的内在思想和解决的问题,毕竟这是前人无数经验总结成的最佳实践,而代码实现则是对加深理解的辅助。

设计模式可以分为三大类:

  1. 结构型模式(Structural Patterns): 通过识别系统中组件间的简单关系来简化系统的设计。
  2. 创建型模式(Creational Patterns): 处理对象的创建,根据实际情况使用合适的方式创建对象。常规的对象创建方式可能会导致设计上的问题,或增加设计的复杂度。创建型模式通过以某种方式控制对象的创建来解决问题。
  3. 行为型模式(Behavioral Patterns):用于识别对象之间常见的交互模式并加以实现,如此,增加了这些交互的灵活性。

上述中一共有23种设计模式,但我们作为前端开发人员,需要了解的大概有以下10种。

创建型模式

故名思意,这些模式都是用来创建实例对象的。

1. 工厂模式

我们从简单的开始。 简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。

上图为例,我们构造一个简单的汽车工厂来生产汽车:

// 汽车构造函数
function SuzukiCar(color) {
  this.color = color;
  this.brand = 'Suzuki';
}


// 汽车构造函数
function HondaCar(color) {
  this.color = color;
  this.brand = 'Honda';
}


// 汽车构造函数
function BMWCar(color) {
  this.color = color;
  this.brand = 'BMW';
}

// 汽车品牌枚举
const BRANDS = {
  suzuki: 1,
  honda: 2,
  bmw: 3
}

/**
 * 汽车工厂
 */
function CarFactory() {

  this.create = (brand, color)=> {
    switch (brand) {
      case BRANDS.suzuki:
        return new SuzukiCar(color);
      case BRANDS.honda:
        return new HondaCar(color);
      case BRANDS.bmw:
        return new BMWCar(color);
      default:
        break;
    }

  }

}

使用一下我们的工厂:

const carFactory = new CarFactory();

const cars = [];

cars.push(carFactory.create(BRANDS.suzuki, 'brown'));
cars.push(carFactory.create(BRANDS.honda, 'grey'));
cars.push(carFactory.create(BRANDS.bmw, 'red'));

function sayHello() {
  console.log(`Hello, I am a ${this.color} ${this.brand} car`);
}

for (const car of cars) {
  sayHello.call(car);
}

输出结果:

Hello, I am a brown Suzuki car

Hello, I am a grey Honda car

Hello, I am a red BMW car

使用工厂模式之后,不再需要重复引入一个个构造函数,只需要引入工厂对象就可以方便的创建各类对象。

2. 单例模式

首先我们需要理解什么是单例?

单:指的是一个。
例:指的是创建的实例。
单例:指的是创建的总是同一个实例。也就是使用类创建的实例始终是相同的。

单例模式的作用:防止一些全局实例被污染

先看下面的一段代码:

class Person{
  constructor(){}
}

let p1 = new Person();

let p2 = new Person();

console.log(p1===p2) //false

上面这段代码,定义了一个Person类,通过这个类创建了两个实例,我们可以看到最终这两个实例是不相等的。也就是说,通过同一个类得到的实例不是同一个(这本就是理所应当),但是如果我们想始终得到的是同一个实例,那么这就是单例模式。那么下面就该介绍如何实现单例模式了:

想要实现单例模式,我们需要注意两点:

  1. 需要使用return。使用new的时候如果没有手动设置return,那么会默认返回this。但是,我们这里要使得每次返回的实例相同,也就是需要手动控制创建的对象,因此这里需要使用return
  2. 我们需要每次return的是同一个对象。也就是说实际上在第一次实例的时候,需要把这个实例保存起来。再下一个实例的时候,直接return这个保存的实例。因此,这里需要用到闭包了
const Person = (function(){
  let instance = null;
  return class{
      constructor(){
        if(!instance){
         //第一次创建实例,那么需要把实例保存
          instance = this;
        }else{
          return instance;
      }
  }

  }

})()


let p3 = new Person();
let p4 = new Person();

console.log(p3===p4)  //true

从上面的代码中,我们可以看到在闭包中,使用instance变量来保存创建的实例,每次返回的都是第一次创建的实例。这样的话就实现了无论创建多少次,创建的都是同一个实例,这就是单例模式。

3. 原型模式

通俗点讲就是创建一个共享的原型,并通过拷贝这些原型创建新的对象。

在我看来,其实原型模式就是指定新创建对象的模型,更通俗一点来说就是我想要新创建的对象的原型是我指定的对象。

最简单的原型模式的实现就是通过Object.create()。Object.create(),会使用现有的对象来提供新创建的对象的__proto__。例如下方代码:

let person = {
  name:'hello',
  age:24
}

let anotherPerson = Object.create(person);
console.log(anotherPerson.__proto__)  //{name: "hello", age: 24}
anotherPerson.name = 'world';  //可以修改属性
anotherPerson.job = 'teacher';

另外,如果我们想要自己实现原型模式,而不是使用封装好的Object.create()函数,那么可以使用原型继承来实现

function F(){}

F.prototype.g = function(){}

//G类继承F类
function G(){
  F.call(this);
}


//原型继承
function Fn(){};
Fn.prototype = F.prototype;
G.prototype = new Fn();
G.prototype.constructor = G;

原型模式就是创建一个指定原型的对象。如果我们需要重复创建某个对象,那么就可以使用原型模式来实现。


结构型模式

1. 装饰器模式

装饰器模式:为对象添加新功能,不改变其原有的结构和功能。

适配器模式是原有的不能用了,要重新封装接口。装饰器模式是原有的还能用,但是需要新增一些东西来完善这个功能。

比如手机壳,手机本身的功能不受影响,手机壳就是手机的装饰器模式。

class Circle {
    draw() {
        console.log('画一个圆形');
    }
}

class Decorator {
    constructor(circle) {
        this.circle = circle;
    }

    draw() {
        this.circle.draw();
        this.setRedBorder(circle);
    }



    setRedBorder(circle) {
        console.log('设置红色边框')
    }

}


// 测试
let circle = new Circle();
let client = new Decorator(circle);


client.draw();

输出结果:

画一个圆形
设置红色边框

如今都2021了,es7也应用广泛,我们在es7中这么写(ES7装饰器):

1、安装 yarn add babel-plugin-transform-decorators-legacy

2、新建.babelrc文件,进行下面的配置

{
    "presets": ["es2015", "latest"],
    "plugins": ["transform-decorators-legacy"]
}

3、上代码

@testDec

class Demo {
    // ...
}

function testDec(target) {
    target.isDec = true
}

console.log(Demo.isDec)

//输出true

打印出来了true,说明@testDec这个装饰器已经成功了,函数是个装饰器,用@testDec给Demo装饰了一遍。这个target其实就是class Demo,然后给她加一个isDec。

拆解后就是下面的内容:

// 装饰器原理

@decorator
class A {}

// 等同于
class A {}
A = decorator(A) || A;

装饰器参数的形式

@testDec(false)

class Demo {
    
}


function testDec(isDec) {
    return function (target) {
        target.isDec = isDec
    }

}
console.log(Demo.isDec);

验证是否是一个真正的装饰器模式需要验证以下几点:

1.将现有对戏那个和装饰器进行分离,两者独立存在

2.符合开放封闭原则

2. 适配器模式

适配器模式:旧接口格式和使用者不兼容,中间加一个适配转换接口。

比如国外的插座跟国内的插座不一样,我们需要买个转换器去兼容。

上代码:

class Adaptee {

    specificRequest() {
        return '德国标准的插头';
    }
}



class Target {
    constructor() {
        this.adaptee = new Adaptee();
    }
    request() {
        let info = this.adaptee.specificRequest();
        return `${info} -> 转换器 -> 中国标准的插头`
    }
}
// 测试
let client = new Target();

client.request();

结果:

德国标准的插头 -> 转换器 -> 中国标准的插头

场景上可封装旧接口:

// 自己封装的ajax,使用方式如下:

ajax({

    url: '/getData',
    type: 'Post',
    dataType: 'json',
    data: {
        id: '123'
    }
}).done(function(){

})

// 但因为历史原因,代码中全都是:
// $.ajax({...})

这个时候需要一个适配器

// 做一层适配器
var $ = {
    ajax: function (options) {
        return ajax(options)
    }
}

3. 代理模式

代理模式:使用者无权访问目标对象,中间加代理,通过代理做授权和控制

明星经纪人:比如有个演出,要请明星,要先联系经纪人。

或者理解为:为一个对象提供一个代用品或者占位符,以便控制对它的访问。例如图片懒加载、中介等。

/**
 * pre:代理模式

 * 小明追求A,B是A的好朋友,小明比不知道A什么时候心情好,不好意思直接将花交给A,

 * 于是小明将花交给B,再由B交给A.
 */


// 花的类 
class Flower{
    constructor(name){
        this.name = name 
    }
}


// 小明拥有sendFlower的方法
let Xioaming = {
    sendFlower(target){
        var flower = new Flower("玫瑰花")
        target.receive(flower)
    }
}


// B对象中拥有接受花的方法,同时接收到花之后,监听A的心情,并且传入A心情好的时候函数
let B = {
    receive(flower){
        this.flower =flower
        A.listenMood(()=>{
            A.receive(this.flower)
        })
    }
}

// A接收到花之后输出花的名字
let A = {
    receive(flower){
        console.log(`A收到了${flower.name} `)
        // A收到了玫瑰花 
    },

    listenMood(func){
        setTimeout(func,1000)
    }

}

Xioaming.sendFlower(B)

虚拟代理用于图片的预加载

图片很大,页面加载时会空白,体验不好,所以我们需要个占位符,来短暂替代这个图片,等图片加载好了放上去。

let myImage = (function(){

    let img = new Image
    document.body.appendChild(img)
    return {
        setSrc:(src)=>{
            img.src = src
        }
    }
})()

let imgProxy =(function(){
    let imgProxy = new Image
    // 这个地方我使用了setTimeout来增强演示效果,否则本地加载太快,根本看不到。
    imgProxy.onload=function(){
        setTimeout(()=>{
            myImage.setSrc(this.src)
        },2000)
    }
    return (src)=>{
        myImage.setSrc("../../img/bgimg.jpeg")
        imgProxy.src=src
    }
})()

imgProxy("../../img/background-cover.jpg")

ES6 Proxy

其实在ES6中,已经有了Proxy,这个内置的函数。我们来用一个例子来演示一下他的用法。这是一个明星代理的问题。

let star={
    name : "张XX",
    age:25,
    phone : "1300001111"
}

let agent = new Proxy(star,
    {
        get:function(target,key){
            if(key === "phone"){
                return  "18839552597"
            }else if(key === "name"){
                return "张XX"
            }else if(key === "price"){
                return "12W"
            }else if(key === "customPrice"){
                return target.customPrice
            }
        },

        set:function(target,key,value){
            if(key === "customPrice"){
                if(value < "10"){
                    console.log("太低了!!!")
                    return false
                }else{
                    target[key] = value
                    return true
                }

            }

        }

    }

)


console.log(agent.name)

console.log(agent.price)

console.log(agent.phone)

console.log(agent.age)

agent.customPrice = "12"

console.log(agent)

console.log(agent.customPrice)

设计原则验证

代理类和目标类分离,隔离开目标类和使用者

符合开放封闭原则


行为型模式

1. 策略模式

策略模式是一种简单却常用的设计模式,它的应用场景非常广泛。我们先了解下策略模式的概念,再通过代码示例来更清晰的认识它。

策略模式由两部分构成:一部分是封装不同策略的策略组,另一部分是 Context。通过组合和委托来让 Context 拥有执行策略的能力,从而实现可复用、可扩展和可维护,并且避免大量复制粘贴的工作。

策略模式的典型应用场景是表单校验中,对于校验规则的封装。接下来我们就通过一个简单的例子具体了解一下:

/**
 * 登录控制器
 */



function LoginController() {

  this.strategy = undefined;
  this.setStrategy = function (strategy) {
    this.strategy = strategy;
    this.login = this.strategy.login;
  }



}


/**

 * 用户名、密码登录策略
 */


function LocalStragegy() {

  this.login = ({ username, password }) => {
    console.log(username, password);
    // authenticating with username and password... 
  }

}


/**
 * 手机号、验证码登录策略
 */

function PhoneStragety() {
  this.login = ({ phone, verifyCode }) => {
    console.log(phone, verifyCode);
    // authenticating with hone and verifyCode... 

  }

}


/**
 * 第三方社交登录策略
 */

function SocialStragety() {
  this.login = ({ id, secret }) => {
    console.log(id, secret);
    // authenticating with id and secret... 
  }

}

const loginController = new LoginController();

// 调用用户名、密码登录接口,使用LocalStrategy
app.use('/login/local', function (req, res) {
  loginController.setStrategy(new LocalStragegy());
  loginController.login(req.body);

});

// 调用手机、验证码登录接口,使用PhoneStrategy
app.use('/login/phone', function (req, res) {

  loginController.setStrategy(new PhoneStragety());

  loginController.login(req.body);

});


// 调用社交登录接口,使用SocialStrategy

app.use('/login/social', function (req, res) {


  loginController.setStrategy(new SocialStragety());


  loginController.login(req.body);


});

从以上示例可以得出使用策略模式有以下优势:

  1. 方便在运行时切换算法和策略
  2. 代码更简洁,避免使用大量的条件判断
  3. 关注分离,每个strategy类控制自己的算法逻辑,strategy和其使用者之间也相互独立

2. 观察者模式

观察者模式又叫发布订阅模式(Publish/Subscribe)------有区别,它定义了一种一或一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。典型代表vue/react等。

使用观察者模式的好处:

  1. 支持简单的广播通信,自动通知所有已经订阅过的对象。
  2. 目标对象与观察者存在的是动态关联,增加了灵活性。
  3. 目标对象与观察者之间的抽象耦合关系能够单独扩展以及重用。

当然给元素绑定事件的addEventListener()也是一种:

target.addEventListener(type, listener [, options]);

Target就是被观察对象Subject,listener就是观察者Observer。

观察者模式中Subject对象一般需要实现以下API:

  • subscribe(): 接收一个观察者observer对象,使其订阅自己
  • unsubscribe(): 接收一个观察者observer对象,使其取消订阅自己
  • fire(): 触发事件,通知到所有观察者

用JavaScript手动实现观察者模式:

// 被观察者
function Subject() {
  this.observers = [];
}

Subject.prototype = {

  // 订阅
  subscribe: function (observer) {
    this.observers.push(observer);
  },

  // 取消订阅
  unsubscribe: function (observerToRemove) {
    this.observers = this.observers.filter(observer => {
      return observer !== observerToRemove;
    })
  },

  // 事件触发
  fire: function () {

    this.observers.forEach(observer => 

      observer.call();

    });
  }

}

验证一下订阅是否成功:

const subject = new Subject();
function observer1() {

  console.log('Observer 1 Firing!');

}

function observer2() {

  console.log('Observer 2 Firing!');

}
subject.subscribe(observer1);

subject.subscribe(observer2);

subject.fire();

输出:

Observer 1 Firing! 
Observer 2 Firing!

验证一下取消订阅是否成功:

subject.unsubscribe(observer2);

subject.fire();

输出:

Observer 1 Firing!

3. 迭代器模式

ES6中的迭代器 Iterator 相信大家都不陌生,迭代器用于遍历容器(集合)并访问容器中的元素,而且无论容器的数据结构是什么(Array、Set、Map等),迭代器的接口都应该是一样的,都需要遵循 迭代器协议。

迭代器模式解决了以下问题:

  1. 提供一致的遍历各种数据结构的方式,而不用了解数据的内部结构
  2. 提供遍历容器(集合)的能力而无需改变容器的接口

一个迭代器通常需要实现以下接口:

  • hasNext():判断迭代是否结束,返回Boolean
  • next():查找并返回下一个元素

为Javascript的数组实现一个迭代器可以这么写:

const item = [1, 'red', false, 3.14];

function Iterator(items) {

  this.items = items;

  this.index = 0;
}

Iterator.prototype = {
  hasNext: function () {
    return this.index < this.items.length;

  },

  next: function () {

    return this.items[this.index++];

  }

}

验证一下迭代器:

const iterator = new Iterator(item);

while(iterator.hasNext()){

  console.log(iterator.next());
}

输出:

1, red, false, 3.14

ES6提供了更简单的迭代循环语法 for...of,使用该语法的前提是操作对象需要实现 可迭代协议(The iterable protocol),简单说就是该对象有个Key为 Symbol.iterator 的方法,该方法返回一个iterator对象。

比如我们实现一个 Range 类用于在某个数字区间进行迭代:

function Range(start, end) {
  return {
    [Symbol.iterator]: function () {

      return {

        next() {

          if (start < end) {

            return { value: start++, done: false };

          }

          return { done: true, value: end };

        }

      }

    }

  }


}

验证:

for (num of Range(1, 5)) {

  console.log(num);
}

结果:

1, 2, 3, 4

4. 状态模式

状态模式:一个对象有状态变化,每次状态变化都会触发一个逻辑,不能总是用if...else来控制。

比如红绿灯:

// 状态(红灯,绿灯 黄灯)

class State {

    constructor(color) {
        this.color = color;
    }

    // 设置状态
    handle(context) {
        console.log(`turn to ${this.color} light`);
        context.setState(this)
    }
}


// 主体
class Context {
    constructor() {
        this.state = null;
    }



    // 获取状态
    getState() {
        return this.state;
    }



    setState(state) {
        this.state = state;
    }

}


// 测试

let context = new Context();

let green = new State('green');

let yellow = new State('yellow');

let red = new State('red');

// 绿灯亮了

green.handle(context);
console.log(context.getState())


// 黄灯亮了
yellow.handle(context);
console.log(context.getState())


// 红灯亮了
red.handle(context);
console.log(context.getState())

设计原则验证

将状态对象和主体对象分离,状态的变化逻辑单独处理

符合开放封闭原则

性能优化(7点)

前端总结--性能优化 - chenjing_amy - 博客园 (cnblogs.com)

首屏加载如何优化?

在vue中,可以通过路由配个路由占位符来完成单页面应用的实现,其原理时通过对路由占位符的更新来完成单页面应用的实现。单页面应用的优点在于页面的切换不会导致整个页面的刷新,而是对路由占位符的更新,比起传统的,单页面应用切换页面速度更快、用户体验更好,代码的样式及标准更好控制,程序员的工作量更少。缺点在于单页面的首屏加载速度较慢,SEO不友好。

1、缩小项目体积:

原理:体积越小,加载越快。

方法:

  • 通过webpack对项目体积进行压缩,开启gzip压缩文件
  • 通过对css3、js文件的合并,如在两不同组件中,拥有相同的样式,可通过全局css文件中设置。在js文件上,将相同的方法封装合并成一个方法,如API请求。
  • 减小图片体积,图标可通过矢量图来代替。

2、减少请求次数/体积:

原理:请求越少,加载越快

方法:

  • 通过精灵图来减少小图标的总请求数

  • 在图片数据多时,页面高度大于浏览器高度,通过图片懒加载,对未可见的图片进行延迟加载。

    • 原生方法: 先将img标签中的src的路径设置为同一张图片(空白图片),将真正的图片路径保存在data-src属性中,通过scrollTop方法获取浏览器窗口顶部与文档顶部之间的距离,通过clientHeight方法获取可视区高度。对window.scoll触发时,执行事件载入data-src(对每个images标签DOM来求其offsetTop,如果images距离顶部的高度 >=可视区高度+窗口距离文档顶部的距离 )。
    • vue方法: 通过安装vue-lazyload依赖->全局引入vue-lazyload依赖->配置依赖
  • 将大文件上传到CDN,通过CDN加载依赖,CDN的全称是Content Delivery Network,即内容分发网络。其通过将站点内容发布至遍布全球的海量加速节点,使其用户可就近获取所需内容,避免因网络拥堵、跨运营商、跨地域、跨境等因素带来的网络不稳定、访问延迟高等问题,有效提升下载速度、降低响应时间,提供流畅的用户体验。 因为CDN的这些特性,我们可以将体积较大的文件或是图片上传到CDN中,通过CDN来加载,减轻了服务器的请求压力,同时也可以通过CDN来获取、加载依赖。

  • 对绑定了请求的按钮或是别的触发因素,对其进行节流,设定时间周期(例如:按钮点击后,将其disable设置为false,禁用按钮,设定定时器,设定时间周期,解除禁用。),防止频繁触发,导致请求数激增,增大服务器压力。

3、减少加载模块:

原理:单页面应用的首屏加载较慢主要是因为单页面应用在首屏时,无论是否需要都会加载所有的模块,可通过按需加载、路由懒加载来优化。

方法:

  • 按需加载,通过对路由文件的配置,来对相关模块划分区间,如登录界面可以和首页、主页面划分一块,在进入首屏时,只对首屏所在的区块进行加载。通过require.ensure()来将多个相同类的组件打包成一个文件。如示例代码,打包时,将两个组件打包成一个js文件,文件名为good。
{
     path: '/goodList',	//path路径	
     name: 'goodList',	//组件名
     component: r => require.ensure([], () => 	r(require('../components/goodList')), 'good')	//good类型的组件
},
{
     path: '/goodOrder',
     name: 'goodOrder',
     component: r => require.ensure([], () => r(require('../components/goodOrder')), 'good')//good类型的组件
}

  • 动态加载,通过import来实现路由的动态加载,这个时候对于路由的加载是动态的,用到再加载。
{
     path: '/goodList',	//path路径	
     name: 'goodList',	//组件名
     component: () => import('../components/goodList')
},
{
     path: '/goodOrder',
     name: 'goodOrder',
     component: () => import('../components/goodOrder'),
}

页面加载缓慢怎么解决

一、页面加载缓慢的原因

当我们打开一个网页,页面加载比较缓慢时,可能原因有以下几点:
(1)过多的http请求
(2)长时间占用js线程
(3)页面回流和重绘较多
(4)资源加载堵塞
(5)内存泄漏导致内存过大
(6)dom节点或事件占用内存过大
(7)长时间占用js线程
(8)资源加载阻塞

二、前端性能优化方法

优化原则:
(1)减少http请求(图片使用雪碧图、Base64、字体图标等,减少重定向、使用缓存,不使用css@import,避免使用空的src和href)
(2)资源压缩与合并(包括html压缩、css压缩、js的压缩和混乱、文件合并等)
(3)优化网络连接(使用CDN,DNS预解析,使用keep-alive或persistent建立持久连接)
(4)优化资源加载,代码拆分,按需加载,降低CSS对渲染的阻塞,尽早的加载CSS,降低加载的大小
(5)减少重绘回流
(6)webpack性能优化(打包公共代码,动态导入和按需加载,删除无用的代码,长缓存优化,公共代码内联)
(7)减少iframes使用
(8)避免table布局
(9)css、js尽量使用外链
(10)实例化后避免添加新的属性
(11)避免读取超过数组的长度
(12)避免元素类型转换

重排重绘优化

(1)缓存DOM计算属性
(2)使用类合并样式,避免逐条改变样式
(3)使用display控制DOM显隐,将DOM离线化
(4)使用 transform 和 opacity 属性更改来实现动画,在 CSS 中,transforms 和 opacity 这两个属性更改不会触发重排与重绘

css3 动画优化

1.使用3D变形来开启GPU加速
-webkit-transform: translate3d(0, 0, 0);
-moz-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
2、一个元素通过translate3d右移500px的动画流畅度会明显优于使用left属性

CSS动画属性会触发整个页面的重排relayout、重绘repaint、重组recomposite

Paint通常是其中最花费性能的,尽可能避免使用触发paint的CSS动画属性,

这也是为什么我们推荐在CSS动画中使用webkit-transform: translateX(3em)的方案代替使用left: 3em,

因为left会额外触发layout与paint,而webkit-transform只触发整个页面composite(这也是为什么推荐在CSS动画中使用webkit-transform: translateX(500px)的方案代替使用left: 500px)

#ball-1 {
  transition: -webkit-transform .5s ease;
  -webkit-transform: translate3d(0, 0, 0);
}
#ball-1.slidein {
  -webkit-transform: translate3d(500px, 0, 0);
}


#ball-2 {
  transition: left .5s ease;
  left: 0;
}
#ball-2.slidein {
  left: 500px;
}
div {
  -webkit-animation-duration: 5s;
  -webkit-animation-name: move;
  -webkit-animation-iteration-count: infinite;
  -webkit-animation-direction: alternate;
  width: 200px;
  height: 200px;
  margin: 100px;
  background-color: #808080;
  position: absolute;
}
/*第一种方案  用css属性left*/
@-webkit-keyframes move{
    from {
        left: 100px;
    }
    to {
        left: 200px;
    }
}
/*第二种方案  用css3动画属性translateX*/
@-webkit-keyframes move{
    from {
        -webkit-transform: translateX(100px);
    }
    to {
        -webkit-transform: translateX(200px);
    }
}
  • 尽可能少的使用box-shadows与gradients
  • 尽可能的让动画元素不在文档流中,以减少重排
position: fixed;
position: absolute;
  • 动画中尽量少使用能触发layout和paint的CSS属性,使用更低耗的transformopacity等属性

前端性能如何优化?---浓缩版

一.网络层面优化

1.webpack项目构建
先说明loader和plugin的区别:对于loader,它是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss转换为A.css,单纯的文件转换过程
plugin是一个扩展器,它丰富了webpack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务。

(1)常用的loader
配置include/exclude缩小Loader对文件的搜索范围,好处是避免不必要的转译
file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 (处理图片和字体)
url-loader: 与file-loader类似,区别是用户可以设置一个阈值,大于阈值会交给file-loader处理,小于阈值时返回文件base64 形式编码 (处理图片和字体)
image-loader:加载并且压缩图片文件
babel-loader:把 ES6 转换成 ES5
sass-loader:将SCSS/SASS代码转换成CSS
css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS
postcss-loader:扩展 CSS 语法,使用下一代 CSS,可以配合 autoprefixer 插件自动补齐 CSS3 前缀
HappyPack:运行在 Node.之上的Webpack是单线程模型的,用HappyPack多进程解析和处理文件

(2)常用的plugin
html-webpack-plugin:简化 HTML 文件创建 (依赖于 html-loader)
uglifyjs-webpack-plugin:压缩js文件
MiniCssExtractPlugin:压缩css文件
HtmlWebpackPlugin:压缩html文件
clean-webpack-plugin:目录清除
mini-css-extract-plugin:分离样式文件,CSS 提取为独立文件,支持按需加载 (替代extract-text-webpack-plugin)
@babel/plugin-syntax-dynamic-import:按需加载,减轻首屏渲染的负担
CommonsChunkPlugin:提取公共代码

2.图像处理

(1)图像选型:了解所有图像类型的特点及其何种应用场景最合适
(2)图像压缩:在部署到生产环境前使用工具或脚本对其压缩处理
(3)图片懒加载:在页面中,先不给图片设置路径,只有当图片出现在浏览器的可视区域时,才去加载真正的图片,这就是延迟加载。对于图片很多的网站来说,一次性加载全部图片,会对用户体验造成很大的影响,所以需要使用图片延迟加载。
(4)调整图片大小:例如,你有一个 1920 * 1080 大小的图片,用缩略图的方式展示给用户,并且当用户鼠标悬停在上面时才展示全图。如果用户从未真正将鼠标悬停在缩略图上,则浪费了下载图片的时间。
所以,我们可以用两张图片来实行优化。一开始,只加载缩略图,当用户悬停在图片上时,才加载大图。还有一种办法,即对大图进行延迟加载,在所有元素都加载完成后手动更改大图的 src 进行下载。
(5)降低图片质量:压缩方法有两种,一是通过 webpack 插件 image-webpack-loader,二是通过在线网站进行压缩

3.静态资源使用 CDN

内容分发网络(CDN)是一组分布在多个不同地理位置的 Web 服务器。我们都知道,当服务器离用户越远时,延迟越高。CDN 就是为了解决这一问题,在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间。

4.缓存处理

缓存策略通过设置HTTP报文实现,在形式上分为强缓存/强制缓存和协商缓存/对比缓存。为了方便对比,笔者将某些细节使用图例展示,相信你有更好的理解。

整个缓存策略机制很明了,先走强缓存,若命中失败才走协商缓存。若命中强缓存,直接使用强缓存;若未命中强缓存,发送请求到服务器检查是否命中协商缓存;若命中协商缓存,服务器返回304通知浏览器使用本地缓存,否则返回最新资源。

注意:有两种较常用的应用场景值得使用缓存策略一试,当然更多应用场景都可根据项目需求制定。
频繁变动资源:设置Cache-Control:no-cache,使浏览器每次都发送请求到服务器,配合Last-Modified/ETag验证资源是否有效
不常变化资源:设置Cache-Control:max-age=31536000,对文件名哈希处理,当代码修改后生成新的文件名,当HTML文件引入文件名发生改变才会下载最新文件

二.渲染层面优化

渲染层面的性能优化,无疑是如何让代码解析更好执行更快,这充满在整个项目流程的开发阶段里。因此在开发阶段需时刻注意以下涉及到的每一点,养成良好的开发习惯,性能优化也自然而然被使用上了。

1.css优化
避免出现超过三层的嵌套规则
避免为ID选择器添加多余选择器
避免使用标签选择器代替类选择器
避免使用通配选择器,只对目标节点声明规则
避免重复匹配重复定义,关注可继承属性

2.DOM处理
缓存DOM计算属性
避免过多DOM操作
使用DOMFragment缓存批量化DOM操作

3.阻塞策略

  • 脚本与DOM/其它脚本的依赖关系很强:对<script>设置defer
  • 脚本与DOM/其它脚本的依赖关系不强:对<script>设置async

4.回流和重绘
(1)缓存DOM计算属性
(2)使用类合并样式,避免逐条改变样式
(3)使用display控制DOM显隐,将DOM离线化
(4)使用 transform 和 opacity 属性更改来实现动画,在 CSS 中,transforms 和 opacity 这两个属性更改不会触发重排与重绘

5.异步更新
在异步任务中修改DOM时把其包装成微任务(要了解EventLoop事件循环机制)

git和webpack

git命令一大堆(合并两个commit,怎么和远程分支建立连接,merge和rebase的区别)

webpack配置项,打包路径怎么修改(这有点懵逼)

treeshaking了解吗?vue3的treeshaking

Tree shaking 是一种通过清除多余代码方式来优化项目打包体积的技术,专业术语叫 Dead code elimination

简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码

如果把代码打包比作制作蛋糕,传统的方式是把鸡蛋(带壳)全部丢进去搅拌,然后放入烤箱,最后把(没有用的)蛋壳全部挑选并剔除出去

而treeshaking则是一开始就把有用的蛋白蛋黄(import)放入搅拌,最后直接作出蛋糕

也就是说 ,tree shaking 其实是找出使用的代码

在Vue2中,无论我们使用什么功能,它们最终都会出现在生产代码中。主要原因是Vue实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到

import Vue from 'vue'

Vue.nextTick(() => {})

而Vue3源码引入tree shaking特性,将全局 API 进行分块。如果你不使用其某些功能,它们将不会包含在你的基础包中

import { nextTick, observable } from 'vue'

tree shaking是基于ES6模板语法(import与exports),主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量

Tree shaking无非就是做了两件事:

编译阶段利用ES6 Module判断哪些模块已经加载

判断那些模块和变量未被使用或者引用,进而删除对应代码

运用了vite,谈谈你 对vite的理解,

一.Webpack

Webpack的HMR

第一次冷启动慢的原因:

在之前的浏览器中没有模块化的设计,所以期望把所有源代码编译进一个 js 文件中提供给浏览器使用,所以在开发中当我们运行启动命令的时候,webpack 总是需要从入口文件去索引整个项目的文件,编译成一个或多个单独的 js 文件,即使采用了代码拆分,也需要一次生成所有路由下的编译后文件(这也是为什么代码拆分对开发模式性能没有帮助)。这也导致了服务启动时间随着项目复杂度而指数增长。

webpack-dev-server的热更新:

与本地服务器建立「socket」连接,注册 hash 和 ok 两个事件,发生文件修改时,给客户端推送 hash 事件。客户端根据 hash 事件中返回的参数来拉取更新后的文件。
HotModuleReplacementPlugin 会在文件修改后,生成两个文件,用于被客户端拉取使用。

原理

  • 先转译打包,然后启动 dev server
  • 热更新时,把改动过模块的相关依赖模块全部编译一次

在热更新上反映的就是部分代码变更, 整个模块的代码都需要重新编译,然后在推送更新。

二.Vite

1.Vite2的HMR

Vite 是基于 native ES module —— 浏览器厂商的不懈努力,现代浏览器基本已经全部支持了import/export 语法了。

在Vite中,启动服务器时,是不需要提交编译文件,而是在浏览器请求对应URL时,再提供文件,实施了真正的路由懒加载,这个比起Webpack就要节省了不少时间。

而在工程中不是所有的引用模块都是ES写法,可能是CommonJS 和 UMD 、AMD 等等,
这个时候Vite 会进行预构建,将其转换为ESM模块,以支持Vite。

并且将有许多内部模块的ESM依赖转换为单个模块,以提高后续页面加载性能。

对于JSX、或者TS 等需要编译的文件,Vite是用esbuild来进行编译的,不同与Webpack的整体编译,Vite是在浏览器请求时,才对文件进行编译,然后提供给浏览器。因为esbuild编译够快,这种每次页面加载都进行编译的其实是不会影响加速速度的。

2.esbuild

  1. 使用 Go 编写,并且编译成了机器码
  2. 大量使用并行算法
  3. esbuild 的所有内容都是从零编写的,避免了不要的数据转换
  4. 更有效利用内存

3.Vite原理:

  • 对于不会变动的第三方依赖,采用编译速度更快的go编写的esbuild预构建
  • 对于 js/jsx/css 等源码,转译为原生 ES Module(ESM)
  • 利用了现代浏览器支持 ESM,会自动向依赖的 Module 发出请求的特性
  • 直接启动 dev server (不需要打包),对请求的模块按需实时编译
  • 热更新时,仅让浏览器重新请求改动过的模块

热更新时webpack做了什么

总的来说,webpack的热更新就是,当我们对代码做修改并保存后,webpack会对修改的代码块进行重新打包,并将新的模块发送至浏览器端,浏览器用新的模块代替旧的模块,从而实现了在不刷新浏览器的前提下更新页面。相比起直接刷新页面的方案,HMR的优点是可以保存应用的状态。当然,随着项目体积的增长,热更新的速度也会随之下降。

热更新时vite做了什么

热更新主要与项目编写的源码有关。前面提到,对于源码,vite使用原生esm方式去处理,在浏览器请求源码文件时,对文件进行处理后返回转换后的源码。vite对于热更新的实现,大致可以分为三步:
a. 监听文件变动
b. 读取文件内容
c. 通知浏览器做相应的更新

vite热更新的实现原理

创建一个websocket服务端。vite执行createWebSocketServer函数,调用ws库创建ws服务端。
创建一个ws客户端来接收ws服务端的信息。vite首先会创建一个ws client文件,然后在处理入口文件index.html时,把对ws client文件的引入注入到index.html文件中。当浏览器访问index.html时,就会加载ws client文件并执行,创建一个客户端ws,从而接收ws服务端的信息。
服务端监听文件变化,发送websocket消息,通知客户端。
服务端调用handleHMRUpdate函数,该函数会根据此次修改文件的类型,通知客户端是要刷新还是重新加载文件。
一个小细节:vite对于node_modules的文件做了强缓存,而对我们编写的源码做了协商缓存。

总结:vite为什么比webpack快

构建速度快:Webpack 会先将代码打包,然后启动开发服务器,请求服务器时返回打包后的结果;而 Vite 是直接启动开发服务器,请求哪个模块再对该模块进行实时编译,省去了打包的过程。

热更新快:相比起webpack,vite会让浏览器帮忙做更多的事情。vite 采用立即编译当前修改文件的办法,当改动了一个模块后,仅需让浏览器重新请求该模块即可。同时 vite 还会使用缓存机制( http 缓存、 vite 内置缓存 ),加载更新后的文件内容。

新启动一个项目,需要你配置什么

TypeScript

1、 TypeScript 的主要特点是什么?为什么大型项目中要使用typescript?

  • 跨平台:TypeScript 编译器可以安装在任何操作系统上,包括 Windows、macOS 和 Linux。
  • ES6 特性:TypeScript 包含计划中的 ECMAScript 2015 (ES6) 的大部分特性,例如箭头函数。
  • 面向对象的语言:TypeScript 提供所有标准的 OOP 功能,如类、接口和模块。
  • 静态类型检查:TypeScript 使用静态类型并帮助在编译时进行类型检查。因此,你可以在编写代码时发现编译时错误,而无需运行脚本。
  • 可选的静态类型:如果你习惯了 JavaScript 的动态类型,TypeScript 还允许可选的静态类型。
  • DOM 操作:您可以使用 TypeScript 来操作 DOM 以添加或删除客户端网页元素。

2、使用 TypeScript 有什么好处?

  • TypeScript 更具表现力,这意味着它的语法混乱更少。
  • 由于高级调试器专注于在编译时之前捕获逻辑错误,因此调试很容易。
  • 静态类型使 TypeScript 比 JavaScript 的动态类型更易于阅读和结构化。
  • 由于通用的转译,它可以跨平台使用,在客户端和服务器端项目中。

为什么越来越多的企业选择使用TypeScript ?

随着 JavaScript 项目规模的扩大,它们变得难以维护,这有几个原因。

首先,JavaScript 从未设计用于构建大型应用程序,它最初的目的是为网页提供小型脚本功能

直到现在,它还没有提供用于构建大型项目的工具和结构,例如类、模块和接口。此外,JavaScript 是动态类型的。

TypeScript 添加了可选的静态类型和语言特性,例如类和模块

TypeScript 纯粹是一个编译时工具,编译后,我们将得到简单、普通的 JavaScript,TypeScript 设计目标是为开发大型应用而生的

3、TypeScript 的内置数据类型有哪些?

数字类型:用于表示数字类型的值。TypeScript 中的所有数字都存储为浮点值。

let identifier: number = value;

布尔类型:一个逻辑二进制开关,包含true或false

let identifier: string = " ";

Null 类型:Null 表示值未定义的变量。

let identifier: bool = Boolean value;

未定义类型:一个未定义的字面量,它是所有变量的起点。

let num: number = null;

void 类型:分配给没有返回值的方法的类型。

let unusable: void = undefined;

5、TypeScript 中的接口是什么?

接口为使用该接口的对象定义契约或结构。接口是用关键字定义的interface,它可以包含使用函数或箭头函数的属性和方法声明。

interface IEmployee {
    empCode: number;
    empName: string;
    getSalary: (number) => number; // arrow function
    getManagerName(number): string; 
}

6、TypeScript 中的模块是什么?

TypeScript 中的模块是相关变量、函数、类和接口的集合。你可以将模块视为包含执行任务所需的一切的容器。可以导入模块以轻松地在项目之间共享代码。

module module_name{
    class xyz{
        export sum(x, y){
        return x+y;
    }
}

8、TypeScript 中的类型断言是什么?

TypeScript 中的类型断言的工作方式类似于其他语言中的类型转换,但没有 C# 和 Java 等语言中可能的类型检查或数据重组。类型断言对运行时没有影响,仅由编译器使用。类型断言本质上是类型转换的软版本,它建议编译器将变量视为某种类型,但如果它处于不同的形式,则不会强制它进入该模型。

9、如何在 TypeScript 中创建变量?

你可以通过三种方式创建变量:var,let,和const。
var是严格范围变量的旧风格。你应该尽可能避免使用,var因为它会在较大的项目中导致问题。

var num:number = 1;

let是在 TypeScript 中声明变量的默认方式。与var相比,let减少了编译时错误的数量并提高了代码的可读性。

let num:number = 1;

const创建一个其值不能改变的常量变量。它使用相同的范围规则,let并有助于降低整体程序的复杂性。

const num:number = 100;

10、在TypeScript中如何从子类调用基类构造函数?

你可以使用该super()函数来调用基类的构造函数。

class Animal {

  name: string;
  constructor(theName: string) {
    this.name = theName;
  }

  move(distanceInMeters: number = 0) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }

}



class Snake extends Animal {

  constructor(name: string) {
    super(name);
  }

  move(distanceInMeters = 5) {
    console.log("Slithering...");
    super.move(distanceInMeters);
  }

}

11、解释如何使用 TypeScript mixin。

Mixin 本质上是在相反方向上工作的继承。Mixins 允许你通过组合以前类中更简单的部分类设置来构建新类。相反,类A继承类B来获得它的功能,类B从类A需要返回一个新类的附加功能。

12、TypeScript 中如何检查 null 和 undefined?

你可以使用 juggle-check,它检查 null 和 undefined,或者使用 strict-check,它返回true设置为null的值,并且不会评估true未定义的变量。

if (x == null) {  
    
}

var a: number;  
var b: number = null;  

function check(x, name) {  
    if (x == null) {  
        console.log(name + ' == null');  
    }  

    if (x === null) {  
        console.log(name + ' === null');  
    }  

    if (typeof x === 'undefined') {  
        console.log(name + ' is undefined');  
    }  

}  

check(a, 'a');  
check(b, 'b');

13、TypeScript 中的 getter/setter 是什么?你如何使用它们?

Getter 和 setter 是特殊类型的方法,可帮助你根据程序的需要委派对私有变量的不同级别的访问。Getters 允许你引用一个值但不能编辑它。Setter 允许你更改变量的值,但不能查看其当前值。这些对于实现封装是必不可少的。例如,新雇主可能能够了解get公司的员工人数,但无权set了解员工人数。

const fullNameMaxLength = 10;

class Employee {
    
  private _fullName: string = "";

  get fullName(): string {
    return this._fullName;
  }

  set fullName(newName: string) {
    if (newName && newName.length > fullNameMaxLength) {
      throw new Error("fullName has a max length of " + fullNameMaxLength);
    }
    this._fullName = newName;
  }
    
}

let employee = new Employee();
employee.fullName = "Bob Smith";

if (employee.fullName) {
  console.log(employee.fullName);
}

14、 如何允许模块外定义的类可以访问?

你可以使用export关键字打开模块以供在模块外使用。

module Admin {
  // use the export keyword in TypeScript to access the class outside
  export class Employee {
    constructor(name: string, email: string) { }
  }

  let alex = new Employee('alex', 'alex@gmail.com');
}

// The Admin variable will allow you to access the Employee class outside the module with the help of the export keyword in TypeScript
let nick = new Admin.Employee('nick', 'nick@yahoo.com'); 

15、如何使用 Typescript 将字符串转换为数字?

与 JavaScript 类似,你可以使用parseInt或parseFloat函数分别将字符串转换为整数或浮点数。你还可以使用一元运算符+将字符串转换为最合适的数字类型,“3”成为整数,3而“3.14”成为浮点数3.14。

var x = "32";
var y: number = +x;

16、什么是 .map 文件,为什么/如何使用它?

甲.map文件是源地图,显示原始打字稿代码是如何解释成可用的JavaScript代码。它们有助于简化调试,因为你可以捕获任何奇怪的编译器行为。调试工具还可以使用这些文件来允许你编辑底层的 TypeScript 而不是发出的 JavaScript 文件。

17、TypeScript 中的类是什么?你如何定义它们?

类表示一组相关对象的共享行为和属性。例如,我们的类可能是Student,其所有对象都具有该attendClass方法。另一方面,John是一个单独的 type 实例,Student可能有额外的独特行为,比如attendExtracurricular.你使用关键字声明类class:

class Student {    
    studCode: number;    
    studName: string;    
    constructor(code: number, name: string) {    
            this.studName = name;    
            this.studCode = code;    
    }

18、TypeScript 与 JavaScript 有什么关系?

TypeScript 是 JavaScript 的开源语法超集,可编译为 JavaScript。所有原始 JavaScript 库和语法仍然有效,但 TypeScript 增加了 JavaScript 中没有的额外语法选项和编译器功能。TypeScript 还可以与大多数与 JavaScript 相同的技术接口,例如 Angular 和 jQuery。

22、 TypeScript 中有哪些范围可用?这与JS相比如何?

  • 全局作用域:在任何类之外定义,可以在程序中的任何地方使用。
  • 函数/类范围:在函数或类中定义的变量可以在该范围内的任何地方使用。
  • 局部作用域/代码块:在局部作用域中定义的变量可以在该块中的任何地方使用。

23、TypeScript 中的箭头/lambda 函数是什么?

胖箭头函数是用于定义匿名函数的函数表达式的速记语法。它类似于其他语言中的 lambda 函数。箭头函数可让你跳过function关键字并编写更简洁的代码。

24、解释rest参数和声明rest参数的规则。

其余参数允许你将不同数量的参数(零个或多个)传递给函数。当你不确定函数将接收多少参数时,这很有用。其余符号之后的所有参数...都将存储在一个数组中。
例如:

function Greet(greeting: string, ...names: string[]) {

    return greeting + " " + names.join(", ") + "!";

}

Greet("Hello", "Steve", "Bill"); // returns "Hello Steve, Bill!"

Greet("Hello");// returns "Hello !"

rest 参数必须是参数定义的最后一个,并且每个函数只能有一个 rest 参数。

27、TypeScript中如何实现函数重载?

要在 TypeScript 中重载函数,只需创建两个名称相同但参数/返回类型不同的函数。两个函数必须接受相同数量的参数。这是 TypeScript 中多态性的重要组成部分。例如,你可以创建一个add函数,如果它们是数字,则将两个参数相加,如果它们是字符串,则将它们连接起来。

function add(a:string, b:string):string;
function add(a:number, b:number): number;
function add(a: any, b:any): any {
    return a + b;
}

add("Hello ", "Steve"); // returns "Hello Steve" 
add(10, 20); // returns 30

28、如何让接口的所有属性都可选?

你可以使用partial映射类型轻松地将所有属性设为可选。

29、什么时候应该使用关键字unknown?

unknown,如果你不知道预先期望哪种类型,但想稍后分配它,则应该使用该any关键字,并且该关键字将不起作用。

30、什么是装饰器,它们可以应用于什么?

装饰器是一种特殊的声明,它允许你通过使用@注释标记来一次性修改类或类成员。每个装饰器都必须引用一个将在运行时评估的函数。例如,装饰器@sealed将对应于sealed函数。任何标有 的@sealed都将用于评估sealed函数。

function sealed(target) {
  // do something with 'target' ...
}

它们可以附加到:

  • 类声明
  • 方法
  • 配件
  • 特性
  • 参数

React

1、说说对React的理解?有哪些特性?

是什么
React,用于构建用户界面的 JavaScript 库,提供了 UI 层面的解决方案,遵循组件设计模式、声明式编程范式和函数式编程概念,以使前端应用程序更高效,使用虚拟DOM来有效地操作DOM,遵循从高阶组件到低阶组件的单向数据流,帮助我们将界面成了各个独立的小块,每一个块就是组件,这些组件之间可以组合、嵌套,构成整体页面.

特性

  • JSX语法
  • 单向数据绑定
  • 虚拟DOM
  • 声明式编程
  • Component(组件化)

优势

  • 高效灵活
  • 声明式的设计,简单使用
  • 组件式开发,提高代码复用率
  • 单向响应的数据流会比双向绑定的更安全,速度更快

2、区分Real DOM和Virtual DOM

Real DOM

  • Real DOM,真实DOM, 意思为文档对象模型,是一个结构化文本的抽象,在页面渲染出的每一个结点都是一个真实DOM结构
  • 更新缓慢
  • 可以直接更新 HTML
  • 如果元素更新,则创建新DOM
  • DOM操作代价很高
  • 消耗的内存较多

Virtual DOM

  • Virtual Dom,本质上是以 JavaScript 对象形式存在的对 DOM 的描述。创建虚拟DOM目的就是为了更好将虚拟的节点渲染到页面视图中,虚拟DOM对象的节点与真实DOM的属性一一照应
  • 更新更快
  • 无法直接更新 HTML
  • 如果元素更新,则更新 JSX
  • DOM 操作非常简单
  • 很少的内存消耗

3、什么是JSX和它的特性?

JSX 是JavaScript XML的缩写,不是html或xml,基于ECMAScript的一种新特性,一种定义带属性树结构的语法;

特性:

  • 自定义组件名首字母大写
  • 嵌套;在render函数中return返回的只能包含一个顶层标签,否则也会报错。
  • 求值表达式;JSX基本语法规则,遇到HTML标签(以<开头),就用HTML规则解析;遇到代码块(以{开头),就用JS规则解析
  • 驼峰命名
  • class属性需要写成className
  • JSX允许直接在模板插入JS变量。如果这个变量是一个数组,则会展开这个数组的所有成员
  • 在JSX中插入用户输入是安全的,默认情况下ReactDOM会在渲染前,转义JSX中的任意值,渲染前,所有的值都被转化为字符串形式,这能预防XSS攻击。

4、类组件和函数组件之间有什么区别?

类组件:

  • 无论是使用函数或是类来声明一个组件,它决不能修改它自己的 props。
  • 所有 React 组件都必须是纯函数,并禁止修改其自身 props。
  • React是单项数据流,父组件改变了属性,那么子组件视图会更新。
  • 属性 props是外界传递过来的,状态 state是组件本身的,状态可以在组件中任意修改
  • 组件的属性和状态改变都会更新视图。

函数组件:

  • 函数组件接收一个单一的 props 对象并返回了一个React元素
  • 函数组件的性能比类组件的性能要高,因为类组件使用的时候要实例化,而函数组件直接执行函数取返回结果即可。为了提高性能,尽量使用函数组件。

5、了解 Virtual DOM 吗?解释一下它的工作原理。

Virtual DOM 是一个轻量级的 JavaScript 对象,它最初只是 real DOM 的副本。它是一个节点树,它将元素、它们的属性和内容作为对象及其属性。 React 的渲染函数从 React 组件中创建一个节点树。然后它响应数据模型中的变化来更新该树,该变化是由用户或系统完成的各种动作引起的。

Virtual DOM 工作过程有三个简单的步骤:

  1. 每当底层数据发生改变时,整个 UI 都将在 Virtual DOM 描述中重新渲染。
  2. 然后计算之前 DOM 表示与新表示的之间的差异。
  3. 完成计算后,将只用实际更改的内容更新 real DOM。

6、说说对 State 和 Props的理解,有什么区别?

State

  • 一个组件的显示形态可以由数据状态和外部参数所决定,而数据状态就是state,一般在 constructor 中初始化
  • 当需要修改里面的值的状态需要通过调用setState来改变,从而达到更新组件内部数据的作用,并且重新调用组件render方法
  • setState还可以接受第二个参数,它是一个函数,会在setState调用完成并且组件开始重新渲染时被调用,可以用来监听渲染是否完成

Props

  • React的核心思想就是组件化思想,页面会被切分成一些独立的、可复用的组件,组件从概念上看就是一个函数,可以接受一个参数作为输入值,这个参数就是props,所以可以把props理解为从外部传入组件内部的数据
  • react具有单向数据流的特性,所以他的主要作用是从父组件向子组件中传递数据
  • props除了可以传字符串,数字,还可以传递对象,数组甚至是回调函数
  • 在子组件中,props在内部不可变的,如果想要改变它看,只能通过外部组件传入新的props来重新渲染子组件,否则子组件的props和展示形式不会改变

相同点

  • 两者都是 JavaScript 对象
  • 两者都是用于保存信息
  • props 和 state 都能触发渲染更新

区别

  • props 是外部传递给组件的,而 state 是在组件内被组件自己管理的,一般在 constructor 中初始化
  • props 在组件内部是不可修改的,但 state 在组件内部可以进行修改
  • state 是多变的、可以修改

7、说说对React refs 的理解?应用场景?

是什么

React 中的 Refs提供了一种方式,允许我们访问 DOM节点或在 render方法中创建的 React元素。
本质为ReactDOM.render()返回的组件实例,如果是渲染组件则返回的是组件实例,如果渲染dom则返回的是具体的dom节点。

如何使用

  • 传入字符串,使用时通过 this.refs.传入的字符串的格式获取对应的元素
  • 传入对象,对象是通过 React.createRef() 方式创建出来,使用时获取到创建的对象中存在 current 属性就是对应的元素
  • 传入函数,该函数会在 DOM 被挂载时进行回调,这个函数会传入一个 元素对象,可以自己保存,使用时,直接拿到之前保存的元素对象即可
  • 传入hook,hook是通过 useRef() 方式创建,使用时通过生成hook对象的 current 属性就是对应的元素

应用场景

在某些情况下,我们会通过使用refs来更新组件,但这种方式并不推荐,过多使用refs,会使组件的实例或者是DOM结构暴露,违反组件封装的原则;

但下面的场景使用refs非常有用:

  • 对Dom元素的焦点控制、内容选择、控制
  • 对Dom元素的内容设置及媒体播放
  • 对Dom元素的操作和对组件实例的操作
  • 集成第三方 DOM 库

8、setState是同步还是异步

setState本身并不是异步,之所以会有一种异步方法的表现形式,归根结底还是因为react框架本身的性能机制所导致的。因为每次调用setState都会触发更新,异步操作是为了提高性能,将多个状态合并一起更新,减少re-render调用。

实现同步:

  • setState提供了一个回调函数供开发者使用,在回调函数中,我们可以实时的获取到更新之后的数据。

    state = {
        number:1
    };
    
    componentDidMount(){
        this.setState({number:3},()=>{
            console.log(this.state.number) // 3
        })
    }
    
  • 利用setTimeout

    state = {
        number:1
    };
    componentDidMount(){
        setTimeout(()=>{
            this.setState({number:3})
            console.log(this.state.number) //3
        },0)
    }
    
  • 还有在原生事件环境下

    state = {
        number:1
    };
    
    componentDidMount() {
        document.body.addEventListener(‘click’, this.changeVal, false);
    }
    
    changeVal = () => {
        this.setState({
        number: 3
    })
    console.log(this.state.number) //3
    }
    

9、super()和super(props)有什么区别?

在ES6中,通过extends关键字实现类的继承,super关键字实现调用父类,super代替的是父类的构建函数,使用super(xx)相当于调用sup.prototype.constructor.call(this.xx),如果在子类中不使用super关键字,则会引发报错
super()就是将父类中的this对象继承给子类的,没有super()子类就得不到this对象

在React中,类组件是基于es6的规范实现的,继承React.Component,因此如果用到constructor就必须写super()才初始化this,在调用super()的时候,我们一般都需要传入props作为参数,如果不传进去,React内部也会将其定义在组件实例中,所以无论有没有constructor,在render中this.props都是可以使用的,这是React自动附带的,但是也不建议使用super()代替super(props),因为在React会在类组件构造函数生成实例后再给this.props赋值,所以在不传递props在super的情况下,调用this.props为undefined,而传入props的则都能正常访问,确保了 this.props 在构造函数执行完毕之前已被赋值,更符合逻辑

总结

  • 在React中,类组件基于ES6,所以在constructor中必须使用super
  • 在调用super过程,无论是否传入props,React内部都会将porps赋值给组件实例porps属性中
  • 如果只调用了super(),那么this.props在super()和构造函数结束之间仍是undefined

10、说说对React事件机制的理解?

是什么
React基于浏览器的事件机制自身实现了一套事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等,在React中这套事件机制被称之为合成事件;

合成事件是 React模拟原生 DOM事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器

执行顺序

  • React 所有事件都挂载在 document 对象上
  • 当真实 DOM 元素触发事件,会冒泡到 document 对象后,再处理 React 事件
  • 所以会先执行原生事件,然后处理 React 事件
  • 最后真正执行 document 上挂载的事件

总结

  • React 上注册的事件最终会绑定在document这个 DOM 上,而不是 React 组件对应的 DOM(减少内存开销就是因为所有的事件都绑定在 document 上,其他节点没有绑定事件)
  • React 自身实现了一套事件冒泡机制,所以这也就是为什么我们 event.stopPropagation()无效的原因。
  • React 通过队列的形式,从触发的组件向父组件回溯,然后调用他们 JSX 中定义的 callback
  • React 有一套自己的合成事件 SyntheticEvent

了解更多详情请点击React事件机制的理解

11、React事件绑定的方式有哪些?区别?

  • render方法中使用bind

  • render方法中使用箭头函数

  • constructor中bind

  • 定义阶段使用箭头函数绑定

    class App extends React.Component {
        handleClick() {
            console.log('this > ', this);
        }
        render() {
    
            return (
    
    
                {/ *1.render方法中使用bind*/}
    
                test
    
                {/*2.render方法中使用箭头函数 */}
                <div onClick={e => this.handleClick(e)}>test
    
            )
    
        }
    
    }
    
    
    
    class App extends React.Component {
        constructor(props) {
            super(props);
            //3.constructor中bind
            this.handleClick = this.handleClick.bind(this);
        }
        
        handleClick() {
            console.log('this > ', this);
        }
        
        
        render() {
        
            return (
                test
            )
    
        }
    
    }
    
    
    
    class App extends React.Component {
    
        //4.定义阶段使用箭头函数绑定
        handleClick = () => {
            console.log('this > ', this);
        }
        
        render() {
            return (
            test
            )
        }
    
    }
    
    
    

区别

  • 编写方面:方式一、方式二写法简单,方式三的编写过于冗杂
  • 性能方面:方式一和方式二在每次组件render的时候都会生成新的方法实例,性能问题欠缺。若该函数作为属性值传给子组件的时候,都会导致额外的渲染。而方式三、方式四只会生成一个方法实例
  • 综合上述,方式四(箭头函数绑定)是最优的事件绑定方式

12、React组件生命周期有几个阶段

  1. 初始渲染阶段这是组件即将开始其生命之旅并进入 DOM 的阶段。

    getDefaultProps:获取实例的默认属性
    getInitialState:获取每个实例的初始化状态
    componentWillMount:组件即将被装载、渲染到页面上
    render:组件在这里生成虚拟的 DOM 节点
    componentDidMount:组件真正在被装载之后

  2. 更新阶段一旦组件被添加到 DOM,它只有在 prop 或状态发生变化时才可能更新和重新渲染。这些只发生在这个阶段。

    componentWillReceiveProps:组件将要接收到属性的时候调用
    shouldComponentUpdate:组件接受到新属性或者新状态的时候(可以返回 false,接收数据后不更新,阻止 render 调用,后面的函数不会被继续执行了)
    componentWillUpdate:组件即将更新不能修改属性和状态
    render:组件重新描绘
    componentDidUpdate:组件已经更新

  3. 卸载阶段这是组件生命周期的最后阶段,组件被销毁并从 DOM 中删除。

    componentWillUnmount:组件即将销毁

13、详细解释 React 组件的生命周期方法

  1. componentWillMount() – 在渲染之前执行,在客户端和服务器端都会执行。
  2. componentDidMount() – 仅在第一次渲染后在客户端执行。
  3. componentWillReceiveProps() – 当从父类接收到 props 并且在调用另一个渲染器之前调用。
  4. shouldComponentUpdate() – 根据特定条件返回 true 或 false。如果你希望更新组件,请返回true ,不想更新组件则返回 false就会阻止render渲染。默认情况下,它返回 true。
  5. componentWillUpdate() – 在 DOM 中进行渲染之前调用。
  6. componentDidUpdate() – 在渲染发生后立即调用。
  7. componentWillUnmount() – 从 DOM 卸载组件后调用。用于清理内存空间。

14、react在哪个生命周期做优化

shouldComponentUpdate,这个方法用来判断是否需要调用 render 方法重绘 dom。
因为 dom 的描绘非常消耗性能,如果我们能在这个方法中能够写出更优化的 dom diff 算法,可以极大的提高性能。

点击React学习笔记四——受控组件和非受控组件查看详解

15、受控组件和非受控组件的区别

受控组件是React控制的组件,input等表单输入框值不存在于 DOM 中,而是以我们的组件状态存在。每当我们想要更新值时,我们就像以前一样调用setState。

不受控制组件是您的表单数据由 DOM 处理,而不是React 组件,Refs 用于获取其当前值;

16、React组件事件代理的原理

和原生HTML定义事件的唯一区别就是JSX采用驼峰写法来描述事件名称,大括号中仍然是标准的JavaScript表达式,返回一个事件处理函数。在JSX中你不需要关心什么时机去移除事件绑定,因为React会在对应的真实DOM节点移除时就自动解除了事件绑定。

React并不会真正的绑定事件到每一个具体的元素上,而是采用事件代理的模式:在根节点document上为每种事件添加唯一的Listener,然后通过事件的target找到真实的触发元素。这样从触发元素到顶层节点之间的所有节点如果有绑定这个事件,React都会触发对应的事件处理函数。这就是所谓的React模拟事件系统。

17、为什么虚拟 dom 会提高性能

虚拟dom(virtual dom) 是 JS对象,是一个真实dom的JS对象;虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要的 dom 操作,从而提高性能。

用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异把 2 所记录的差异应用到步骤 1 所构建的真正的 DOM 树上,视图就更新了。

18、React中的key有什么作用?

跟Vue一样,React 也存在diff算法,而元素key属性的作用是用于判断元素是新创建的还是被移动的元素,从而减少不必要的Diff,因此key的值需要为每一个元素赋予一个确定的标识。

  • 如果列表数据渲染中,在数据后面插入一条数据,key作用并不大;前面的元素在diff算法中,前面的元素由于是完全相同的,并不会产生删除创建操作,在最后一个比较的时候,则需要插入到新的DOM树中。因此,在这种情况下,元素有无key属性意义并不大。
  • 如果列表数据渲染中,在前面插入数据时,当拥有key的时候,react根据key属性匹配原有树上的子元素以及最新树上的子元素,只需要将元素插入到最前面位置,当没有key的时候,所有的li标签都需要进行修改
  • 并不是拥有key值代表性能越高,如果说只是文本内容改变了,不写key反而性能和效率更高,主要是因为不写key是将所有的文本内容替换一下,节点不会发生变化,而写key则涉及到了节点的增和删,发现旧key不存在了,则将其删除,新key在之前没有,则插入,这就增加性能的开销

总结

良好使用key属性是性能优化的非常关键的一步,注意事项为:

  • key 应该是唯一的
  • key不要使用随机值(随机数在下一次 render 时,会重新生成一个数字)
  • 避免使用 index 作为 key

19、react的diff算法是怎么完成的

  • 把树形结构按照层级分解,只比较同级元素
  • 通过给列表结构的每个单元添加的唯一 key值进行区分同层次的子节点的比较。
  • React 只会匹配相同 class 的 component(这里面的 class 指的是组件的名字)
  • 合并操作,调用 component 的 setState 方法的时候, React 将其标记为 dirty. 到每一个事件循环结束, React 检查所有标记 dirty 的 component 重新绘制。
  • 选择性渲染。开发人员可以重写 shouldComponentUpdate 提高 diff 的性能。

20、react组件之间如何通信

  • 父子:父传子:props; 子传父:子调用父组件中的函数并传参;
  • 兄弟:利用redux实现和利用父组件
  • 所有关系都通用的方法:利用PubSub.js订阅

点击React学习笔记十二——组件之间的通信方式查看详细内容

21、什么是高阶组件?

高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。基本上,这是从React的组成性质派生的一种模式,我们称它们为“纯”组件, 因为它们可以接受任何动态提供的子组件,但它们不会修改或复制其输入组件的任何行为。

  • 高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧
  • 高阶组件的参数为一个组件返回一个新的组件
  • 组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件

点击对高阶组件的理解查看详解

22、说说对React Hooks的理解?解决了什么问题?

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性,因此,现在的函数组件也可以是有状态的组件,内部也可以维护自身的状态以及做一些逻辑方面的处理;

最常见的hooks有:useState、useEffect

hooks的出现,使函数组件的功能得到了扩充,拥有了类组件相似的功能;

点击React学习笔记十一——扩展知识点(setState / lazyLoad / Hook / Fragment / Context)查看其中的Hook知识点详解900-

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表