前端面试题题库

前端知识库

HTML+CSS

1. 什么是 DOCTYPE, 有何作用?

Doctype是HTML5的文档声明,通过它可以告诉浏览器,使用哪一个HTML版本标准解析文档。

在浏览器发展的过程中,HTML出现过很多版本,不同的版本之间格式书写上略有差异。

如果没有事先告诉浏览器,那么浏览器就不知道文档解析标准是什么?此时,大部分浏览器将开启最大兼容模式来解析网页,我们一般称为怪异模式,这不仅会降低解析效率,而且会在解析过程中产生一些难以预知的bug,所以文档声明是必须的。

2.html 语义化

HTML标签的语义化,简单来说,就是用正确的标签做正确的事情,给某块内容用上一个最恰当最合适的标签,使页面有良好的结构,页面元素有含义,无论是谁都能看懂这块内容是什么。

语义化的优点如下:

  • 在没有CSS样式情况下也能够让页面呈现出清晰的结构
  • 有利于SEO和搜索引擎建立良好的沟通,有助于爬虫抓取更多的有效信息,爬虫是依赖于标签来确定上下文和各个关键字的权重
  • 方便团队开发和维护,语义化更具可读性,遵循W3C标准的团队都遵循这个标准,可以减少差异化3.srchref 的区别

src和href都是HTML中特定元素的属性,都可以用来引入外部的资源。两者区别如下:

  • src:全称source,它通常用于img、video、audio、script元素,通过src指向请求外部资源的来源地址,指向的内容会嵌入到文档中当前标签所在位置,在请求src资源时,它会将资源下载并应用到文档内,比如说:js脚本、img图片、frame等元素。当浏览器解析到该元素时,会暂停其它资源下载,直到将该资源加载、编译、执行完毕。这也是为什么将js脚本放在底部而不是头部的原因。
  • href:全称hyper reference,意味着超链接,指向网络资源,当浏览器识别到它指向的⽂件时,就会并⾏下载资源,不会停⽌对当前⽂档的处理,通常用于a、link元素。

3.HTML5新增了哪些新特性?移除了哪些元素?

HTML5主要是关于图像、位置、存储、多任务等功能的增加:

  • 语义化标签,如:article、footer、header、nav等
  • 视频video、音频audio
  • 画布canvas
  • 表单控件,calemdar、date、time、email
  • 地理
  • 本地离线存储,localStorage长期存储数据,浏览器关闭后数据不丢失,sessionStorage的数据在浏览器关闭后自动删除
  • 拖拽释放

移除的元素:

  • 纯表现的元素:basefont、font、s、strike、tt、u、big、center
  • 对可选用性产生负面影响的元素:frame、frameset、noframes

4.CSS3 新增了那些东西?

CSS3 新增东西众多,这里列举出一些关键的新增内容:

  • 选择器
  • 盒子模型属性:border-radius、box-shadow、border-image
  • 背景:background-size、background-origin、background-clip
  • 文本效果:text-shadow、word-wrap
  • 颜色:新增 RGBA,HSLA 模式
  • 渐变:线性渐变、径向渐变
  • 字体:@font-face
  • 2D/3D转换:transform、transform-origin
  • 过渡与动画:transition、@keyframes、animation
  • 多列布局
  • 媒体查询

5.行内元素和块级元素的区别

区别行内元素块级元素
宽高无效有效
padding有效有效
margin水平方向有效有效
自动换行不可以可以
多个元素排列默认从左到右默认从上到下

6.行内元素和块级元素分别有哪些?有何区别?怎样转换?

常见的块级元素:p、div、form、ul、li、ol、table、h1、h2、h3、h4、h5、h6、dl、dt、dd

常见的行级元素:span、a、img、button、input、select

块级元素:

  • 总是在新行上开始,就是每个块级元素独占一行,默认从上到下排列
  • 宽度缺少时是它的容器的100%,除非设置一个宽度
  • 高度、行高以及外边距和内边距都是可以设置的
  • 块级元素可以容纳其它行级元素和块级元素

行内元素:

  • 和其它元素都会在一行显示
  • 高、行高以及外边距和内边距可以设置
  • 宽度就是文字或者图片的宽度,不能改变
  • 行级元素只能容纳文本或者其它行内元素

使用行内元素需要注意的是:

  • 行内元素设置宽度width无效
  • 行内元素设置height无效,但是可以通过line-height来设置
  • 设置margin只有左右有效,上下无效
  • 设置padding只有左右有效,上下无效

可以通过display属性对行内元素和块级元素进行切换(主要看第 2、3、4三个值):

img

7.title* 与 h1 的区别、bstrong 的区别、iem 的区别?

  • title 属性表示网页的标题,h1 元素则表示层次明确的页面内容标题,对页面信息的抓取也有很大的影响
  • strong 是标明重点内容,有语气加强的含义,使用阅读设备阅读网络时:strong会重读,而b是展示强调内容
  • iitalic(斜体)的简写,是早期的斜体元素,表示内容展示为斜体,而 ememphasize(强调)的简写,表示强调的文本

8. img上 title 与 alt

  • alt:全称alternate,切换的意思,如果无法显示图像,浏览器将显示alt指定的内容
  • title:当鼠标移动到元素上时显示title的内容

区别:

一般当鼠标滑动到元素身上的时候显示title,而alt是img标签特有的属性,是图片内容的等价描述,用于图片无法加载时显示,这样用户还能看到关于丢失了什么东西的一些信息,相对来说比较友好。

9.label的作用是什么?是怎么用的?

label元素不会向用户呈现任何特殊效果,但是,它为鼠标用户改进了可用性,当我们在label元素内点击文本时就会触发此控件。也就是说,当用户选择该标签时,浏览器就会自动将焦点转到和标签相关的表单控件上。最常用label的地方就是表单中的性别单选框了,当点击文字时也能够自动聚焦绑定的表单控件。

<form>
     <label for="male"></label>
     <input type="radio" name="sex" id="male">
     <label for="female"></label>
     <input type="radio" name="sex" id="female">
</form>

10.对于Web标准以及W3C的理解

Web标准简单来说可以分为结构、表现、行为。其中结构是由HTML各种标签组成,简单来说就是body里面写入标签是为了页面的结构。表现指的是CSS层叠样式表,通过CSS可以让我们的页面结构标签更具美感。行为指的是页面和用户具有一定的交互,这部分主要由JS组成

W3C,全称:world wide web consortium是一个制定各种标准的非盈利性组织,也叫万维网联盟,标准包括HTML、CSS、ECMAScript等等,web标准的制定有很多好处,比如说:

  • 可以统一开发流程,统一使用标准化开发工具(VSCode、WebStorm、Sublime),方便多人协作
  • 学习成本降低,只需要学习标准就行,否则就要学习各个浏览器厂商标准
  • 跨平台,方便迁移都不同设备
  • 降低代码维护成本

11.怎么处理HTML5新标签兼容问题?

主要有两种方式:

  1. 实现标签被识别:通过document.createElement(tagName)方法可以让浏览器识别新的标签,浏览器支持新标签后。还可以为新标签添加CSS样式
  2. 用JavaScript解决:使用HTML5的shim框架,在head标签中调用以下代码:
<!--[if lt IE 9]>
    <script> src="http://html5shim.googlecode.com/svn/trunk/html5.js"</script>
<![endif]-->

12.如何实现在一张图片上的某个区域做到点击事件

我们可以通过图片热区技术:

  1. 插入一张图片,并设置好图像的有关参数,在``标记中设置参数usemap="#Map",以表示对图像地图的引用。
  2. 用``标记设定图像地图的作用区域,并取名:Map;
  3. 分别用``标记针对相应位置互粉出多个矩形作用区域,并设定好链接参数href

例:

<body>
 <img src="./image.jpg" alt="" usemap="#Map" />
   <map name="Map" id="Map">
     <area alt="" title="" href="#" shape="poly"
         coords="65,71,98,58,114,90,108,112,79,130,56,116,38,100,41,76,52,53,83,34,110,33,139,46,141,75,145,101,127,115,113,133,85,132,82,131,159,117" />
     <area alt="" title="" href="#" shape="poly" coords="28,22,57,20,36,39,27,61" />
 </map>
</body>

13.a元素除了用于导航外,还有什么作用?

href属性中的url可以是浏览器支持的任何协议,所以a标签可以用来手机拨号[110](tel:110),也可以用来发送短信[110](sms:110),还有邮件等等

当然,a元素最常见的就是用来做锚点下载文件

锚点可以在点击时快速定位到一个页面的某个位置,而下载的原理在于a标签所对应的资源浏览器无法解析,于是浏览器会选择将其下载下来。

14.px 和 em 的区别

px全称pixel像素,是相对于屏幕分辨率而言的,它是一个绝对单位,但同时具有一定的相对性。因为在同一个设备上每个像素代表的物理长度是固定不变的,这点表现的是绝对性。但是在不同的设备之间每个设备像素所代表的物理长度是可以变化的,这点表现的是相对性

em是一个相对长度单位,具体的大小需要相对于父元素计算,比如父元素的字体大小为80px,那么子元素1em就表示大小和父元素一样为80px,0.5em就表示字体大小是父元素的一半为40px

15. vw、vh 是什么?

vwvhCSS3 新单位,即 view width 可视窗口宽度 和 view height 可视窗口高度。1vw 就等于可视窗口宽度的百分之一,1vh 就等于可视窗口高度的百分之一。

16.块格式化上下文(Block Formatting Context,BFC)

BFC: 块格式化上下文(Block Formatting Context,BFC)是Web页面的可视化CSS渲染的一部分,是布局过程中生成块级盒子的区域,也是浮动元素与其他元素的交互限定区域。

通俗的讲,BFC是一个独立的环境布局,可以理解为一个容器,在这个容器中按照一定的规则进行物品摆放,并且不会影响其他环境中的物品。如果一个元素符合触发BFC的条件,则BFC中的元素布局不受外部影响。

BFC的创建条件
  • 根元素:body
  • 元素设置浮动:float除none以外的值
  • 元素设置绝对定位:position设置为absolute或fixed
  • display设置为inline-block、table-cell、table-caption、flex等
  • overflow设置为hidden、auto、scroll
BFC的特点
  • 垂直方向上,自上而下排列,和文档流的排列方式一致
  • 在BFC中上下相邻的的两个容器的margin会重叠
  • 计算BFC的高度时,需要计算浮动元素的高度
  • BFC区域不会与浮动的容器发生重叠
  • BFC是独立的容器,容器内部元素不会影响外部元素
  • 每个元素的margin-left值和容器的border-left相接处
BFC的作用
  • 解决margin的重叠问题:由于BFC是一个独立的区域,内部的元素和外部的元素互不影响,将两个元素变为两个BFC,就解决了margin重叠的问题
  • 解决高度塌陷的问题:对子元素设置浮动后,父元素会发生高度塌陷,即height变为0。只需将父元素变成一个BFC即可,常用的办法是给父元素设置overflow:hidden
  • 创建自适应两栏布局:左边的宽度固定,右边的宽度自适应。左侧设置float:left,右侧设置overflow: hidden。这样右边就触发了BFC,BFC的区域不会与浮动元素发生重叠,所以两侧就不会发生重叠,实现了自适应两栏布局

17.BFC 及其应用

所谓 BFC,指的是一个独立的布局环境,BFC 内部的元素布局与外部互不影响。

触发 BFC 的方式有很多,常见的有:

  • 设置浮动
  • overflow 设置为 auto、scroll、hidden
  • positon 设置为 absolute、fixed

常见的 BFC 应用有:

  • 解决浮动元素令父元素高度坍塌的问题
  • 解决非浮动元素被浮动元素覆盖问题
  • 解决外边距垂直方向重合的问题

18.BFC、IFC、GFCFFC

BFC:块级格式上下文,指的是一个独立的布局环境,BFC 内部的元素布局与外部互不影响。

IFC:行内格式化上下文,将一块区域以行内元素的形式来格式化。

GFC:网格布局格式化上下文,将一块区域以 grid 网格的形式来格式化。

FFC:弹性格式化上下文,将一块区域以弹性盒的形式来格式化。

19.display属性值及其作用

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

20.block、inline和inline-block的区别

区别blockinlineinline-block
独占一行
width
height
margin水平方向有效
padding

21.flex 布局如何使用?

flex 是 Flexible Box 的缩写,意为"弹性布局"。指定容器display: flex即可。

容器有以下属性:flex-direction,flex-wrap,flex-flow,justify-content,align-items,align-content。

  • flex-direction属性决定主轴的方向;
  • flex-wrap属性定义,如果一条轴线排不下,如何换行;
  • flex-flow属性是flex-direction属性和flex-wrap属性的简写形式,默认值为row nowrap;
  • justify-content属性定义了项目在主轴上的对齐方式。
  • align-items属性定义项目在交叉轴上如何对齐。
  • align-content属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。

项目(子元素)也有一些属性:order,flex-grow,flex-shrink,flex-basis,flex,align-self。

  • order属性定义项目的排列顺序。数值越小,排列越靠前,默认为0。
  • flex-grow属性定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。
  • flex-shrink属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。
  • flex-basis属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。
  • flex属性是flex-grow, flex-shrink 和 flex-basis的简写,默认值为0 1 auto。后两个属性可选。
  • align-self 属性允许单个项目有与其他项目不一样的对齐方式,可覆盖 align-items 属性。默认值为 auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch。

22.隐藏元素的方法

方法说明
display: none;渲染树不会包含该渲染对象,因此该元素不会在页面中占据位置,也不会响应绑定的监听事件
visibility: hidden;元素在页面中仍占据空间,但是不会响应绑定的监听事件
opacity: 0;透明度设置为0,来隐藏元素。元素在页面中仍然占据空间,并且能够响应元素绑定的监听事件
position: absolute;通过使用绝对定位将元素移除可视区域内,以此来实现元素的隐藏
z-index: -10;使用其余元素遮盖当前元素实现隐藏
clip/clip-path使用元素裁剪的方法来实现元素的隐藏,这种方法下,元素仍在页面中占据位置,但是不会响应绑定的监听事件
transform: scale(0,0)将元素缩放为 0,来实现元素的隐藏。这种方法下,元素仍在页面中占据位置,但是不会响应绑定的监听事件

23.transition和animation的区别

transitionanimation
过渡属性,强调过渡,需要触发事件来实现过渡效果。动画属性,不需要触发事件,可自己执行,并且可以循环

24.分析比较 opacity: 0、visibility: hidden、display: none 优劣和适用场景

结构: display:none: 会让元素完全从渲染树中消失,渲染的时候不占据任何空间, 不能点击, visibility: hidden:不会让元素从渲染树消失,渲染元素继续占据空间,只是内容不可见,不能点击 opacity: 0: 不会让元素从渲染树消失,渲染元素继续占据空间,只是内容不可见,可以点击

继承: display: none和opacity: 0:是非继承属性,子孙节点消失由于元素从渲染树消失造成,通过修改子孙节点属性无法显示。 visibility: hidden:是继承属性,子孙节点消失由于继承了hidden,通过设置visibility: visible;可以让子孙节点显式。

性能: displaynone : 修改元素会造成文档回流,读屏器不会读取display: none元素内容,性能消耗较大 visibility:hidden: 修改元素只会造成本元素的重绘,性能消耗较少读屏器读取visibility: hidden元素内容 opacity: 0 : 修改元素会造成重绘,性能消耗较少

display: none (不占空间,不能点击)(场景,显示出原来这里不存在的结构)
visibility: hidden(占据空间,不能点击)(场景:显示不会导致页面结构发生变动,不会撑开)
opacity: 0(占据空间,可以点击)(场景:可以跟transition搭配)

25.如何用 cssjs 实现多行文本溢出省略效果,考虑兼容性

CSS 实现方式

单行:

overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;

多行:

display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3; //行数
overflow: hidden;

兼容:

p{position: relative; line-height: 20px; max-height: 40px;overflow: hidden;}
p::after{content: "..."; position: absolute; bottom: 0; right: 0; padding-left: 40px;
background: -webkit-linear-gradient(left, transparent, #fff 55%);
background: -o-linear-gradient(right, transparent, #fff 55%);
background: -moz-linear-gradient(right, transparent, #fff 55%);
background: linear-gradient(to right, transparent, #fff 55%);
}

JS 实现方式:

  • 使用split + 正则表达式将单词与单个文字切割出来存入words
  • 加上 ‘…’
  • 判断scrollHeight与clientHeight,超出的话就从words中pop一个出来

26.说出 space-betweenspace-around 的区别?

这个是 flex 布局的内容,其实就是一个边距的区别,按水平布局来说,space-between是两端对齐,在左右两侧没有边距,而space-around是每个 子项目左右方向的 margin 相等,所以两个item中间的间距会比较大。

27.CSS3transitionanimation 的属性分别有哪些

transition 过渡动画:

  • transition-property:指定过渡的 CSS 属性
  • transition-duration:指定过渡所需的完成时间
  • transition-timing-function:指定过渡函数
  • transition-delay:指定过渡的延迟时间

animation 关键帧动画:

  • animation-name:指定要绑定到选择器的关键帧的名称
  • animation-duration:动画指定需要多少秒或毫秒完成
  • animation-timing-function:设置动画将如何完成一个周期
  • animation-delay:设置动画在启动前的延迟间隔
  • animation-iteration-count:定义动画的播放次数
  • animation-direction:指定是否应该轮流反向播放动画
  • animation-fill-mode:规定当动画不播放时(当动画完成时,或当动画有一个延迟未开始播放时),要应用到元素的样式
  • animation-play-state:指定动画是否正在运行或已暂停

28.如何用 CSS 实现一个三角形

可以利用 border 属性

利用盒模型的 border 属性上下左右边框交界处会呈现出平滑的斜线这个特点,通过设置不同的上下左右边框宽度或者颜色即可得到三角形或者梯形。

如果想实现其中的任一个三角形,把其他方向上的 border-color 都设置成透明即可。

示例代码如下:

<div></div>
div{
width: 0;
height: 0;
border: 10px solid red;
border-top-color: transparent;
border-left-color: transparent;
border-right-color: transparent;
}

29.如何实现一个自适应的正方形

方法1:利用 CSS3 的 vw 单位

vw 会把视口的宽度平均分为 100 份

.square {
 width: 10vw;
 height: 10vw;
 background: red;
}

方法2:利用 margin 或者 padding 的百分比计算是参照父元素的 width 属性

.square {
 width: 10%;
 padding-bottom: 10%; 
 height: 0; // 防止内容撑开多余的高度
 background: red;
}

30.浮动

非IE浏览器下,容器不设置高度且子元素浮动时,容器高度不能被撑开。此时,内容会溢出到容器外面影响布局

浮动的工作原理
  • 浮动元素脱离文档流,不占据空间(引起“高度塌陷”)
  • 浮动元素碰到包含它的边框或者其它浮动元素的边框停留

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

浮动元素引起的问题
  • 父元素的高度无法撑开,影响与父元素同级的元素
  • 与浮动元素同级的非浮动元素会跟随其后
  • 若浮动的元素不是第一个元素,则该元素之前的元素元素也要浮动,否则会影响页面的显示结构
清除浮动的方式
  • 给父级元素设置高度
  • 最后一个浮动元素之后添加一个空div标签,并添加clear: both样式
  • 包含浮动元素的父级元素添加overflow: hiddenoverflow: auto样式
  • 使用 ::after 伪元素
  • 使用clear属性清除浮动

31.清除浮动的方法

clear 清除浮动(添加空div法)在浮动元素下方添加空div,并给该元素写css样式: {clear:both;height:0;overflow:hidden;}

给浮动元素父级设置高度

父级同时浮动(需要给父级同级元素添加浮动)

父级设置成inline-block,其margin: 0 auto居中方式失效

给父级添加overflow:hidden 清除浮动方法

万能清除法 ::after 伪元素清浮动(现在主流方法,推荐使用)

32.盒模型

盒模型由四个部分组成,分别是margin、border、padding、content

标准盒模型和IE盒模型的区别在于:在设置width和height时,所对应的范围不同

  • 标准盒模型的width和height属性的范围只包含了content
  • IE盒模型的width和height属性的范围包含了border、padding和content

可以通过修改元素的box-sizing属性来改变元素的盒模型:

  • box-sizing: content-box 表示标准盒模型(默认值)
  • box-sizing: border-box 表示IE盒模型(怪异盒模型)

33.说说两种盒模型以及区别

盒模型也称为框模型,就是从盒子顶部俯视所得的一张平面图,用于描述元素所占用的空间。它有两种盒模型,W3C盒模型和IE盒模型(IE6以下,不包括IE6以及怪异模式下的IE5.5+)

理论上两者的主要区别是二者的盒子宽高是否包括元素的边框和内边距。当用CSS给给某个元素定义高或宽时,IE盒模型中内容的宽或高将会包含内边距和边框,而W3C盒模型并不会。

34.如何触发重排和重绘?

任何改变用来构建渲染树的信息都会导致一次重排或重绘:

  • 添加、删除、更新DOM节点
  • 通过display: none隐藏一个DOM节点-触发重排和重绘
  • 通过visibility: hidden隐藏一个DOM节点-只触发重绘,因为没有几何变化
  • 移动或者给页面中的DOM节点添加动画
  • 添加一个样式表,调整样式属性
  • 用户行为,例如调整窗口大小,改变字号,或者滚动。
如何减少重排?
  • 使用 transform 代替 top
  • 不要把节点的属性值放在一个循环里,当成循环里的变量
  • 不要使用 table 布局,可能很小的一个改动会造成整个 table 的重新布局
  • 把 DOM 离线后修改。如:使用 documentFragment 对象在内存里操作 DOM
  • 不要一条一条的修改样式,可以预先定义好 class,然后修改 DOM 的 className
  • 使用 absolute 或 fixed 使元素脱离文档流

35.重绘与重排的区别?

重排: 部分渲染树(或者整个渲染树)需要重新分析并且节点尺寸需要重新计算,表现为重新生成布局,重新排列元素

重绘: 由于节点的几何属性发生改变或者由于样式发生改变,例如改变元素背景色时,屏幕上的部分内容需要更新,表现为某些元素的外观被改变

单单改变元素的外观,肯定不会引起网页重新生成布局,但当浏览器完成重排之后,将会重新绘制受到此次重排影响的部分

重排和重绘代价是高昂的,它们会破坏用户体验,并且让UI展示非常迟缓,而相比之下重排的性能影响更大,在两者无法避免的情况下,一般我们宁可选择代价更小的重绘。

『重绘』不一定会出现『重排』,『重排』必然会出现『重绘』。

36.如何优化图片

  1. 对于很多装饰类图片,尽量不用图片,因为这类修饰图片完全可以用 CSS 去代替。
  2. 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。
  3. 小图使用 base64 格式
  4. 将多个图标文件整合到一张图片中(雪碧图)
  5. 选择正确的图片格式:
  • 对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好

  • 小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替

  • 照片使用 JPEG

37.margin重叠问题

两个块级元素的上外边距和下外边距可能会合并(折叠)为一个外边距,其大小会取其中外边距值大的那个,这种行为就是外边距折叠。需要注意的是,浮动的元素和绝对定位这种脱离文档流的元素的外边距不会折叠。重叠只会出现在垂直方向

计算原则
  • 如果两者都是正数,取较大的那个
  • 如果一正一负,取正值减去负值后的绝对值
  • 都是负值是,用0减去两个中绝对值大的那个
解决办法

对于折叠的情况,主要有两种:兄弟之间重叠父子之间重叠

兄弟间折叠:

  • 底部元素变为行内盒子:display: inline-block
  • 底部元素设置浮动:float
  • 底部元素的position的值为absolute/fixed

父子间的折叠:

  • 父元素加入:overflow: hidden
  • 父元素添加透明边框:border: 1px solid transparent
  • 子元素变为行内盒子:display: inline-block
  • 子元素加入浮动属性或定位

38.什么是严格模式与混杂模式?

  • 严格模式:是以浏览器支持的最高标准运行
  • 混杂模式:页面以宽松向下兼容的方式显示,模拟老式浏览器的行为

39.前端页面有哪三层构成,分别是什么?

构成:结构层表示层行为层

  1. 结构层(structural layer)

    结构层类似于盖房子需要打地基以及房子的悬梁框架,它是由HTML超文本标记语言来创建的,也就是页面中的各种标签,在结构层中保存了用户可以看到的所有内容,比如说:一段文字、一张图片、一段视频等等

  2. 表示层(presentation layer)

    表示层是由CSS负责创建,它的作用是如何显示有关内容,学名:层叠样式表,也就相当于装修房子,看你要什么风格的,田园的、中式的、地中海的,总之CSS都能办妥

  3. 行为层(behaviorlayer)

    行为层表示网页内容跟用户之间产生交互性,简单来说就是用户操作了网页,网页给用户一个反馈,这是JavaScriptDOM主宰的领域

40.iframe的作用以及优缺点

iframe也称作嵌入式框架,嵌入式框架和框架网页类似,它可以把一个网页的框架和内容嵌入到现有的网页中。

优点:

  • 可以用来处理加载缓慢的内容,比如:广告
  • iframe 能原封不动的把嵌入的网页展现出来。
    如果有多个网页引用 iframe,只需修改 iframe 的内容,就可以实现调用每一个页面的更改,方便快捷。

缺点:

  • iframe会阻塞主页面的Onload事件
  • iframe和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载。但是可以通过JS动态给ifame添加src属性值来解决这个问题,当然也可以解决iframe会阻塞主页面的Onload事件的问题
  • 会产生很多页面,不易管理,iframe 框架页面会增加服务器的 http 请求,对于大型网站不可取。
  • 浏览器的后退按钮没有作用
  • 无法被一些搜索引擎识别,很多移动设备无法完全显示框架,设备兼容性差。
  • iframe 和主页面共享链接池,而浏览器对相同域的链接有限制,所以会影响页面的并行加载。
    不利于 SEO,代码复杂,无法一下被搜索引擎索引到。
注意:通过动态给 iframe 添加 src 属性值,可解决前两个问题。

41.常见CSS选择器

注意事项

  • !important声明的样式的优先级最高
  • 如果优先级一致,则最后出现的样式生效
  • 继承得到的样式的优先级最低
  • 样式来源不同时,优先级顺序为:内联样式 > 内部样式 > 外部样式 > 浏览器用户自定义样式 > 浏览器默认样式

42.伪元素和伪类的区别和作用

伪元素伪类
在元素前后插入额外的元素或样式,插入的元素没有子文档中生成,它们只在外部显示可见将特殊的效果添加到特定的选择器上。它是在现有元素上添加类别,并不会产生新的元素
css3中伪元素在书写是使用双冒号::,比如::before冒号:用于伪类,比如:hover

伪类是通过在元素选择器上加入伪类改变元素的状态,而伪元素通过对元素的操作来改变元素

43.隐藏页面中的某个元素的方法有哪些?

隐藏类型

屏幕并不是唯一的输出机制,比如说屏幕上看不见的元素(隐藏的元素),其中一些依然能够被读屏软件阅读出来(因为读屏软件依赖于可访问性树来阐述)。为了消除它们之间的歧义,我们将其归为三大类:

  • 完全隐藏:元素从渲染树中消失,不占据空间。
  • 视觉上的隐藏:屏幕中不可见,占据空间。
  • 语义上的隐藏:读屏软件不可读,但正常占据空。

完全隐藏

(1) display 属性

 display: none;

(2) hidden 属性 HTML5 新增属性,相当于 display: none

<div hidden></div>

视觉上的隐藏

(1) 设置 posoition 为 absolute 或 fixed,通过设置 top、left 等值,将其移出可视区域。

position:absolute;
left: -99999px;

(2) 设置 position 为 relative,通过设置 top、left 等值,将其移出可视区域。

position: relative;
left: -99999px;
height: 0

(3) 设置 margin 值,将其移出可视区域范围(可视区域占位)。

margin-left: -99999px;
height: 0;

语义上隐藏

aria-hidden 属性

读屏软件不可读,占据空间,可见。

<div aria-hidden="true"></div>

45.实现单行、多行文本溢出隐藏

单行文本溢出

overflow: hidden;           // 溢出隐藏
text-overflow: ellipsis;    // 溢出部分使用省略号显示
white-space: nowrap;        // 规定段落中的文本不可换行

多行文本溢出

overflow: hidden;            // 溢出隐藏
text-overflow: ellipsis;     // 溢出用省略号显示
display:-webkit-box;         // 作为弹性伸缩盒子模型显示。
-webkit-box-orient:vertical; // 设置伸缩盒子的子元素排列方式:从上到下垂直排列
-webkit-line-clamp:3;        // 显示的行数

46.实现水平垂直居中

利用绝对定位(一)

.parent {
    position: relative;
}
.child {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%)
}

利用绝对定位(二):适用于已知盒子宽高

.parent {
    position: relative;
}
.child {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    margin: auto;
}

利用绝对定位(三):适用于已知盒子宽高

.parent {
    position: relative;
}
.child {
    position: absolute;
    top: 50%;
    left: 50%;
    margin-top: -50px;     /* 自身 height 的一半 */
    margin-left: -50px;    /* 自身 width 的一半 */
}

flex布局

.parent {
    display: flex;
    justify-content: center;
    align-items: center;
}

47.网页布局有哪几种,有什么区别

静态、自适应、流式、响应式四种网页布局
静态布局:意思就是不管浏览器尺寸具体是多少,网页布局就按照当时写代码的布局来布置;
自适应布局:就是说你看到的页面,里面元素的位置会变化而大小不会变化;
流式布局:你看到的页面,元素的大小会变化而位置不会变化——这就导致如果屏幕太大或者太小都会导致元素无法正常显示。
自适应布局:每个屏幕分辨率下面会有一个布局样式,同时位置会变而且大小也会变。

48.SGML、HTML、XML 和 XHTML的区别

  • SGML 是标准通用标记语言,是一种定义电子文档结构和描述其内容的国际标准语言,是所有电子文档标记语言的起源。
  • HTML 是超文本标记语言,主要是用于规定怎样显示网页。
  • XML 是可扩展标记语言,是未来网页语言的发展方向,XML 和 HTML 的最大区别就在于 XML 的标签是可以自己创建的,数量无限多,而 HTML 的标签都是固定的而且数量有限。
  • XHTML 也是现在基本上所有网页都在用的标记语言,他其实和 HTML 没什么本质的区别,标签都一样,用法也都一样,就是比 HTML 更严格,比如标签必须都用小写,标签都必须有闭合标签等

49.对浏览器内核的理解

主要分为两部分:渲染引擎和JS引擎。

  • 渲染引擎:其职责就是渲染,即在浏览器窗口中显示所请求的内容。默认情况下,渲染引擎可以显示 HTML、 XML 文档及图片,它也可以借助一些浏览器扩展插件显示其他类型数据,如:使用PDF阅读器插件可以显示 PDF 格式。
  • JS引擎:解析和执行 JavaScript 来实现网页的动态效果。

最开始渲染引擎和JS引擎并没有区分的很明显,后来JS引擎越来越独立,内核就倾向于只指渲染引擎了。

50.什么是文档的预解析?

当执行 JavaScript 脚本时,另一个线程解析剩下的文档,并加载后面需要通过网络加载的资源。这种方式可以使资源并行加载,从而使整体速度更快。

需要注意的是,预解析并不改变DOM树,它将这个工作交给主解析过程,自己只解析外部资源的引用,比如:外部脚本、样式及图片。

51.浏览器的渲染原理

简记: 生成DOM树 --> 生成CSS规则树 --> 构建渲染树 --> 布局 --> 绘制

  1. 首先解析收到的文档,根据文档定义构建一颗 DOM 树,DOM 树是由 DOM 元素及属性节点组成的。
  2. 然后对 CSS 进行解析,生成一颗 CSS 规则树
  3. 根据 DOM 树和 CSS 规则树构建渲染树。渲染树的节点被称为渲染对象,它是一个包含有颜色等属性的矩形。渲染对象和 DOM 元素相对应,但这种关系不是一对一的,不可见的 DOM 元素不会插入渲染树。还有一些 DOM 元素对应几个可见对象,它们一般是一些具有复杂结构的元素,无法用一个矩形来描述。
  4. 当渲染对象被创建并添加到树中,它们没有位置和大小,所以当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的是计算出各个节点在页面中确切位置和大小。通常这一行为也被称为自动重排。
  5. 布局阶段结束后是绘制阶段,遍历渲染树并调用渲染对象的 paint 方法将它们的内容显示到屏幕上。值得注意的是,这个过程是逐步完成的,为了更好的用户体验,渲染引擎会尽早的将内容呈现到屏幕上,并不会等到所有 HMTL 内容都解析完之后再去构建和布局渲染树,它是解析完一部分内容就显示一部分内容,同时,可能还通过网络下载其余内容。

52.什么是 canvas,基本用法是什么?

canvas 元素是 HTML5 的一部分,允许脚步语言动态渲染位图像。canvas 由一个可控制区域 HTML 代码中的属性定义决定高度和宽度。JavaScript 代码可以访问该区域,通过一套完整的绘图功能类似于其他通用二维的 API,从而生成动态的图形。

  1. 创建 canvas 标签
<canvas id="myCanvas" width="150" height="150">该浏览器不支持canvas</canvas>

  1. 渲染上下文
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

代码第一行通过使用 document.getElementById() 方法获取 `` 元素对应的 DOM 对象,然后可以通过使用它的 getContext() 方法来绘制上下文。 创建 canvas 标签时可以在标签内写上不支持的提示信息;也可以通过 getContext() 方法判读是否支持编程。

var canvas = document.getElementById('myCanvas');
if (canvas.getContext) {
  var ctx = canvas.getContext('2d');
  // other code
} else {
  // 不支持 canvas 的其他代码
}

用途:

canvas 可用于游戏和图表(echarts.js、heightchart.js 都是基于 canvas 来绘图)制作。

53.style 标签写在 body 后与 body 前有什么区别?

  1. 写在 body 标签前有利于浏览器逐步渲染: resources downloading --> cssDOM + DOM --> Render Tree --> layout --> paint
  2. 写在 body 标签后: 由于浏览器以逐行方式对 HTML 文档进行解析,当解析到写在尾部的样式表(外联或写在 style 标签)会导致浏览器停止之前的渲染,等待加载且解析样式完成后重新渲染;在 windows 的 IE 下可能出现样式失效导致的页面闪烁问题。

54.rgba() 和 opacity 设置透明度的区别是什么?

rgba() 和 opacity 都能实现透明效果,但最大的不同是 opacity 作用于元素,以及元素内的所有内容的透明度;而 rgba() 只作用于元素的颜色或其背景色,设置 rgba() 透明的元素的子元素不会继承透明效果。

55.浏览器是如何解析 css 选择器的?

从右向左解析的。若从左向右匹配,发现不符合规则,需要回溯,会损失很多性能。若从右向左匹配,先找到所有的最后节点,对于每一个节点,向上寻找其父节点直到查找至根元素或满足条件的匹配规则,则结束这个分支的遍历。

在 css 解析完毕后,需将解析结果 css 规则树和 DOM Tree 一起进行分析建立一颗 Render Tree,最终用来进行绘图。

56.简述 transform,transition,animation 的作用

  1. transform:描述了元素的静态样式,本身不会呈现动画效果,可以对元素进行旋转 rotate、扭曲 skew、缩放 scale 和移动 translate 以及矩阵变形 matrix。transitionanimation 两者都能实现动画效果。transform 常配合transitionanimation 使用。
  2. transition:样式过渡,从一种效果逐渐改变为另一种效果,它是一个合写属性。transition: transition-property transition-duration transition-timing-function transition-delay 从左到右,依次是:过渡效果的css属性名称、过渡效果花费时间、速度曲线、过渡开始的延迟时间 transition 通常和 hover 等事件配合使用,需要由事件来触发过渡。
  3. animation:动画,有 @keyframes 来描述每一帧的样式。

区别:

  • transform 仅描述元素的静态样式,常配合transitionanimation 使用。
  • transition 通常和 hover 等事件配合使用;animation 是自发的,立即播放。
  • animation 可以设置循环次数。
  • animation 可以设置每一帧的样式和时间,transition 只能设置头尾。
  • transition 可以与 js 配合使用, js 设定要变化的样式,transition 负责动画效果。

57.box-sizing 属性

  1. content-box,对应标准盒模型。
  2. border-box,IE盒模型。
  3. inherit,继承父元素的 box-sizing 值。

58.实现图片懒加载的原理

懒加载原理:先设置图片的 data-set 属性值(也可以是其他任意的,只要不发生 http 请求就可以,作用是为了存取值)为图片路径,由于不是 src 属性,故不会发生 http 请求。然后计算出页面的 scrollTop 的高度和浏览器的高度之和,如果图片距页面顶端距离小于前两者之和,说明图片要显示出来了,这时将 data-set 属性替换为 src 属性即可。

59.css sprites (雪碧图/精灵图)

css sprites 就是把网页中一些小图片整合到一张图片文件中,再利用 css 的 background-image、background-repeat、background-position 的组合进行背景定位。

优点: 减少图片体积;减少 http 请求次数

缺点:维护比较麻烦;不能随便改变大小,会失真模糊

60.什么是字体图标?

字体图标简单的说,就是一种特殊的字体,通过这种字体,显示给用户的就像一个个图片一样。字体图标最大的好处,在于它不会变形和加载速度快。字体图标可以像文字一样,随意通过 css 来控制它的大小和颜色,非常方便。

61.主流浏览器内核私有属性 css 前缀?

  • mozilla(firefox、flock等): -moz
  • webkit 内核(safari、chrome等): -webkit
  • opera 内核(opera浏览器): -o
  • trident 内核(ie 浏览器): -ms

HTTP

1.什么是 HTTP?

HTTP (HyperText Transfer Protocol),即超文本传输协议,是一种实现网络通信的规范。它定义了客户端和服务器之间交换报文的格式和方式,默认使用的是80端口,其底层使用TCP作为传输层协议,保证了数据传输的可靠性。

特点:

  • 简单快速:客户端向服务器请求服务时,只需传送请求方法和路径。由于HTTP协议简单,使得HTTP服务器的规模小,因而通信速度很快。
  • 灵活:HTTP允许传输任意类型的数据对象。
  • 无连接:限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。
  • 无状态:HTTP协议无法根据之前的状态进行本次的请求处理。
  • 明文:HTTP是以明文的形式传递内容。

2.HTTP 和 HTTPS 的区别?

HTTPSHTTP协议的安全版本。HTTPS的出现主要是为了解决HTTP明文传输内容导致其不安全的特性。为保证数据加密传输,让HTTP运行安全的SSL/TLS协议上,即 HTTPS = HTTP + SSL/TLS。通过SSL证书来验证服务器的身份,并为浏览器和服务器之间的通信进行加密。

二者的区别:

  • 安全性:HTTP协议的数据传输是明文的,是不安全的;HTTPS 使用了SSL/TLS协议进行加密处理,相对更加安全。
  • 连接方式:二者使用的连接方式不同,HTTP是三次握手,HTTPS是三次握手+数字证书。
  • 默认端口:HTTP的默认端口是80HTTPS的默认端口是443
  • 响应速度:由于HTTPS需要进行加解密过程,因此速度不如HTTP
  • 费用:HTTPS需要使用SSL证书,功能越强大的证书其费用越高;HTTP不需要。

3.HTTP 1.0 和 1.1 的区别?

  1. 连接:HTTP 1.0默认使用非持久连接,HTTP 1.1则默认使用持久连接。HTTP 1.1通过使用持久连接来使多个HTTP请求复用同一个TCP连接,避免了HTTP 1.0中使用非持久连接造成的每次请求都需要建立连接的时延。
  2. 缓存:HTTP 1.0主要使用header中的If-Modified-SinceExpires 来做为缓存判断的标准;HTTP 1.1则引入了更多的缓存控制策略,例如:EtagIf-Unmodified-SinceIf-MatchIf-None-Match等更多可供选择的缓存头来控制缓存策略。
  3. 资源请求:HTTP 1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能;HTTP 1.1则在请求头引入了range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
  4. host:HTTP 1.1引入了host,用来指定服务器的域名。
  5. 方法:HTTP 1.1相较于HTTP 1.0新增了许多方法,如:putdeleteoptions等。

4.HTTP 状态码有哪些?

状态码第一位数字决定了不同的响应状态:
1xx表示请求已被接受,需要继续处理;
2xx表示请求成功;
3xx表示重定向;
4xx表示客户端错误;
5xx表示服务端错误。

常见的状态码:

  • 101:服务器根据客户端的请求切换协议,主要用于websockethttp2升级
  • 200:请求已成功,请求所希望的数据将随响应一起返回。
  • 201:请求成功并且服务器创建了新的资源。
  • 202:服务器已接受响应请求,但尚未处理。
  • 301:请求的网页已永久移动至新的位置。
  • 302:临时重定向/临时转移。服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
  • 304:本次获取到的内容是读取缓存中的数据,会每次去服务器校验。
  • 401:请求需要进行身份验证,尚未认证,没有登录网站。
  • 403:禁止访问,服务器拒绝请求。
  • 404:服务器没有找到相应资源。
  • 500:服务器遇到错误,无法完成对请求的处理。
  • 503:服务器无法使用。

5.GET和POST的请求的区别

区别GETPOST
幂等性
应用场景用于对服务器资源不会产生影响的场景(比如请求一个网页的资源等)用于对服务器资源会产生影响的情景(比如注册用户等)
是否缓存
传参方式查询字符串传参请求体传参
安全性将参数放入url中向服务器发送,不安全在请求体中,安全
请求长度浏览器对于url长度有限制,会受到影响在请求体中,不会收到浏览器影响
参数类型ASCII字符文件、图片等

幂等性:指一个请求方法执行一次和多次的效果完全相同

6.POST和PUT的请求的区别

区别POSTPUT
作用创建数据更新数据

为什么POST请求会发送两次?

  • 第一次请求为options预检请求,状态码为204

作用1:询问服务器是否支持修改的请求头,如果服务器支持,则在第二次中发送真正的请求
作用2: 检测服务器是否为同源请求,是否支持跨域

  • 第二次请求为真正的POST请求

7.常见的HTTP请求头和响应头

HTTP Request Header定义
Accept浏览器能够处理的内容类型
Accept-Charset浏览器能够显示的字符集
Accept-Encoding浏览器能够处理的压缩编码
Accept-Language浏览器当前设置的语言
Connection浏览器与服务器之间连接的类型
Cookie浏览器当前页面设置的任何Cookie
Host当前发出请求的页面所在的域
Referer当前发出请求的页面的URL
User-Agent浏览器的用户代理字符串
HTTP Responses Header定义
Date表示消息发送的时间,时间的描述格式由rfc822定义
server服务器名称
Connection浏览器与服务器之间连接的类型
Cache-Control控制HTTP缓存
content-type表示后面的文档属于什么MIME类型
Content-Type定义
application/x-www-form-urlencoded浏览器原生form表单
multipart/form-data表单上传文件
application/json服务器消息主体是序列化后的 JSON 字符串
text/xml提交 XML 格式的数据

8.状态码304

为什么会有304?

服务器为了提高网站访问速度,对之前访问的部分页面指定缓存机制。当客户端再次请求页面时,服务器会判断请求的页面是否已被缓存,若已经被缓存则返回304,此时客户端将调用缓存内容。

状态码304不应该被认为是一种错误,而是对客户端有缓存情况下服务端的一种响应。

产生较多304状态码的原因是什么?

  • 页面更新周期长或者长时间未更新
  • 纯静态页面或强制生成静态HTML

304状态码过多会造成什么问题?

  • 网站快照停止
  • 收录减少
  • 权重下降

9.常见的HTTP请求方法

方法作用
GET向服务器获取数据
POST向服务器发送数据
PUT修改数据
PATCH用于对资源进行部分修改
DELETE删除指定数据

10.如何解决跨域问题?

  • JSONP:Jsonp(JSON with Padding) 是 json 的一种"使用模式",可以让网页从别的域名(网站)那获取资料,即跨域读取数据。ajax 请求受同源策略影响,不允许进行跨域请求,而 script 标签 src 属性中的链接却可以访问跨域的 js 脚本,利用这个特性,服务端不再返回 JSON 格式的数据,而是返回一段调用某个函数的 js 代码,在 src 中进行了调用,这样实现了跨域。
  • CORS:CORS(Cross-origin resource sharing)跨域资源共享,服务器设置对 CORS 的支持。其原理是,服务器设置 Access-Control-Allow-Origin HTTP响应头之后,浏览器将会允许跨域请求。实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
  • proxy代理:最常用多方式。通俗点说就是客户端浏览器发起一个请求会存在跨域问题,但是服务端向另一个服务端发起请求并无跨域,因为跨域问题归根结底源于同源策略,而同源策略只存在于浏览器。那么我们是不是可以通过 Nginx 配置一个代理服务器,反向代理访问跨域的接口,并且我们还可以修改 Cookie 中 domain 信息,方便当前域 Cookie 写入。

11.正向代理和反向代理?

正向代理隐藏了真实的请求客户端,服务端不知道真实的客户端是谁,客户端请求的服务都被代理服务器代替来请求。

反向代理隐藏了真实的服务端,当发送一个请求时,其背后可能有很多台服务器为我们服务,但具体是哪一台,我们不知道,也不需要知道,我们只需要知道反向代理服务器是谁就好了,反向代理服务器会帮我们把请求转发到真实的服务器那里去。反向代理器一般用来实现负载平衡。

12.从输入URL到看到页面的过程,发生了什么?

  1. url解析:首先会判断输入的是一个合法 url还是关键词,并根据输入的内容进行相应的操作。

  2. 查找缓存:浏览器会判断所请求的资源是否在浏览器缓存中,以及是否失效。如果没有失效就直接使用;如果没有缓存或失效了,就继续下一步。

  3. DNS解析:此时需要获取url中域名对应的IP地址。浏览器会依次查看浏览器缓存操作系统缓存中是否有ip地址,如果缓存中没有就会向本地域名服务器发起请求,获取ip地址。本地域名服务器也会先检查缓存,有则直接返回;如果也没有,则采用迭代查询方式,向上级域名服务器查询。先向根域名服务器发起请求,获取顶级域名服务器的地址;再向顶级域名服务器发起请求以获取权限域名服务器地址;然后向权限域名服务器发起请求并得到url中域名对应的IP地址。

  4. 建立TCP连接:根据ip地址,三次握手与服务器建立TCP连接。

  5. 发起请求:浏览器向服务器发起HTTP请求。

  6. 响应请求:服务器响应HTTP请求,将相应的HTML文件返回给浏览器。

  7. 关闭TCP连接四次挥手关闭TCP连接。

  8. 渲染页面
    

    :浏览器解析HTML内容,并开始渲染。

    浏览器渲染过程

    如下:

    • 构建DOM树:词法分析然后解析成DOM树,DOM树是由DOM元素及属性节点组成,树的根是document对象。
    • 构建CSS规则树:生成CSS 规则树。
    • 构建渲染树:将DOM树和CSS规则树结合,构建出渲染树。
    • 布局:计算每个节点的位置。
    • 绘制:使用浏览器的UI接口进行绘制。

13.即时通讯的实现方式?

主要有四种方式,它们分别是轮询长轮询(comet)长连接(SSE)WebSocket。它们大体可以分为两类,一种是在HTTP基础上实现的,包括短轮询、comet和SSE;另一种不是在HTTP基础上实现是,即WebSocket。

  1. 轮询
    短轮询的基本思路就是浏览器每隔一段时间向浏览器发送http请求,服务器在收到请求后,不论是否有数据更新,都直接进行响应。这种方式实现的即时通信,本质上还是浏览器发送请求,服务器接受请求的一个过程,通过让客户端不断的进行请求,使得客户端能够模拟实时地收到服务器端的数据的变化。
    优点:比较简单,易于理解,实现起来没有什么技术难点。
    缺点:由于需要不断的建立http连接,严重浪费了服务器端和客户端的资源。
  2. 长轮询
    当服务器收到客户端发来的请求后,服务器端不会直接进行响应,而是先将这个请求挂起,然后判断服务器端数据是否有更新。如果有更新,则进行响应;如果一直没有数据,则到达一定的时间限制(服务器端设置)才返回。
    优点:长轮询和短轮询比起来,明显减少了很多不必要的http请求次数,相比之下节约了资源。
    缺点:连接挂起也会导致资源的浪费。
  3. 长连接
    SSE是HTML 5新增的功能,全称为Server-Sent Events。它可以允许服务推送数据到客户端。SSE在本质上就与之前的长轮询、短轮询不同,虽然都是基于http协议的,但是轮询需要客户端先发送请求。而SSE最大的特点就是不需要客户端发送请求,可以实现只要服务器端数据有更新,就可以马上发送到客户端。 优点:不需要建立或保持大量的客户端发往服务器端的请求,节约了很多资源,提升应用性能;实现非常简单,并且不需要依赖其他插件。
  4. WebSocket
    WebSocket是HTML 5定义的一个新协议,与传统的http协议不同,该协议可以实现服务器与客户端之间全双工通信。简单来说,首先需要在客户端和服务器端建立起一个连接,这部分需要http。连接一旦建立,客户端和服务器端就处于平等的地位,可以相互发送数据,不存在请求和响应的区别。
    优点:实现了双向通信。
    缺点:服务器端的逻辑非常复杂。

14.Cookie

Cookie是最早被提出来的本地存储方式,在此之前,服务端是无法判断网络中的两个请求是否是同一用户发起的,为解决这个问题,Cookie就出现了。Cookie的大小只有4kb,它是一种纯文本文件,每次发起HTTP请求都会携带Cookie

特性
  • Cookie一旦创建成功,就无法修改
  • Cookie是无法跨域的
  • 每个域名下Cookie的数量不能超过20个,每个Cookie的大小不能超过4kb
  • 存在安全问题,一旦被拦截,即可获得session的所有信息
  • Cookie在请求一个新的页面的时候都会被发送出去
如何解决无法跨域问题?
  • 使用Nginx反向代理
  • 在一个站点登陆之后,往其他网站写Cookie。服务端的Session存储到一个节点,Cookie存储sessionId
应用场景
  • 和session结合使用,将sessionId存储到Cookie中,每次发送请求都会携带这个sessionId,以便于服务端识别和响应
  • 可以用来统计页面的点击次数

15.LocalStorage

LocalStorage是HTML5新引入的特性,由于有的时候我们存储的信息较大,Cookie就不能满足我们的需求,这时候LocalStorage就派上用场了

优点
  • LocalStorage能存储5MB的信息
  • LocalStorage能够持久化存储数据,数据不会随着页面的关闭而消失,除非手动清除
  • 仅存储在本地,发起HTTP请求的时候不会被携带
缺点
  • 存在兼容性问题,IE8以下版本浏览器不支持
  • 如果浏览器设置为隐私模式,我们将无法获取到LocalStorage
  • 受到同源策略的限制,即端口、协议、主机地址有任何一个不相同,都不会访问
常用API
API注释
localStorage.setItem(key, value)保存数据到 localStorage
localStorage.getItem(key)从 localStorage 获取数据
localStorage.removeItem(key)从 localStorage 删除key对应的数据
localStorage.clear()从 localStorage 删除所有保存的数据
localStorage.key(index)获取某个索引的Key
应用场景
  • 一些网站配置个人设置的时候,比如肤色、字体等会将数据保存在LocalStorage中
  • 保存一些不经常变动的个人信息或用户浏览信息

16.SessionStorage

SessionStorage和LocalStorage都是在HTML5才提出来的存储方案,SessionStorage 主要用于临时保存同一窗口(或标签页)的数据,刷新页面时不会删除,关闭窗口或标签页之后将会删除这些数据

SessionStorage与LocalStorage对比
  • SessionStorage和LocalStorage都在本地进行数据存储
  • SessionStorage也有同源策略的限制,但是SessionStorage有一条更加严格的限制,SessionStorage只有在同一浏览器的同一窗口下才能够共享
  • LocalStorage和SessionStorage都不能被爬虫爬取
常用API
API注释
sessionStorage.setItem(key, value)保存数据到 sessionStorage
sessionStorage.getItem(key)从 sessionStorage获取数据
sessionStorage.removeItem(key)从 sessionStorage删除key对应的数据
sessionStorage.clear()从 sessionStorage删除所有保存的数据
sessionStorage.key(index)获取某个索引的Key
应用场景
  • 由于SessionStorage具有时效性,所以可以用来存储一些网站的游客登录的信息,还有临时的浏览记录的信息。当关闭网站之后,这些信息也就随之消除了

17.Cookie、LocalStorage、SessionStorage区别

CookieLocalStorageSessionStorage
实最开始是服务器端用于记录用户状态的一种方式,由服务器设置,在客户端存储,然后每次发起同源请求时,发送给服务器端。cookie 最多能存储 4 k 数据,它的生存时间由 expires 属性指定,并且 cookie 只能被同源的页面访问共享html5 提供的一种浏览器本地存储的方法,它一般也能够存储 5M 或者更大的数据。它和 sessionStorage 不同的是,除非手动删除它,否则它不会失效,并且 localStorage 也只能被同源页面所访问共享html5 提供的一种浏览器本地存储的方法,它借鉴了服务器端 session 的概念,代表的是一次会话中所保存的数据。它一般能够存储 5M 或者更大的数据,它在当前窗口关闭后就失效了,并且 sessionStorage 只能被同一个窗口的同源页面所访问共享
  1. 共同点:都是保存在浏览器端,且同源的
  2. 区别:
  • cookie 始终在同源的 http 请求中携带(即使不需要),即 cookie 在浏览器和服务器之间来回传递;而 sessionStorage 和 localStorage 不会自动把数据发送到服务器,仅在本地保存。cookie 还有路径(path)的概念,可以限制 cookie 只属于某个路径下。
  • 存储大小限制不同。cookie 不能超过 4K,因为每次 http 请求都会携带 cookie,所以 cookie 只适合保存很小的数据,如:会话标识。sessionStorage 和 localStorage 虽然也有存储大小限制,但比 cookie 大得多,可以达到 5M 或更大。
  • 数据有效期不同。sessionStorage 仅在当前浏览器窗口关闭之前有效;localStorage 始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;cookie 只在设置的 cookie 过期时间之前有效。
  • 作用域不同。sessionStorage 不在不同的浏览器窗口中共享,即使是同一个页面;localStorage 和 cookie 在所有同源窗口中都是共享的。

18.ajax

AJAX Ajax 即“AsynchronousJavascriptAndXML”(异步 JavaScript 和 XML),是指一种创建交互式网页应用的网页开发技术。它是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。通过在后台与服务器进行少量数据交换,Ajax 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。传统的网页(不使用 Ajax)如果需要更新内容,必须重载整个网页页面。其缺点如下:

  • 本身是针对MVC编程,不符合前端MVVM的浪潮
  • 基于原生XHR开发,XHR本身的架构不清晰
  • 不符合关注分离(Separation of Concerns)的原则
  • 配置和调用方式非常混乱,而且基于事件的异步模型不友好
(1)创建对象

var xhr = new XMLHttpRequest();

(2)打开请求

xhr.open('GET', 'example.txt', true);

(3)发送请求

xhr.send(); 发送请求到服务器

(4)接收响应

xhr.onreadystatechange =function(){}

(1)当readystate值从一个值变为另一个值时,都会触发readystatechange事件。

(2)当readystate==4时,表示已经接收到全部响应数据。

(3)当status ==200时,表示服务器成功返回页面和数据。

(4)如果(2)(3)内容同时满足,则可以通过xhr.responseText,获得服务器返回的内容。

19.fetch

Fetch fetch号称是AJAX的替代品,是在ES6出现的,使用了ES6中的promise对象。Fetch是基于promise设计的。Fetch的代码结构比起ajax简单多。fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象

优点缺点
语法简洁,更加语义化fetch只对网络请求报错,对400,500都当做成功的请求,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。
基于标准 Promise 实现,支持 async/awaitfetch默认不会带cookie,需要添加配置项: fetch(url, {credentials: ‘include’})
更加底层,提供的API丰富(request, response)fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费
脱离了XHR,是ES规范里新的实现方式fetch没有办法原生监测请求的进度,而XHR可以

20.axios

Axios 是一种基于Promise封装的HTTP客户端

  • 浏览器端发起XMLHttpRequests请求
  • node端发起http请求
  • 支持Promise API
  • 监听请求和返回
  • 对请求和返回进行转化
  • 取消请求
  • 自动转换json数据
  • 客户端支持抵御XSRF攻击

21.Promise

ES6新增的一种异步编程的解决方案,比传统的回调函数和事件更加的合理和强大。通过Promise可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。Promise可以解决异步的问题,但不能说Promise是异步的

特点
  1. 对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:
  2. pending——进行中
  3. resolved——已成功
  4. rejected——已失败
  5. 一旦状态改变,就不会再发生变化,任何时候都可以得到这个结果。Promise对象状态的改变只有两种可能:
  6. pending——resolved
  7. pending——rejected
  8. Promise内部发生错误,不会影响到外部程序的执行。
  9. Promise一旦执行则无法取消:
  10. 一旦创建就会立即执行,无法中途取消(*缺点1*
  11. 如果不设置回调函数,Promise内部抛出的错误将不会反应到外部(*缺点2*
  12. 当处于pending状态时,无法得知目前进展到哪一阶段,即无法预测是刚刚开始还是即将完成(*缺点3*
用法

创建Promise实例时,必须传入一个函数作为参数:

new Promise(() => {})

该函数可以接收另外两个由JavaScript引擎提供的函数,resolvereject:

  • resolve——将Promise对象的状态从pending变为resolved,将异步操作的结果作为参数传递出去
  • reject——将Promise对象的状态从pending变为rejected,将异步操作报出的错误作为参数传递出去
const promise = new Promise((resolve, reject) => {
    if (true) resolve('value')
    else reject('error')
})

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数:

promise.then(value => {
    console.log(value)  // 'value'
}, error => {
    console.log(error)  // 'error'
})

then方法只有一个函数参数时,此时为resolved状态的回调方法:

promise.then(value => {
    // 只有状态为resolved时才能调用,如果返回的是rejected状态,则报错 Uncaught (in promise) error
    console.log(value)
})

只有当promise的状态变为resolved或者rejected时,then方法才会被调用

Promise新建后就会立即执行,并且调用resolvereject后不会终结 Promise的参数函数的执行。

let promise = new Promise(resolve => {
    console.log('1')
    resolve()
    console.log('2')
})
promise.then(resolved => {
    console.log('3')
})
console.log('3')

resolve返回的是另外一个Promise实例:

const p1 = new Promise((_, reject) => {
  setTimeout(() => reject('error'), 3000);
})
const p2 = new Promise(resolve => {
  setTimeout(() => resolve(p1), 1000);
})
p2.then(
  result => console.log(result),
  error => console.log(error) // error
)

上面代码中,p1是一个 Promise,3 秒之后变为rejectedp2的状态在 1 秒之后改变,resolve方法返回的是p1。由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以,后面的then语句都变成针对后者(p1)。又过了 2 秒,p1变为rejected,导致触发catch方法指定的回调函数。可以理解成p2.then 实际上是p1.then
resolve返回的是另一个Promise实例的时候,当前Promise实例的状态会根据返回的Promise实例的状态来决定

常用API

Promise.resolve()

有时需要将现有对象转为 Promise 对象,Promise.resolve()方法就起到这个作用,且实例状态为resolve:

Promise.resolve('foo')
// 等价于
return new Promise(resolve => resolve('foo'))

Promise.resolve()的参数有以下几种情况:

  • 参数是一个Promise实例:
const promise = new Promise(resolve => {
    resolve('resolve')
})
let p = Promise.resolve(promise)
// p 相当于
let p = new Promise(resolve => {
    resolve(promise)
})
console.log(p === promise)  // true
  • 参数是一个thenable对象:

thenable对象指的是具有then方法的对象,Promise.resolve()会将这个对象转为Promise对象,然后立即执行thenable对象的then方法

const thenable = {
    then(resolve, reject) {
        resolve('resolved')
    }
}
const p1 = Promise.resolve(thenable)
p1.then(res => {
    console.log(res)    // 'resolved'
})

上面代码中,thenable对象的then()方法执行后,对象p1的状态就变为resolved,从而立即执行最后那个then()方法指定的回调函数,输出’resolved’

  • 参数不是具有then()方法的对象,或者根本不是对象
const promise = Promise.resolve({name: 'James'})
promise.then(res => {
    console.log(res)    // {name: 'James'}
})

当参数是不含有then()方法的对象,或者根本不是对象时,会直接返回该参数

  • 不带有任何参数
const promise = Promise.resolve()
promise.then(res => {
    console.log(res)    // undefined
})
Promise.resolve()`方法允许调用时不带参数,直接返回一个`resolved`状态的 `Promise` 对象,传参为`undefined

Promise.reject()

Promise.reject(reason)`方法也会返回一个新的 `Promise` 实例,该实例的状态为`rejected
const promise = Promise.reject('Error')
// 等价于
const promise = new Promise((resolve, reject) => {
    reject('Error')
})

Promise.all()

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例

const p1 = new Promise((resolve, reject) => {})
const p1 = new Promise((resolve, reject) => {})
const p1 = new Promise((resolve, reject) => {})
const promise = Promise.all([p1, p2, p3])
promise.then(result => {}, error => {})

面代码中,Promise.all()方法接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。p的状态由p1p2p3决定,分成两种情况:

  • 只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数
  • 只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数
const number = 35

const p1 = new Promise((resolve, reject) => {
  if (number >= 10) resolve('p1 success!')
  else reject('p1 failed!')
})
const p2 = new Promise((resolve, reject) => {
  if (number >= 20) resolve('p2 success!')
  else reject('p2 failed!')
})
const p3 = new Promise((resolve, reject) => {
  if (number >= 30) resolve('p3 success!')
  else reject('p3 failed!')
})
const promise = Promise.all([p1, p2, p3]).then(res => {
  console.log(res)  // 当number为35时,res值为[ 'p1 success!', 'p2 success!', 'p3 success!' ]
}, error => {
  console.log(error)    // 当number为25时,p3会返回rejected,promise状态会变成rejected,error值为p3 failed!
})

如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()catch方法

const p1 = new Promise(resolve => {
  resolve("hello");
}).then(result => result).catch(e => e);

const p2 = new Promise(() => {
  throw new Error("报错了");
}).then(result => result).catch(e => e); // p2实际上是catch返回的promise实例

Promise.all([p1, p2]).then(result => console.log(result)).catch(e => console.log(e));

22.async/await

async声明function是一个异步函数,返回一个promise对象,可以使用 then 方法添加回调函数。
async函数内部return语句返回的值,会成为then方法回调函数的参数。
如果async函数没有返回值 async函数返回一个undefined的promise对象

async function test() {
    return 'test'
}
console.log(test)   // [AsyncFunction: test] async函数是[`AsyncFunction`]构造函数的实例
console.log(test()) // Promise { 'test' }

// async返回的是一个promise对象
test().then(res => {
    console.log(res)    // 'test'
})

await 操作符只能在异步函数 async function 内部使用
如果一个 Promise 被传递给一个 await 操作符,await 将等待 Promise 正常处理完成并返回其处理结果,也就是说它会阻塞后面的代码,等待 Promise 对象结果;如果等待的不是 Promise 对象,则返回该值本身

async function test() {
  return new Promise((resolve)=>{
    setTimeout(() => {
        resolve('test 1000');
    }, 1000);
  })
}
function fn() {
  return 'fn';
}

async function next() {
    let res0 = await fn(),
        res1 = await test(),
        res2 = await fn();
    console.log(res0);
    console.log(res1);
    console.log(res2);
}
next(); // 1s 后才打印出结果 为什么呢 就是因为 res1在等待promise的结果 阻塞了后面代码。
错误处理

如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject

async function test() {
  await Promise.reject('错误了')
}

test().then(res=>{
  console.log('success',res)
},err=>{
  console.log('err ',err)   // err 错误了
})

防止出错的方法,也是将其放在try...catch代码块之中

async function test() {
  try {
    await new Promise(function (resolve, reject) {
      throw new Error('错误了');
    });
  } catch(e) {
      console.log('err', e)
  }
  return await('成功了');
}

多个await命令后面的异步操作,如果不存在继发关系(即互不依赖),最好让它们同时触发

let foo = await getFoo();
let bar = await getBar();
// 上面这样写法 getFoo完成以后,才会执行getBar

// 同时触发写法 ↓

// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
优点

async/await的优势在于处理由多个Promise组成的 then 链,在之前的Promise文章中提过用then处理回调地狱的问题,async/await相当于对promise的进一步优化。 假设一个业务,分多个步骤,且每个步骤都是异步的,而且依赖上个步骤的执行结果

// 假设表单提交前要通过俩个校验接口

async function check(ms) { // 模仿异步 
  return new Promise((resolve)=>{
    setTimeout(() => {
        resolve(`check ${ms}`);
    }, ms);
  })
}
function check1() {
  console.log('check1');
  return check(1000);
}
function check2() {
  console.log('check2');
  return check(2000);
}

// -------------promise------------
function submit() {
  console.log('submit');
  // 经过俩个校验 多级关联 promise传值嵌套较深
  check1().then(res1=>{
    check2(res1).then(res2=>{
       /*
        * 提交请求
        */
    })
  })
}
submit();

// -------------async/await-----------
async function asyncAwaitSubmit() {
    let res1 = await check1(),
        res2 = await check2(res1);
        console.log(res1, res2);
        /*
        * 提交请求
        */
}

23.TCP 的三次握手和四次挥手?

三次握手🤝:

  1. 第一次握手:客户端给服务器发送一个SYN报文。
  2. 第二次握手:服务器收到SYN报文后,应答一个ACK报文,同时发出自己的SYN报文,即应答了一个SYN + ACK报文。
  3. 第三握手:客户端收到SYN + ACK报文后,回应一个ACk报文。服务器收到ACK报文之后,三次握手完毕。

四次挥手🙋:

  1. 第一次挥手:客户端认为没有数据需要继续发送,于是向服务器发送一个FIN报文,申请断开客户端到服务器端的连接,报文中同时会指定一个序列号seq(等于已传送数据的最后一个字节的序号加1)。此时,客户端进入FIN_WAIT_1状态。
  2. 第二次挥手:服务器接收到FIN报文后,向客户端发送一个确认报文ACK,表示已经接收到了客户端释放连接的请求,以后不会再接收客户端发送过来的数据。同时会把客户端报文中的序列号seq加1,作为ACK报文中的序列号值。此时,服务端处于CLOSE_WAIT状态。客户端接收到报文之后,进入FIN_WAIT_2状态,等待服务器发送连接释放报文(由于TCP连接是全双工的,此时客户端到服务区器的连接已释放,但是服务器仍可以发送数据给客户端)。
  3. 第三次挥手:服务器端发送完所有的数据,会向客户端发送一个FIN报文,申请断开服务器端到客户端的连接。此时,服务器进入LAST_ACK 状态。
  4. 第四次挥手:客户端接收到FIN报文之后,向服务器发送一个ACK报文作为应答,且把服务器报文中的序列号值加1作为ACK报文的序列号值。此时,客户端就进入TIME_WAIT状态。需要注意的是,该阶段会持续一段时间,以确保服务器收到了该ACK报文。这个时间是 2 * MSL(最长报文段时间),如果在这个时间内,没有收到服务器的重发请求,时间过后,客户端就进入CLOSED状态;如果收到了服务器的重发请求就需要重新发送确认报文。服务器只要一收到ACK报文,就会立即进入CLOSED状态。

24.理解web安全吗?都有哪几种,介绍以及如何预防

1.XSS,也就是跨站脚本注入
攻击方法:
1. 手动攻击
	编写注入脚本,比如”/><script>alert(document.cookie());</script><!--等,手动测试目标网站上有的input, textarea等所有可能输入文本信息的区域
2. 自动攻击
	利用工具扫描目标网站所有的网页并自动测试写好的注入脚本,比如:Burpsuite等
防御方法:
1. 将cookie等敏感信息设置为httponly,禁止Javascript通过document.cookie获得
2. 对所有的输入做严格的校验尤其是在服务器端,过滤掉任何不合法的输入,比如手机号必须是数字,通常可以采用正则表达式
3. 净化和过滤掉不必要的html标签,比如:<iframe>, alt,<script> 等
4. 净化和过滤掉不必要的Javascript的事件标签,比如:onclick, onfocus等
5. 转义单引号,双引号,尖括号等特殊字符,可以采用htmlencode编码 或者过滤掉这些特殊字符
6. 设置浏览器的安全设置来防范典型的XSS注入
2.SQL注入
攻击方法:
编写恶意字符串,比如‘ or  1=1--等,
手动测试目标网站上所有涉及数据库操作的地方
防御方法:
1. 禁止目标网站利用动态拼接字符串的方式访问数据库
2. 减少不必要的数据库抛出的错误信息
3. 对数据库的操作赋予严格的权限控制
4. 净化和过滤掉不必要的SQL保留字,比如:where, or, exec 等
5. 转义单引号,上引号,尖括号等特殊字符,可以采用htmlencode编码 或者过滤掉这些特殊字符
3.CSRF,也就是跨站请求伪造
就是攻击者冒用用户的名义,向目标站点发送请求
防范方法:
1. 在客户端进行cookie的hashing,并在服务端进行hash认证
2. 提交请求是需要填写验证码
3. 使用One-Time Tokens为不同的表单创建不同的伪随机值  

25.介绍下 http1.0http1.1http2.0 协议的区别?

首先说 http1.0

它的特点是每次请求和响应完毕后都会销毁 TCP 连接,同时规定前一个响应完成后才能发送下一个请求。这样做有两个问题:

  1. 无法复用连接

每次请求都要创建新的 TCP 连接,完成三次握手和四次挥手,网络利用率低

  1. 队头阻塞

如果前一个请求被某种原因阻塞了,会导致后续请求无法发送。

然后是 http1.1

http1.1 是 http1.0 的改进版,它做出了以下改进:

  • 长连接

http1.1 允许在请求时增加请求头connection:keep-alive,这样便允许后续的客户端请求在一段时间内复用之前的 TCP 连接

  • 管道化

基于长连接的基础,管道化可以不等第一个请求响应继续发送后面的请求,但响应的顺序还是按照请求的顺序返回。

  • 缓存处理

新增响应头 cache-control,用于实现客户端缓存。

  • 断点传输

在上传/下载资源时,如果资源过大,将其分割为多个部分,分别上传/下载,如果遇到网络故障,可以从已经上传/下载好的地方继续请求,不用从头开始,提高效率

最后是 http2.0

http2.0 进一步优化了传输效率,它主要有以下改进:

  • 二进制分帧

将传输的消息分为更小的二进制帧,每帧有自己的标识序号,即便被随意打乱也能在另一端正确组装

  • 多路复用

基于二进制分帧,在同一域名下所有访问都是从同一个 tcp 连接中走,并且不再有队头阻塞问题,也无须遵守响应顺序

  • 头部压缩

http2.0 通过字典的形式,将头部中的常见信息替换为更少的字符,极大的减少了头部的数据量,从而实现更小的传输量

  • 服务器推

http2.0 允许服务器直接推送消息给客户端,无须客户端明确的请求

26.介绍 HTTPS 握手过程

  1. 客户端请求服务器,并告诉服务器自身支持的加密算法以及密钥长度等信息
  2. 服务器响应公钥和服务器证书
  3. 客户端验证证书是否合法,然后生成一个会话密钥,并用服务器的公钥加密密钥,把加密的结果通过请求发送给服务器
  4. 服务器使用私钥解密被加密的会话密钥并保存起来,然后使用会话密钥加密消息响应给客户端,表示自己已经准备就绪
  5. 客户端使用会话密钥解密消息,知道了服务器已经准备就绪。
  6. 后续客户端和服务器使用会话密钥加密信息传递消息

27.HTTPS 握手过程中,客户端如何验证证书的合法性

  1. 校验证书的颁发机构是否受客户端信任。
  2. 通过 CRL 或 OCSP 的方式校验证书是否被吊销。
  3. 对比系统时间,校验证书是否在有效期内。
  4. 通过校验对方是否存在证书的私钥,判断证书的网站域名是否与证书颁发的域名一致。

28.Http 状态码 301 和 302 的应用场景分别是什么

301 表示永久重定向,302 表示临时重定向。

如果浏览器收到的是 301,则会缓存重定向的地址,之后不会再重新请求服务器,直接使用缓存的地址请求,这样可以减少请求次数。但如果浏览器收到的是 302,则不会缓存重定向地址,浏览器将来会继续以原有地址请求。

因此,301 适合地址永久转移的场景,比如域名变更;而 302 适合临时转移的场景,比如首页临时跳转到活动页

29. cookie 和 token 都存放在 header 中,为什么不会劫持 token?

由于浏览器会自动发送 cookie 到服务器,因此攻击者可以利用这种特点进行 csrf 攻击。

而通常 token 是不放到 cookie 中的,需要浏览器端使用 JS 自行保存到 localstorage 中,在请求时也需要手动的加入到请求头中,因此不容易引发 csrf 攻击。

30. 实现 token 加密

以最常见的 token 格式 jwt 为例, token 分为三段,分别是 header、payload、signature。 其中,header 标识签名算法和令牌类型;payload 标识主体信息,包含令牌过期时间、发布时间、发行者、主体内容等;signature 是使用特定的算法对前面两部分进行加密,得到的加密结果。

token 有防篡改的特点,如果攻击者改动了前面两个部分,就会导致和第三部分对应不上,使得 token 失效。而攻击者不知道加密秘钥,因此又无法修改第三部分的值。

所以,在秘钥不被泄露的前提下,一个验证通过的 token 是值得被信任的。

31. 单点登录

SSO 一般都需要一个独立的认证中心(passport),子系统的登录均得通过 passport,子系统本身将不参与登录操作,当一个系统成功登录以后,passport 将会颁发一个令牌给各个子系统,子系统可以拿着令牌会获取各自的受保护资源,为了减少频繁认证,各个子系统在被 passport 授权以后,会建立一个局部会话,在一定时间内可以无需再次向 passport 发起认证。

具体流程是

  • 用户访问系统 1 的受保护资源,系统 1 发现用户未登录,跳转至 sso 认证中心,并将自己的地址作为参数
  • sso 认证中心发现用户未登录,将用户引导至登录页面
  • 用户输入用户名密码提交登录申请
  • sso 认证中心校验用户信息,创建用户与 sso 认证中心之间的会话,称为全局会话,同时创建授权令牌
  • sso 认证中心带着令牌跳转会最初的请求地址(系统 1)
  • 系统 1 拿到令牌,去 sso 认证中心校验令牌是否有效
  • sso 认证中心校验令牌,返回有效,注册系统 1
  • 系统 1 使用该令牌创建与用户的会话,称为局部会话,返回受保护资源
  • 用户访问系统 2 的受保护资源
  • 系统 2 发现用户未登录,跳转至 sso 认证中心,并将自己的地址作为参数
  • sso 认证中心发现用户已登录,跳转回系统 2 的地址,并附上令牌
  • 系统 2 拿到令牌,去 sso 认证中心校验令牌是否有效
  • sso 认证中心校验令牌,返回有效,注册系统 2
  • 系统 2 使用该令牌创建与用户的局部会话,返回受保护资源

32.网络的五层模型

从上到下分别为:应用层、传输层、网络层、数据链路层、物理层。在发送消息时,消息从上到下进行打包,每一层会在上一层基础上加包,而接受消息时,从下到上进行解包,最终得到原始信息。

其中:

  • 应用层主要面向互联网中的应用场景,比如网页、邮件、文件中心等等,它的代表协议有 http、smtp、pop3、ftp、DNS 等等
  • 传输层主要面向传输过程,比如 TCP 协议是为了保证可靠的传输,而 UDP 协议则是一种无连接的广播,它们提供了不同的传输方式
  • 网络层主要解决如何定位目标的问题,比如 IP、ICMP、ARP 等等
  • 数据链路层的作用是将数据可靠的传输到目标,比如常见的以太网协议、P2P 协议
  • 物理层是要规范网络两端使用的物理设备,比如蓝牙、wifi、光纤、网线接头等等

33.http 劫持是什么?

是指攻击者在客户端和服务器之间同时建立了连接通道,通过某种方式,让客户端请求发送到自己的服务器,然后自己就拥有了控制响应内容的能力,从而给客户端展示错误的信息。

34. HTTP 劫持、DNS 劫持与 XSS

http 劫持是指攻击者在客户端和服务器之间同时建立了连接通道,通过某种方式,让客户端请求发送到自己的服务器,然后自己就拥有了控制响应内容的能力,从而给客户端展示错误的信息,比如在页面中加入一些广告内容。

DNS 劫持是指攻击者劫持了 DNS 服务器,获得了修改 DNS 解析记录的权限,从而导致客户端请求的域名被解析到了错误的 IP 地址,攻击者通过这种方式窃取用户资料或破坏原有正常服务。

XSS 是指跨站脚本攻击。攻击者利用站点的漏洞,在表单提交时,在表单内容中加入一些恶意脚本,当其他正常用户浏览页面,而页面中刚好出现攻击者的恶意脚本时,脚本被执行,从而使得页面遭到破坏,或者用户信息被窃取。

要防范 XSS 攻击,需要在服务器端过滤脚本代码,将一些危险的元素和属性去掉或对元素进行HTML实体编码。

35.xss csrf 攻击

  • XSS:

XSS 是指跨站脚本攻击。攻击者利用站点的漏洞,在表单提交时,在表单内容中加入一些恶意脚本,当其他正常用户浏览页面,而页面中刚好出现攻击者的恶意脚本时,脚本被执行,从而使得页面遭到破坏,或者用户信息被窃取。

要防范 XSS 攻击,需要在服务器端过滤脚本代码,将一些危险的元素和属性去掉或对元素进行HTML实体编码。

  • CSRF:

CSRF 是跨站请求伪造,是一种挟制用户在当前已登录的Web应用上执行非本意的操作的攻击方法

它首先引导用户访问一个危险网站,当用户访问网站后,网站会发送请求到被攻击的站点,这次请求会携带用户的cookie发送,因此就利用了用户的身份信息完成攻击。

防御 CSRF 攻击有多种手段:

  1. 不使用cookie
  2. 为表单添加校验的 token 校验
  3. cookie中使用sameSite字段
  4. 服务器检查 referer 字段

36. https 验证身份也就是 TSL/SSL 身份验证的过程

  • 客户端请求服务器,并告诉服务器自身支持的加密算法以及密钥长度等信息
  • 服务器响应公钥和服务器证书
  • 客户端验证证书是否合法,然后生成一个会话密钥,并用服务器的公钥加密密钥,把加密的结果通过请求发送给服务器
  • 服务器使用私钥解密被加密的会话密钥并保存起来,然后使用会话密钥加密消息响应给客户端,表示自己已经准备就绪
  • 客户端使用会话密钥解密消息,知道了服务器已经准备就绪。
  • 后续客户端和服务器使用会话密钥加密信息传递消息

37. 为什么需要 CA 机构对证书签名

主要是为了解决证书的可信问题。如果没有权威机构对证书进行签名,客户端就无法知晓证书是否是伪造的,从而增加了中间人攻击的风险,https 就变得毫无意义。

38.身份验证过程中会涉及到密钥,对称加密,非对称加密,摘要的概念,请解释一下

参考答案:

  • 密钥

密钥是一种参数,它是在明文转换为密文或将密文转换为明文的算法中输入的参数。密钥分为对称密钥与非对称密钥,分别应用在对称加密和非对称加密上。

  • 对称加密

对称加密又叫做私钥加密,即信息的发送方和接收方使用同一个密钥去加密和解密数据。对称加密的特点是算法公开、加密和解密速度快,适合于对大数据量进行加密,常见的对称加密算法有 DES、3DES、TDEA、Blowfish、RC5 和 IDEA。

  • 非对称加密

非对称加密也叫做公钥加密。非对称加密与对称加密相比,其安全性更好。对称加密的通信双方使用相同的密钥,如果一方的密钥遭泄露,那么整个通信就会被破解。而非对称加密使用一对密钥,即公钥和私钥,且二者成对出现。私钥被自己保存,不能对外泄露。公钥指的是公共的密钥,任何人都可以获得该密钥。用公钥或私钥中的任何一个进行加密,用另一个进行解密。

  • 摘要

摘要算法又称哈希/散列算法。它通过一个函数,把任意长度的数据转换为一个长度固定的数据串(通常用 16 进制的字符串表示)。算法不可逆。

39.webSocket 协议是什么,能简述一下吗?

websocket 协议 HTML5 带来的新协议,相对于 http,它是一个持久连接的协议,它利用 http 协议完成握手,然后通过 TCP 连接通道发送消息,使用 websocket 协议可以实现服务器主动推送消息。

首先,客户端若要发起 websocket 连接,首先必须向服务器发送 http 请求以完成握手,请求行中的 path 需要使用ws:开头的地址,请求头中要分别加入upgrade、connection、Sec-WebSocket-Key、Sec-WebSocket-Version标记

然后,服务器收到请求后,发现这是一个 websocket 协议的握手请求,于是响应行中包含Switching Protocols,同时响应头中包含upgrade、connection、Sec-WebSocket-Accept标记

当客户端收到响应后即可完成握手,随后使用建立的 TCP 连接直接发送和接收消息。

40.webSocket 协议是什么

websocket 协议 HTML5 带来的新协议,相对于 http,它是一个持久连接的协议,它利用 http 协议完成握手,然后通过 TCP 连接通道发送消息,使用 websocket 协议可以实现服务器主动推送消息。

首先,客户端若要发起 websocket 连接,首先必须向服务器发送 http 请求以完成握手,请求行中的 path 需要使用ws:开头的地址,请求头中要分别加入upgrade、connection、Sec-WebSocket-Key、Sec-WebSocket-Version标记

然后,服务器收到请求后,发现这是一个 websocket 协议的握手请求,于是响应行中包含Switching Protocols,同时响应头中包含upgrade、connection、Sec-WebSocket-Accept标记

当客户端收到响应后即可完成握手,随后使用建立的 TCP 连接直接发送和接收消息。

42.webSocket 与传统的 http 有什么优势

当页面中需要观察实时数据的变化(比如聊天、k 线图)时,过去我们往往使用两种方式完成:

第一种是短轮询,即客户端每隔一段时间就向服务器发送消息,询问有没有新的数据

第二种是长轮询,发起一次请求询问服务器,服务器可以将该请求挂起,等到有新消息时再进行响应。响应后,客户端立即又发起一次请求,重复整个流程。

无论是哪一种方式,都暴露了 http 协议的弱点,即响应必须在请求之后发生,服务器是被动的,无法主动推送消息。而让客户端不断的发起请求又白白的占用了资源。

websocket 的出现就是为了解决这个问题,它利用 http 协议完成握手之后,就可以与服务器建立持久的连接,服务器可以在任何需要的时候,主动推送消息给客户端,这样占用的资源最少,同时实时性也最高。

43. 如何劫持 https 的请求,提供思路

https 有防篡改的特点,只要浏览器证书验证过程是正确的,很难在用户不察觉的情况下进行攻击。但若能够更改浏览器的证书验证过程,便有机会实现 https 中间人攻击。

所以,要劫持 https,首先要伪造一个证书,并且要想办法让用户信任这个证书,可以有多种方式,比如病毒、恶意软件、诱导等。一旦证书被信任后,就可以利用普通中间人攻击的方式,使用伪造的证书进行攻击。

44.前端如何实现即时通讯?

  • 短轮询,即客户端每隔一段时间就向服务器发送消息,询问有没有新的数据
  • 长轮询,发起一次请求询问服务器,服务器可以将该请求挂起,等到有新消息时再进行响应。响应后,客户端立即又发起一次请求,重复整个流程。
  • websocket,握手完毕后会建立持久性的连接通道,随后服务器可以在任何时候推送新消息给客户端

45. HTTP 常用状态码 301 302 304 403

  • 301 永久重定向,浏览器会把重定向后的地址缓存起来,将来用户再次访问原始地址时,直接引导用户访问新地址
  • 302 临时重定向,浏览器会引导用户进入新地址,但不会缓存原始地址,下一次用户访问源地址时,浏览器仍然要请求原地址的服务器
  • 304 资源未修改,服务器通过该状态码告诉客户端,请求的资源和过去一样,并没有任何变化,建议自行使用过去的缓存。通常,304 状态码的响应中,服务器不会附带任何的响应体。
  • 403 不允许访问。服务器通过该状态码告诉客户端,这个资源目前不允许访问。这种状态码通常出现在权限不足的情况下。

46. 在浏览器地址栏输入地址,并按下回车键后,发生了哪些事情?

  • 浏览器自动补全协议、端口
  • 浏览器自动完成url编码
  • 浏览器根据url地址查找本地缓存,根据缓存规则看是否命中缓存,若命中缓存则直接使用缓存,不再发出请求
  • 通过DNS解析找到服务器的IP地址
  • 浏览器向服务器发出建立TCP连接的申请,完成三次握手后,连接通道建立
  • 若使用了HTTPS协议,则还会进行SSL握手,建立加密信道。使用SSL握手时,会确定是否使用HTTP2
  • 浏览器决定要附带哪些cookie到请求头中
  • 浏览器自动设置好请求头、协议版本、cookie,发出GET请求
  • 服务器处理请求,进入后端处理流程。完成处理后,服务器响应一个HTTP报文给浏览器。
  • 浏览器根据使用的协议版本,以及Connection字段的约定,决定是否要保留TCP连接。
  • 浏览器根据响应状态码决定如何处理这一次响应
  • 浏览器根据响应头中的Content-Type字段识别响应类型,如果是text/html,则对响应体的内容进行HTML解析,否则做其他处理
  • 浏览器根据响应头的其他内容完成缓存、cookie的设置
  • 浏览器开始从上到下解析HTML,若遇到外部资源链接,则进一步请求资源
  • 解析过程中生成DOM树、CSSOM树,然后一边生成,一边把二者合并为渲染树(rendering tree),随后对渲染树中的每个节点计算位置和大小(reflow),最后把每个节点利用GPU绘制到屏幕(repaint)
  • 在解析过程中还会触发一系列的事件,当DOM树完成后会触发DOMContentLoaded事件,当所有资源加载完毕后会触发load事件

47. 列举优化网络性能方法

  1. 优化打包体积

利用一些工具压缩、混淆最终打包代码,减少包体积

  1. 多目标打包

利用一些打包插件,针对不同的浏览器打包出不同的兼容性版本,这样一来,每个版本中的兼容性代码就会大大减少,从而减少包体积

  1. 压缩

现代浏览器普遍支持压缩格式,因此服务端的各种文件可以压缩后再响应给客户端,只要解压时间小于优化的传输时间,压缩就是可行的

  1. CDN

利用 CDN 可以大幅缩减静态资源的访问时间,特别是对于公共库的访问,可以使用知名的 CDN 资源,这样可以实现跨越站点的缓存

  1. 缓存

对于除 HTML 外的所有静态资源均可以开启协商缓存,利用构建工具打包产生的文件 hash 值来置换缓存

  1. http2

开启 http2 后,利用其多路复用、头部压缩等特点,充分利用带宽传递大量的文件数据

  1. 雪碧图

对于不使用 HTTP2 的场景,可以将多个图片合并为雪碧图,以达到减少文件的目的

  1. defer、async

通过 defer 和 async 属性,可以让页面尽早加载 js 文件

  1. prefetch、preload

通过 prefetch 属性,可以让页面在空闲时预先下载其他页面可能要用到的资源

通过 preload 属性,可以让页面预先下载本页面可能要用到的资源

  1. 多个静态资源域

对于不使用 HTTP2 的场景,将相对独立的静态资源分到多个域中保存,可以让浏览器同时开启多个 TCP 连接,并行下载

48.什么是 DNS 域名解析?

DNS 域名解析是指把域名解析成 IP 地址的过程。

在具体的实现上,域名解析是由多个层级的服务器共同完成的。在查询域名时,客户端会先检查自身的 DNS 映射表,若找不到解析记录,则使用用户配置的 DNS 服务器,若目标 DNS 服务器中找不到记录,则继续往上一个层级寻找,直到到达根域名服务器,根域名服务器会根据域名的类型,将解析任务分发到对应的子域名服务器依次查找,直到找到解析记录为止。

JavaScript

1.数据类型(8种)

基本数据类型引用数据类型
Undefined、Null、Boolean、String、Number、Symbol(ES6)、BigInt(ES6)Object(包括数组、函数、对象等)
  • Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题
  • BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

2.数据类型检测

优缺点typeofinstanceofconstructorObject.prototype.toString.call
优点使用简单能检测出引用类型数据基本能检测所有的类型(除了null和undefined)检测出所有的类型
缺点只能检测出除null外的基本数据类型和引用数据类型中的function不能检测出基本类型,且不能跨iframeconstructor易被修改,也不能跨iframeIE6下,undefined和null均为Object
  1. typeof

typeof可以区分除了Null类型以外的其他基本数据类型,以及从对象类型中识别出函数(function)。

其返回值有:numberstringbooleanundefinedsymbolbigintfunctionobject

其中, typeof null返回 "object"

如果要识别null,可直接使用===全等运算符来判断。

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

2.instanceof

instanceof一般是用来判断引用数据类型,但不能正确判断基本数据类型,根据在原型链中查找判断当前数据的原型对象是否存在返回布尔类型。

1 instanceof Number; // false
true instanceof Boolean; // false
'str' instanceof String; // false
[] instanceof Array; // true
function(){} instanceof Function; // true
{} instanceof Object; // true
let date = new Date();
date instance of Date; // true

3.Object.prototype.toString

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]"

4.Array.isArray

Array.isArray(value)可以判断 value 是否为数组。

Array.isArray([]); // true
Array.isArray({}); // false
Array.isArray(1); // false
Array.isArray('string'); // false
Array.isArray(true); // false

3.判断数组的方式

Object.prototype.toString.call([1, 2, 3]) // [object Array]

通过ES6的Array.isArray([1, 2, 3])做判断 // true or false

[1, 2, 3] instanceof Array // true or false

Array.prototype.isPrototypeOf([1, 2, 3]) // true or false

通过原型链去判断:[1, 2, 3].__proto__ === Array.prototype

4.数组的遍历方法

方法改变原数组特点
forEach无返回值
map返回新数组,可链式调用
filter过滤数组,返回包含符合条件的元素的数组,可链式调用
for…offor…of遍历具有Iterator迭代器的对象的属性,返回的是数组的元素、对象的属性值,不能遍历普通的obj对象,将异步循环变成同步循环
every遍历的数组里的元素全部符合条件时,返回true
some遍历的数组里的元素至少有一个符合条件时,返回true
find返回第一个符合条件的值
findIndex返回第一个返回条件的值的索引值
reduce对数组正序操作
reduceRight对数组逆序操作

5.Undefined与Null

解释一:
  • Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null
  • undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始化
  • undefined 在 JavaScript 中不是一个保留字,这意味着可以使用 undefined 来作为一个变量名,但是这样的做法是非常危险的,它会影响对 undefined 值的判断。我们可以通过一些方法获得安全的 undefined 值,比如说 void 0
  • typeof null 的返回值为object,且undefined == null返回true
解释二:

null是一个表示"无"的对象,转为数值时为0;undefined是一个表示"无"的原始值,转为数值时为NaN。

当声明的变量还未被初始化时,变量的默认值为undefined。 null用来表示尚未存在的对象

undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。典型用法是:

(1)变量被声明了,但没有赋值时,就等于undefined。

(2)调用函数时,应该提供的参数没有提供,该参数等于undefined。

(3)对象没有赋值的属性,该属性的值为undefined。

(4)函数没有返回值时,默认返回undefined。

null表示"没有对象",即该处不应该有值。典型用法是:

(1) 作为函数的参数,表示该函数的参数不是对象。

(2) 作为对象原型链的终点。

6.this的指向问题

this是一个在运行时才进行绑定的引用,在不同的情况下它可能会被绑定不同的对象

this 永远指向最后调用它的那个对象

  • 函数调用模式:当一个函数不是一个对象的属性,直接作为函数来调用时,this指向全局对象
  • 方法调用模式:当一个函数作为一个对象的方法来调用时,this指向这个对象
  • 构造器调用模式:如果一个函数使用new调用时,函数在执行前会创建一个新的对象,this就指向这个新的对象

如何改变this的指向?

  • 使用ES6箭头函数,箭头函数不绑定this,箭头函数的this使用指向函数定义时的this
  • 在函数内部定义一个变量_this保存this
  • 使用 apply、call、bind
  • new 实例化一个对象

this绑定的优先级:new绑定优先级 > 显示绑定优先级 > 隐式绑定优先级 > 默认绑定优先级

7.apply、bind和call

type:1

apply、bind和call都可以改变this的指向

apply(thisArg[, argsArray])

apply() 方法调用一个具有给定 this 值的函数,以及以一个数组(或一个类数组对象)的形式提供的参数

thisArg: 在函数运行时使用的 this 值。请注意,this 可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 nullundefined 时会自动替换为指向全局对象,原始值会被包装

argsArray: 可选。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 函数。如果该参数的值为 nullundefined,则表示不需要传入任何参数

返回值:调用有指定 this 值和参数的函数的结果

const numbers = [1, 3, 2, 5, 7, 4]
const max = Math.max.apply(null, numbers)   // 7
const min = Math.max.apply(null, numbers)   // 1

call(thisArg[, arg1[, arg2[, …]]])

call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数
该方法的语法和作用与 apply() 方法类似,只有一个区别,就是 call() 方法接受的是一个参数列表,而 apply() 方法接受的是一个包含多个参数的数组

thisArg: 在函数运行时使用的 this 值。请注意,this 可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 nullundefined 时会自动替换为指向全局对象,原始值会被包装

arg1, arg2, …: 指定的参数列表

返回值:调用有指定 this 值和参数的函数的结果

bind(thisArg[, arg1[, arg2[, …]]])

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用(bind 是创建一个新的函数,我们必须要手动去调用)

thisArg: 调用绑定函数时作为 this 参数传递给目标函数的值。如果使用new运算符构造绑定函数,则忽略该值。当使用 bindsetTimeout 中创建一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 object。如果 bind 函数的参数列表为空,或者thisArgnullundefined,执行作用域的 this 将被视为新函数的 thisArg

arg1, arg2, …: 指定的参数列表

返回值:返回一个原函数的拷贝,并拥有指定的 this 值和初始参数

type:2
  • call()可以传递两个参数,第一个参数是指定函数内部中this的指向,第二个参数是函数调用时需要传递的参数。改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次。
// 实现call方法
Function.prototype.myCall = function (context) {
  // 判断调用对象
  if (typeof this != "function") {
    throw new Error("type error");
  }
  // 首先获取参数
  let args = [...arguments].slice(1);
  let res = null;
  // 判断context是否传入,如果没有,就设置为window
  context = context || window;
  // 将被调用的方法置入context的属性
  // this 即为要调用的方法
  context.fn = this;
  // 执行要被调用的方法
  res = context.fn(...args);
  // 删除手动增加的属性方法
  delete context.fn;
  // 执行结果返回
  return res;
}
  • apply()接受两个参数,第一个参数是this的指向,第二个参数是函数接受的参数,以数组的形式传入。改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次。
// 实现apply方法
Function.prototype.myApply = function(context) {
  if (typeof this != "function") {
    throw new Error("type error");
  }
  let res = null;
  context = context || window;
  // 使用 symbol 来保证属性唯一
  // 也就是保证不会重写用户自己原来定义在context中的同名属性
  const fnSymbol = Symbol();
  context[fnSymbol] = this;
  // 执行被调用的方法
  if (arguments[1]) {
    res = context[fnSymbol](...arguments[1]);
  } else {
    res = context[fnSymbol]();
  }
  delete context[fnSymbol];
  return res;
}
  • bind()方法的第一参数也是this的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入)。改变this指向后不会立即执行,而是返回一个永久改变this指向的函数。
// 实现bind方法
Function.prototype.myBind = function (context) {
  if (typeof this != "function") {
    throw new Error("type error");
  }
  let args = [...arguments].slice(1);
  const fn = this;
  return function Fn() {
    return fn.apply(
      this instanceof Fn ? this : context,
      // 当前这个 arguments 是指 Fn 的参数
      args.concat(...arguments)
    );
  };
}

8.伪数组(类数组)

一个拥有length属性和若干索引属性的对象可以被成为类数组对象,类数组对象和数组类似,但不能调用数组的方法
常见的类数组对象:arguments和DOM方法的返回结果,还有一个函数也可以被看作是类数组对象,因为它含有 length 属性值,代表可接收的参数个数

9.类数组如何转换为数组

  • 通过call方法调用数组的slice方法
Array.prototype.slice.call(arrayLike);
  • 通过call方法调用数组的splice方法
Array.prototype.splice.call(arrayLike, 0);
  • 通过apply调用数组的concat方法
Array.prototype.concat.apply([], arrayLike);
  • 通过Array.from方法
Array.from(arrayLike);
  • 通过展开运算符
const array = [...arrayLike]

10.如何遍历类数组

arguments是一个对象,它的属性是从 0 开始依次递增的数字,还有calleelength等属性,与数组相似;但是它却没有数组常见的方法属性,如forEach, reduce等,所以叫它们类数组

  • 使用call或apply方法
function sum() {
    Array.prototype.forEach.call(arguements, a => { console.log(a) })
}

function sum() {
    Array.prototype.forEach.apply(arguements, [a => { console.log(a)] })
}
  • 使用Array.from方法将类数组转化成数组
function sum() {
    const args = Array.from(arguements)
    args.forEach(a => { console.log(a) })
}
  • 使用展开运算符将类数组转成数组
function sum() {
    const args = [...arguements]
    args.forEach(a => { console.log(a) })
}

11.for…in与for…of

区别for…infor…of
遍历对象对象的键名,会遍历整个原型链,性能差对象的键值,只遍历当前对象
遍历数组返回数组中所有可枚举属性,包括原型链上的属性只返回对应数组的下标对应的属性值

for…in循环主要是为了遍历对象,不适用于遍历数组,for…of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象

12.forEach、map和for循环

forEach
  1. 没有返回值;
js var a = [1,2,3,4,5] var b = a.forEach((item) => { item = item * 2 }) console.log(b) // undefined
  1. 无法中断执行;
  2. 可以使用return跳过当前循环;
  3. 跳过数组的空位,但不会跳过nullundefined
js var a = [null, , undefined] a.forEach(item => { console.log('item', item) // null undefined })
  1. 改变数组情况;
  2. 为什么直接修改item无法修改原数组呢,因为item的值并不是相应的原数组中的值,而是重新建立的一个新变量,值和原数组相同。因此,如果item是基础数据类型,那么并不会改变数组里面的值,如果是引用类型,那么item和数组里面的值是指向同一内存地址,则都会被改变。
js var a = [1,2,3,4,5] a.forEach((item) => { item = item * 2 }) console.log(a) // [1,2,3,4,5]
  • 数组中的对象的值也没有改变,是因为新创建的变量和原数组中的对象虽然指向同一个地址,但改变的是新变量的值,也就是重新赋值,即新对象的值为2,原数组中的对象还是{num:1}
js var a = [1,'1',{num:1},true] a.forEach((item, index, arr) => { item = 2 }) console.log(a) // [1,'1',{num:1},true]
  • 由于对象是引用类型,新对象和旧对象指向的都是同一个地址,所以新对象把num变成了2,原数组中的对象也改变了。
js var a = [1,'1',{num:1},true] a.forEach((item, index, arr) => { item.num = 2 item = 2 }) console.log(a) // [1,'1',{num:2},true]
  1. 手写forEach方法;
js Array.prototype.new_forEach = function(callback) { for (let i = 0; i < this.length; i++) { callback(this[i], i, this) } }
map
  1. 有返回值;
js var a = [1,2,3,4,5] var b = a.map((item) => { return item = item * 2 }) console.log(a) // [1,2,3,4,5] console.log(b) // [2,4,6,8,10]
  1. 无法中断执行,同forEach;
  2. 可以使用return跳过当前循环,同forEach;
  3. 跳过数组的空位,但不会跳过nullundefined,同forEach;
  4. 改变数组情况,同forEach;
  5. 手写map方法;
js Array.prototype.new_map = function(callback) { const res = [] for (let i = 0; i < this.length; i++) { res.push(callback(this[i], i, this)) } return res }
for循环
  1. for循环是个语句,forEach和map则是表达式;
  2. for循环可以使用break结束循环;
  3. for循环可以使用continue语句跳过当前循环;
  4. for循环不会跳过数组的空位,会默认空位为undefined;
性能对比
  1. for 循环当然是最简单的,因为它没有任何额外的函数调用栈和上下文;
  2. forEach 其次,因为它其实比我们想象得要复杂一些,它的函数签名实际上是array.forEach(function(currentValue, index, arr), thisValue)它不是普通的 for 循环的语法糖,还有诸多参数和上下文需要在执行的时候考虑进来,这里可能拖慢性能;
  3. map 最慢,因为它的返回值是一个等长的全新的数组,数组创建和赋值产生的性能开销很大。

13.forEach如何跳出循环

forEach是不能通过break或者return来实现跳出循环的,forEach的回调函数形成了一个作用域,在里面使用return并不会跳出,只会被当做continue

实现方法:try…catch

function getItemById(arr, id) {
    var item = null;
    try {
        arr.forEach(function (curItem, i) {
            if (curItem.id == id) {
                item = curItem;
                throw Error();
            }
        })
    } catch (e) {}
    return item;
}

14.深拷贝

深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象(新旧对象不共享同一块内存),且修改新对象不会影响原来的对象(深拷贝采用了在堆内存中申请新的空间来存储数据,这样每个可以避免指针悬挂)

实现方式如下:

JSON.parse(JSON.stringify())

这也是利用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则,因为这两者基于JSON.stringify和JSON.parse处理后,得到的正则就不再是正则(变为空对象),得到的函数就不再是函数(变为null)了

lodash的_.cloneDeep

需要安装lodash

jQuery.extend()
手写递归循环

递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null) return obj // 如果是null或者undefined我就不进行拷贝操作
  if (obj instanceof Date) return new Date(obj)
  if (obj instanceof RegExp) return new RegExp(obj)
  // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝
  if (typeof obj !== "object") return obj
  // 是对象的话就要进行深拷贝
  if (hash.get(obj)) return hash.get(obj)
  let cloneObj = new obj.constructor()
  // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
  hash.set(obj, cloneObj)
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 实现一个递归拷贝
      cloneObj[key] = deepClone(obj[key], hash)
    }
  }
  return cloneObj
}

15.浅拷贝

浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象

实现方式如下:

Object.assign()

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象

let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' }
let obj2 = Object.assign({}, obj1);
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }
lodash的_.clone

需要安装lodash

展开运算符

展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()的功能相同

let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}
obj1.address.x = 200;
obj1.name = 'wade'
console.log('obj2',obj2) // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }
Array.prototype.concat()
let arr = [1, 3, {
    username: 'kobe'
}];
let arr2 = arr.concat();    
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]
Array.prototype.slice()
let arr = [1, 3, {
    username: ' kobe'
}];
let arr3 = arr.slice();
arr3[2].username = 'wade'
console.log(arr); // [ 1, 3, { username: 'wade' } ]

16.赋值与深/浅拷贝的区别

对于引用数据类型

赋值深拷贝浅拷贝
当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。
和原数据是否指向同一对象第一层数据为基本数据类型且修改基本类型数据时原数据中包含子对象且修改子对象时
赋值改变会使原数据一起改变改变会使原数据一起改变
深拷贝改变不会使原数据一起改变改变不会使原数据一起改变
浅拷贝改变不会使原数据一起改变改变会使原数据一起改变

17.闭包

闭包是指有权访问另一个函数作用域中的变量的函数
闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成

用途
  • 使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量
  • 使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收
使用场景
  • return 回一个函数
  • 函数作为参数
  • 自动执行函数
  • 循环赋值
  • 回调函数
  • 节流防抖
  • 函数柯里化
执行过程
  1. 形成私有上下文
  2. 进栈执行
  3. 开始一系列操作
  4. 初始化作用域链(两头<当前作用域,上级作用域>)
  5. 初始化this
  6. 初始化arguments
  7. 赋值形参
  8. 变量提升
  9. 代码执行
  10. 正常情况下,代码执行完成之后,私有上下文出栈被回收。但是遇到特殊情况,如果当前私有上下文执行完成之后中的某个东西被执行上下文以外的东西占用,则当前私有上下文就不会出栈释放,也就是形成了不被销毁的上下文,闭包
注意事项

容易导致内存泄漏。闭包会携带包含其它的函数作用域,因此会比其他函数占用更多的内存。过度使用闭包会导致内存占用过多,所以要谨慎使用闭包。

18.作用域Scope

作用域是在运行时代码中的某些特定部分中变量、函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

全局作用域
  • 直接写在script标签的JS代码,都在全局作用域。在全局作用域下声明的变量叫做全局变量(在块级外部定义的变量)
  • 所有末定义直接赋值的变量自动声明为拥有全局作用域
  • 全局变量在全局的任何位置下都可以使用;全局作用域中无法访问到局部作用域的中的变量
  • 全局作用域在页面打开的时候创建,在页面关闭时销毁
  • 所有 window 对象的属性拥有全局作用域

var和function命令声明的全局变量和函数是window对象的属性和方法
let命令、const命令、class命令声明的全局变量,不属于window对象的属性

值得注意的是,块语句(大括号之间的语句,如if语句、switch语句、for循环语句、while语句)不会创建一个新的作用域,在块语句中定义的变量将保留在它们存在的作用域中

函数作用域(局部作用域)
  • 调用函数时会创建函数作用域,函数执行完毕之后,作用域销毁。每调用一次函数就会创建一个新的函数作用域,他们之间是相互独立的
  • 在函数作用域中可以访问全局变量,在全局作用域中一般情况下无法访问函数内的变量(可以通过闭包访问)
  • 在函数作用域中操作一个变量时,它会先在自身作用域内寻找,如果有就直接使用,如果没有就向上一级作用域中寻找,知道找到全局作用域中。如果全局作用域中仍未找到,则报错
块级作用域

块级作用域可通过新增命令let和const声明,所声明的变量在指定的块级作用域外无法被访问,块级作用域在如下情况被创建:

  • 在一个函数内部
  • 在一个代码块(由一对花括号包裹)内部

let 声明的语法与 var 的语法一致。基本上可以用 let 来代替 var 进行变量声明,但会将变量的作用域限制在当前代码块中。块级作用域有以下几个特点:

  • 声明变量不会提升到代码块顶部
  • 禁止重复声明

19.作用域链

在某个作用域内访问一个变量时,会先在当前作用域内寻找,如果没有找到,则去上一级作用域内寻找,以此类推。这样的变量作用域访问的链式结构,被称为作用域链

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

`作用域链`:当在 JS 中使用一个变量时,JS 引擎会尝试在当前作用域下寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推,直至找到该变量或是查找至全局作用域,如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错。

20.预解析(变量提升)

JS 引擎在执行一段代码的时候,会按照下面的步骤进行工作

  • 把变量的声明提升到当前作用域的最前面,只会提升声明,不会提升赋值
  • 吧函数的声明提升到当前作用域的最前面,只会提升声明,不会提升调用
  • 先提升function,再提升var声明的变量

区别

  • JavaScript 代码执行前引擎会先进行预编译,预编译期间会将变量声明与函数声明提升至其对应作用域的最顶端,函数内声明的变量只会提升至该函数作用域最顶层。当函数内部定义的一个变量与外部相同时,那么函数体内的这个变量就会被上升到最顶端
  • 函数提升只会提升函数声明式写法,函数表达式的写法不存在函数提升
  • 函数提升的优先级大于变量提升的优先级,即函数提升在变量提升之上

21.内存泄露

内存泄露是指由于疏忽或错误造成程序未能释放已经不再使用的内存

内存泄露的原因有以下几种:

  • 意外的全局变量:由于使用为声明的变量,而意外的创建了一个变量,这个变量将一直留在内存中无法被回收
  • 被遗忘的定时器或回调函数:设置了setInterval定时器而忘记销毁,如果循环函数有对外部的引用的话,这个变量将一直被留在内存中无法被回收
  • 脱离DOM的引用:获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收
  • 闭包:不合理的使用闭包,从而导致某些变量一直被留在内存当中

22.函数式编程的优缺点

优点缺点
降低维护成本过渡包装会导致性能开销
代码的复用性更强资源占用更强
组合起来更加优雅为了实现迭代,可能会掉入递归陷阱

23.纯函数

纯函数是对给定的输入返还相同的输出的函数,并且要求所有的数据都是不可变的

特性

  • 函数内部传入指定的值,就会返回唯一确定的值
  • 不会造成超出作用域的变化,例如修改全局变量或引用传递的参数

优势

  • 通过纯函数可以产生可测试的代码
  • 不依赖外部环境计算,不会产生副作用,复用性高
  • 可读性高,不管是不是纯函数,都会有一个语义化的名称,便于阅读
  • 符合模块化概念及单一职责原则

24.高阶函数

高阶函数是指使用其它函数作为参数、或者返回一个函数作为返回值的函数

常见的高阶函数

  • Array.prototype.map
  • Array.prototype.filter
  • Array.prototype.forEach
  • Array.prototype.reduce

25.函数柯里化

柯里化(Currying)又叫函数的部分求值,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术

优点

  • 参数复用:需要输入多个参数,最终只需输入一个,其余通过arguments对象获取
  • 提前确认:避免重复判断某一条件是否符合,不符合则return
  • 延迟运行:避免重复执行程序,等真正需要结果的时候再执行
function curry(fn, args) {
    args = args || []
    var arity = fn.length

    return function() {
        var _args = Array.prototype.slice.call(arguments)
        Array.prototype.unshift.call(_args, ...args)
        _args = _args.concat(args)

        if (_args.length < arity) {
            return currying.call(null, fn, _args)
        }

        return fn.apply(null, _args)
    }
}

26.箭头函数

type:1

ES6中允许使用“箭头”(=>) 来定义函数。箭头函数相当于匿名函数,并且简化了函数定义

特点

  • 箭头函数不绑定this,箭头函数里的this永远指向定义箭头函数时所处的作用域
  • 箭头函数的this永远不会变,call、apply、bind也无法改变
  • 箭头函数只能声明成匿名函数,但可以通过表达式的方式让箭头函数具名
  • 箭头函数没有原型prototype
  • 因为this的指向问题,箭头函数不能作为构造函数使用
  • 箭头函数没有 arguments 在箭头函数内部访问这个变量访问的是外部环境的arguments, 可以使用 …代替
type:2

箭头函数是定义函数一种新的方式,比普通函数定义更加方便和简单。

箭头函数不绑定this,会捕获其所在上下文的this,作为自己的this

箭头函数不能用作构造函数,也就是说不可以使用new命令,否则会抛出错误。

箭头函数不绑定arguments,取而代之用rest参数解决,同时没有supernew.target

使用callapplybind并不会改变箭头函数中的this指向。对箭头函数使用callapply方法时,只会传入参数并调用函数;对箭头函数使用bind方法时,只会返回一个预设参数的新函数,并不会改变这个新函数的this指向。

27.防抖

当事件触发时,相应的函数不会立即触发,而是等待一段时间;
当事件连续触发时,函数的触发等待时间会被不断重置(推迟)。

通俗的讲,防抖就是,每次触发事件时,在一段时间后才真正响应这个事件,具体应用如下:

  • 输入框中频繁输入内容,如果输入框改变一次就发送一次请求的话,会对服务器造成很大的压力,所以我们希望在连续输入的时候不发送请求,直到用户输入完或者一段时间没有继续输入的话才发送请求;
  • 频繁点击按钮触发事件(恶意的行为)
  • 用户缩放浏览器时频繁触发resize事件
  • 王者荣耀回城
如何实现防抖函数
function debounce(callback, time) {
    let timer
    return function() {
        clearTimeout(timer)
        let args = arguments
        timer = setTimeout(() => {
            callback.apply(this, args)
        }, time)
    }
}

28.节流

如果事件被频繁出发,那么节流函数会按照一定的频率来执行函数;
不管中间触发了多少次,执行函数的频率总是固定的

说白了节流就是在间隔一段时间执行一次,具体应用如下:

  • 王者荣耀冷却中的技能无法再次释放;
  • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断;
  • 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)。
如何实现节流函数
function throttle(func, delay) {
    let timer;
    return function() {
        let args = arguments;
        if (!timer) {
            timer = setTimeout(() => {
                timer = null;
                func.apply(this, args)
            }, delay)
        }
    }
}

29.添加 删除 替换 插入到某个接点的方法

1)创建新节点

 createElement() //创建一个具体的元素
 createTextNode() //创建一个文本节点

2)添加、移除、替换、插入

 appendChild() //添加
 removeChild() //移除
 replaceChild() //替换
 insertBefore() //插入

3)查找

 getElementsByTagName() //通过标签名称
 getElementsByName() //通过元素的Name属性的值
 getElementById() //通过元素Id,唯一性

30.document load 和document ready的区别

document.onload 是在结构和样式,外部js以及图片加载完才执行js
document.ready是dom树创建完成就执行的方法,原生种没有这个方法,jquery中有 $().ready(function)

31. ””和“=”的不同

前者会自动转换类型,再判断是否相等
后者不会自动类型转换,直接去比较

32.Javascript的事件流模型都有什么?

“事件冒泡”:事件开始由最具体的元素接受,然后逐级向上传播

“事件捕捉”:事件由最不具体的节点先接收,然后逐级向下,一直到最具体的

“DOM事件流”:三个阶段:事件捕捉,目标阶段,事件冒泡

33.new操作符具体干了什么呢?

1、创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。

2、属性和方法被加入到 this 引用的对象中。

3、新创建的对象由 this 所引用,并且最后隐式的返回 this 。

34.解释什么是Json:

(1) JSON 是一种轻量级的数据交换格式。

(2) JSON 独立于语言和平台,JSON 解析器和 JSON 库支持许多不同的编程语言。

(3) JSON的语法表示三种类型值,简单值(字符串,数值,布尔值,null),数组,对象

35.可视区的大小:

(1)innerXXX(不兼容ie)

window.innerHeight 可视区高度,包含滚动条宽度

window.innerWidth 可视区宽度,包含滚动条宽度

(2)document.documentElement.clientXXX(兼容ie)

document.documentElement.clientWidth可视区宽度,不包含滚动条宽度

document.documentElement.clientHeight可视区高度,不包含滚动条宽度

36.innerHTML和outerHTML的区别

innerHTML(元素内包含的内容)

outerHTML(自己以及元素内的内容)

37. offsetWidth offsetHeight和clientWidth clientHeight的区别

(1)offsetWidth (content宽度+padding宽度+border宽度)

(2)offsetHeight(content高度+padding高度+border高度)

(3)clientWidth(content宽度+padding宽度)

(4)clientHeight(content高度+padding高度)

38.冒泡排序算法

冒泡排序:
var array = [5, 4, 3, 2, 1];
var temp = 0;
for (var i = 0; i <array.length; i++){
	for (var j = 0; j <array.length - i; j++){
		if (array[j] > array[j + 1]){
			temp = array[j + 1];
			array[j + 1] = array[j];
			array[j] = temp;
        }
    }
}

39.js 实现一个函数 获得url参数的值

function getQueryString(name) { 
  var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i"); 
  var r = window.location.search.substr(1).match(reg); 
  if (r != null) return unescape(r[2]); return null; 
} 

40.如何遍历对象的属性?

  • 遍历自身可枚举的属性(可枚举、非继承属性)Object.keys() 方法,该方法会返回一个由给定对象的自身可枚举属性组成的数组。
  • 遍历自身的所有属性(可枚举、不可枚举、非继承属性)Object.getOwnPropertyNames()方法,该方法会返回一个由指定对象的所有自身属性组成的数组
  • 遍历可枚举的自身属性和继承属性for ... in ...

41.如何判断两个对象是否相等?

  1. Object.is(obj1, obj2),判断两个对象都引用地址是否一致,true 则一致,false 不一致。

  2. 判断两个对象内容是否一致,思路是遍历对象的所有键名和键值是否都一致

    ① 判断两个对象是否指向同一内存
    ② 使用 Object.getOwnPropertyNames 获取对象所有键名数组
    ③ 判断两个对象的键名数组是否相等
    ④ 遍历键名,判断键值是否都相等

function isObjValueEqual(a, b) {
  // 判断两个对象是否指向同一内存,指向同一内存返回 true
  if (a === b) return true;
  // 获取两个对象的键名数组
  let aProps = Object.getOwnPropertyNames(a);
  let bProps = Object.getOwnPropertyNames(b);
  // 判断两键名数组长度是否一致,不一致返回 false
  if (aProps.length !== bProps.length) return false;
  // 遍历对象的键值
  for (let prop in a) {
    // 判断 a 的键名,在 b 中是否存在,不存在,直接返回 false
    if (b.hasOwnProperty(prop)) {
      // 判断 a 的键值是否为对象,是对象的话需要递归;
      // 不是对象,直接判断键值是否相等,不相等则返回 false
      if (typeof a[prop] === 'object') {
        if (!isObjValueEqual(a[prop], b[prop])) return false;
      } else if (a[prop] !== b[prop]){
        return false
      }
    } else {
      return false
    }
  }
  return true;
}

42.强制类型转换和隐式类型转换有哪些

  • 强制:
    转换成字符串: toString()、String()
    转换成数字:Number()、parseInt()、parseFloat()
    转换成布尔类型:Boolean()
  • 隐式:
    拼接字符串:let str = 1 + “”;

43.Array.from() 和 Array.of() 的使用及区别?

Array.from():将伪数组对象或可遍历对象转换为真数组。接受三个参数:input、map、context。input:待转换的伪数组对象或可遍历对象;map:类似于数组的 map 方法,用来对每个元素进行处理,将处理后的值放入返回的数组;context:绑定map中用到的 this。

Array.of():将一系列值转换成数组,会创建一个包含所有传入参数的数组,而不管参数的数量与类型,解决了new Array()行为不统一的问题。

44.EventLoop 事件循环?

js 是单线程运行的,当遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列–事件队列(Task Queue)。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码…,如此反复,这样就形成了一个无限的循环,这个过程被称为事件循环(Event Loop)

实际上,异步任务之间并不相同,它们的执行优先级也有区别。异步任务分两类:微任务(micro task)和宏任务(macro task)

微任务包括: promise 的回调、node 中的 process.nextTick 、对 Dom 变化监听的 MutationObserver

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

在一个事件循环中,异步事件返回结果后会被放到一个事件队列中。然而,根据这个异步事件的类型,这个事件实际上会被对应的宏任务队列或者微任务队列中去。并且在当前执行栈为空的时候,主线程会查看微任务队列是否有事件存在。如果不存在,那么再去宏任务队列中取出一个事件并把对应的回调加入当前执行栈;如果存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈…如此反复,进入循环。

在当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行

TypeScrip

1、TypeScript和JavaScript的区别是什么?

Typescript 是 JavaScript 的超集,可以被编译成 JavaScript 代码。用 JavaScript 编写的代码,在 TypeScript 中依然有效。Typescript 是纯面向对象的编程语言,包含类和接口的概念。 程序员可以用它来编写面向对象的服务端或客户端程序,并将它们编译成 JavaScript 代码。

2、为什么要使用 TypeScript ? TypeScript 相对于 JavaScript 的优势是什么?

增加了静态类型,可以在开发人员编写脚本时检测错误,使得代码质量更好,更健壮。

优势:

  • 杜绝手误导致的变量名写错;
  • 类型可以一定程度上充当文档;
  • IDE自动填充,自动联想;

3、TypeScript 中 any 类型的作用是什么?

为编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。

4、TypeScript 中 any、never、unknown、null & undefined 和 void 有什么区别?

any: 动态的变量类型(失去了类型检查的作用)。

never: 永不存在的值的类型。例如:never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。

unknown: 任何类型的值都可以赋给 unknown 类型,但是 unknown类型的值只能赋给 unknown 本身和 any 类型。

null & undefined: 默认情况下 null 和 undefined 是所有类型的子类型。 就是说你可以把 null 和 undefined 赋值给 number 类型的变量。当你指定了 --strictNullChecks 标记,null 和 undefined 只能赋值给 void 和它们各自。

void: 没有任何类型。例如:一个函数如果没有返回值,那么返回值可以定义为void。

5、 TypeScript 中的 this 和 JavaScript 中的 this 有什么差异?

TypeScript:noImplicitThis: true 的情况下,必须去声明 this 的类型,才能在函数或者对象中使用this。

Typescript 中[箭头函数]的 this 和 ES6 中箭头函数中的 this 是一致的。

6、TypeScript 中使用 Union Types 时有哪些注意事项?

属性或方法访问: 当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法。

function getLength(something: string | number): number {
   return something.length;
}
// index.ts(2,22): error TS2339: Property 'length' does not exist on type >'string | number'.
//   Property 'length' does not exist on type 'number'.

function getString(something: string | number): string {
   return something.toString();
}
// 公共方法和属性可以访问

7、TypeScript 如何设计 Class 的声明?

class Greeter {
   greeting: string;
   constructor(message: string) {
       this.greeting = message;
   }
   greet(): string{
       return "Hello, " + this.greeting;
   }
}
let greeter = new Greeter("world");
// 在声明类的时候,一般类中都会包含,构造函数、对构造函数中的属性进行类型声明、类中的方法。

8、TypeScript 中如何联合枚举类型的 Key?

enum str {
   A,
   B,
   C
}
type strUnion =  keyof typeof str; // 'A' | 'B' | 'C'

9、TypeScript 中 type 和 interface 的区别?

相同点:

  1. 都可以描述 ‘对象’ 或者 ‘函数’
  2. 都允许拓展(extends)

不同点:

  1. type 可以声明基本类型,联合类型,元组
  2. type 可以使用 typeof 获取实例的类型进行赋值
  3. 多个相同的 interface 声明可以自动合并

使用 interface 描述‘数据结构’,使用 type 描述‘类型关系’

10、简单介绍一下 TypeScript 模块的加载机制?

假设有一个导入语句 import { a } from "moduleA";

  1. 首先,编译器会尝试定位需要导入的模块文件,通过绝对或者相对的路径查找方式;
  2. 如果上面的解析失败了,没有查找到对应的模块,编译器会尝试定位一个外部模块声明(.d.ts);
  3. 最后,如果编译器还是不能解析这个模块,则会抛出一个错误 error TS2307: Cannot find module 'moduleA'.

11、TypeScript的内置数据类型有哪些?

// 数字类型:用于表示数字类型的值。TypeScript 中的所有数字都存储为浮点值。
let num: number = 1;

// 字符类型: 用于表示字符串类型的值
let str: string = "CoderBin";

// 布尔类型:一个逻辑二进制开关,包含true或false
let flag: boolean = true

// void 类型:分配给没有返回值的方法的类型。
let unusable: void = undefined;

12、TypeScript中的模块是什么?

TypeScript 中的模块是相关变量、函数、类和接口的集合。 你可以将模块视为包含执行任务所需的一切的容器。可以导入模块以轻松地在项目之间共享代码。

module module_name{
  class xyz{
    export sum(x, y){
      return x+y;
    }
  }
}

13、TS中什么是方法重载?

方法重载是指在一个类中定义多个同名的方法,但要求每个方法具有不同的参数的类型或参数的个数。 基本上,它在派生类或子类中重新定义了基类方法。

方法覆盖规则:

  • 该方法必须与父类中的名称相同。
  • 它必须具有与父类相同的参数。
  • 必须存在IS-A关系或继承。

14、如何在TS中实现继承?

继承是一种从另一个类获取一个类的属性和行为的机制。它是面向对象编程的一个重要方面,并且具有从现有类创建新类的能力,继承成员的类称为基类,继承这些成员的类称为派生类。

继承可以通过使用extend关键字来实现。我们可以通过下面的例子来理解它。

class Shape {     
  Area:number     
  constructor(area:number) {     
     this.Area = area    
  }     
}     
class Circle extends Shape {     
  display():void {     
     console.log("圆的面积: "+this.Area)     
  }     
}    
var obj = new Circle(320);     
obj.display() 

15、TS中的泛型是什么?

TypeScript Generics是提供创建可重用组件的方法的工具。 它能够创建可以使用多种数据类型而不是单一数据类型的组件。 而且,它在不影响性能或生产率的情况下提供了类型安全性。 泛型允许我们创建泛型类,泛型函数,泛型方法和泛型接口。

在泛型中,类型参数写在左括号(<)和右括号(>)之间,这使它成为强类型集合。 它使用一种特殊的类型变量来表示类型

function identity<T>(arg: T): T {
  return arg;
}
let output1 = identity<string>("CoderBin");
let output2 = identity<number>( 117 );
console.log(output1);
console.log(output2);

16、解释如何使用TypeScript mixin

Mixin 本质上是在相反方向上工作的继承。Mixins 允许你通过组合以前类中更简单的部分类来设置构建新类。

相反,类A继承类B来获得它的功能,类B从类A需要返回一个新类的附加功能。

17、 TS中的类型有哪些?

类型系统表示语言支持的不同类型的值。它在程序存储或操作所提供的值之前检查其有效性。

它可以分为两种类型,

  • 内置:包括数字(number),字符串(string),布尔值(boolean),无效(void),空值(null)和未定义(undefined)。
  • 用户定义的:它包括枚举(enums),类(classes),接口(interfaces),数组(arrays)和元组(tuple)。

18、TypeScript中const和readonly的区别是什么?

const用于变量,readonly用于属性

const在运行时检查,readonly在编译时检查

使用const变量保存的数组,可以使用push,pop等方法。但是如果使用Readonly Array声明的数组不能使用push,pop等方法

vue2

1.Vue的基本原理

当一个Vue实例创建时,Vue会遍历data中的属性,用Object.defineProperty将它们转为getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。每个组件实例都有相应的watcher程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而使它关联的组件得以更新

2.响应式原理

数据劫持

利用Object.defineProperty劫持对象的访问器,在属性发生变化时我们可以获取变化,从而进行下一步操作

发布者模式和订阅者模式

在软件架构中,发布订阅是一种消息范式,发布者不会将消息直接发送给订阅者,而是将发布的消息分为不同的类别,无需了解哪些订阅者是否存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者是否存在
发布者和订阅者都不知道对方的存在,发布者只需发送消息到订阅器里,订阅者只管接收自己订阅的内容

响应式原理

vue的响应式原理就是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的getter和setter,在数据变动时发布消息给订阅者,触发相应的监听回调。只要分为以下几个步骤:

  1. 需要给Observe被劫持的数据对象)的数据对象进行递归遍历,包括子属性对象的属性,都加上getter、setter这样的属性。修改这个对象的某个属性,就会触发setter,那么就能监听到数据的变化
  2. CompileVue的编译器)解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加侦听数据的订阅者,当数据发生变化时会收到通知,更新视图
  3. Watcher订阅者)是ObserveCompile之间的桥梁,主要做的事情有:
  4. 在自身实例化时Dep(用于收集订阅者的订阅器)里添加自己
  5. 自身必须拥有一个**update()**方法
  6. 待属性变动dep.notice() 通知时,能调用自身的update() 方法,并触发Compile中绑定的回调,则功成身退
  7. MVVM作为数据绑定的入口,整合ObserverCompileWatcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起ObserverCompile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果
Object.defineProperty(obj, prop, descriptor)
  • obj - 要定义的对象
  • prop - 要定义或修改的属性名称或Symbol
  • descriptor - 要定义或修改的属性的描述符(配置对象)
  • get 属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值
  • **默认为 [undefined]**set 属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。 默认为 [undefined]

缺点:在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染,因为 Object.defineProperty() 不能拦截到这些操作。更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题

  • 返回值:被传递给函数的对象obj

3.MVVM

MVVM(Model-View-ViewModel)模式是一种基于前端开发的架构模式,其核心是提供对 View 和 Model 的双向数据绑定,即当 View 或 Model 其中任意一个发生变化时,都会通过 ViewModel 引起另外一个的改变。

  • MVVM的优点
  1. 双向绑定。数据层和视图层中任何一层发生变化,都会通过ViewModel使另一层变化,时刻保持数据和视图的一致性;
  2. 通过数据驱动视图更新,不再手动操作DOM元素,提高性能;
  • MVVM的缺点
  1. 由于双向绑定的技术,产生bug后无法确定问题出现在数据层还是视图层;
  2. 数据层过大会占用更多的内存资源,影响性能;

4.常用指令

指令作用
v-on缩写为@,绑定事件
v-bind简写为:,动态绑定
v-slot简写为#,组件插槽
v-for循环对象或数组元素,同时生成DOM
v-show显示内容与否
v-if显示与隐藏,决定当前DOM元素是否渲染
v-else必须和v-if连用 不能单独使用 否则报错
v-text解析文本
v-html解析html标签

5.动态绑定class与style

classstyle
绑定对象:class=“{ className: isActive }” 或 :class=“classNameObject”:style=“{color: ‘#ffffff’}” 或 :style=“styleObject”
绑定数组:class=“[‘active’, ‘is-success’, { ‘is-disabled’: isDisabled }]”:style=“[styleObject1, styleObject2, styleObject3, …]”

6.常见修饰符

v-on
修饰符作用
.stopevent.stopPropagation(),阻止单击事件继续传播(阻止默认事件)
.preventevent.preventDefault(),提交事件不再重载页面(阻止默认行为)
.native监听组件根元素的原生事件
.once点击事件将只会触发一次
v-model
修饰符作用
.lazy取代 input 监听 change 事件
.number输入值转为数值类型
.trim输入首尾空格过滤

7.v-if与v-show的区别

原理
  • v-if会调用addIfCondition方法根据条件渲染,为false时在生成vnode的时候会忽略对应节点,render的时候就不会渲染
  • 添加v-show指令的元素一定会渲染,只是通过修改display属性的值来决定是否显示
切换
  • v-if切换时,DOM元素会重复生成和销毁,会执行生命周期钩子
  • v-show切换时不会执行生命周期钩子
应用场景

需要频繁切换DOM时,使用v-show;反之则使用v-if

8.为什么避免v-for和v-if在一起使用?

Vue 处理指令时,v-for 比 v-if 具有更高的*优先级*,存在性能问题。 如果你有5个元素被v-for循环,,v-if也会分别执行5次

9.v-for 循环为什么一定要绑定key

提升vue渲染性能

  • key 的作用主要是为了更高效的更新虚拟 DOM,因为它可以非常精确的找到相同节点,因此 patch 过程会非常高效
  • Vue 在 patch 过程中会判断两个节点是不是相同节点时,key 是一个必要条件。比如渲染列表时,如果不写 key,Vue 在比较的时候,就可能会导致频繁更新元素,使整个 patch 过程比较低效,影响性能
  • 应该避免使用数组下标作为 key,因为 key 值不是唯一的话可能会导致上面图中表示的 bug,使 Vue 无法区分它他,还有比如在使用相同标签元素过渡切换的时候,就会导致只替换其内部属性而不会触发过渡效果
  • Vue 判断两个节点是否相同时主要判断两者的元素类型和 key 等,如果不设置 key,就可能永远认为这两个是相同节点,只能去做更新操作,就造成大量不必要的 DOM 更新操作,明显是不可取的

10.为什么不建议用index索引作为key?

使用index作为key和没写基本上没区别,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2…这样排列,导致 Vue 会复用错误的旧子节点,做很多无意义的额外工作

11.v-model

v-model用于实现视图层与数据层的双向绑定,数据层变化时可以驱动视图层更新,当视图层变化时会改变数据。v-model本质上是一个语法糖,默认情况下相当于:value@input的结合。

v-model通常使用在表单项上,但也能使用在自定义组件上,表示对某个值的输入和输出控制。使用v-model可以减少大量繁琐的事件处理代码,提高开发效率。

  1. 概念:
    Vue 中双向绑定是一个指令v-model,可以绑定一个响应式数据到视图,同时视图的变化能改变该值。v-model是语法糖,默认情况下相当于:value@input,使用v-model可以减少大量繁琐的事件处理代码,提高开发效率。
  2. 使用:
    通常在表单项上使用v-model,还可以在自定义组件上使用,表示某个值的输入和输出控制。
  3. 原理:
    v-model是一个指令,双向绑定实际上是Vue 的编译器完成的,通过输出包含v-model模版的组件渲染函数,实际上还是value属性的绑定及input事件监听,事件回调函数中会做相应变量的更新操作。

12.v-model与.sync的对比

共同点:都是语法糖,都可以实现父子组件中数据的双向通信

不同点:

v-model.sync
父组件中使用v-model传递数据时,子组件通过@input触发事件父组件中传递数据时,子组件通过@update:xxx触发事件
一个组件只能绑定一个v-model vue2一个组件可以多个属性用.sync修饰符,可以同时"双向绑定多个“prop”
v-model针对更多的是最终操作结果,是双向绑定的结果,是value,是一种change操作.sync针对更多的是各种各样的状态,是状态的互相传递,是status,是一种update操作

13.computed与watch的区别

computed

  • 支持缓存,只有依赖的数据发生了变化,才会重新计算
  • 不支持异步,当computed中有异步操作时,无法监听数据的变化
  • computed的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,即data中声明的数据或props传递的数据
  • 如果一个属性是由其他属性计算而来,这个属性依赖其它属性,一般会使用computed
  • 如果computed属性的属性值是函数,那么默认使用get方法,函数的返回值就是属性的属性值;在computed中,属性有一个get方法和一个set方法,当数据发生变化时,会调用set方法

watch

  • 不支持缓存,数据变化会执行相应操作
  • 支持异步监听
  • 监听的函数接收两个参数,第一个参数为更新后的值,第二个参数为更新前的值‘
  • 当一个属性发生变化时,就需要执行相应的操作
  • 监听数据必须是data中声明的或者父组件传递过来的props中的数据,当发生变化时,会触发其他操作,函数有两个的参数:
  • immediate:默认为false,为true时组件加载会立即触发
  • deep:默认为false,为true时开启深度监听。需要注意的是,deep无法监听到数组和对象内部的变化

当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用watch

总结
computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
watch 侦听器 : 更多的是观察的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。

14.组件通信

组件通信指的是组件通过某一种方式来传递信息以达到某个目的的过程。每个组件都是独立的,在开发中我们就是通过组件通信使各个组件的功能联动起来

props/$emit

父组件通过props向子组件传递数据,子组件通过$emit事件和父组件通信

  • props只能是父组件向子组件传递数据,使得父子组件直接形成一个向下的单向数据流。子组件不能直接修改props数据,只能通知父组件来修改
  • $emit绑定一个自定义事件,可以将参数传递给父组件;父组件中则通过v-on注册监听事件同时接收参数
provide/inject

这种方式是通过依赖注入的方式实现组件的(可跨级)通信。依赖注入所提供的属性是非响应式的。

  • provide:用来发送数据或方法
  • inject:用来接收数据或方法
ref/$refs

在父组件中通过ref可以获取子组件实例,通过实例来访问子组件的属性和方法

p a r e n t / parent/ parent/children
  • p a r e n t 可以获取上一级父组件实例, parent可以获取上一级父组件实例, parent可以获取上一级父组件实例,root来访问根组件的实例
  • $children可以让组件访问所有子组件的实例,但是不能保证顺序,访问的数据也不是响应式的
  • 在根组件#app上拿 p a r e n t 得到的是 n e w V u e ( ) 的实例,在这实例上再拿 parent得到的是new Vue()的实例,在这实例上再拿 parent得到的是newVue()的实例,在这实例上再拿parent得到的是undefined,而在最底层的子组件拿$children是个空数组
  • c h i l d r e n 的值是 ∗ ∗ 数组 ∗ ∗ ,而 children 的值是**数组**,而 children的值是数组,而parent是个对象
a t t r s / attrs/ attrs/listeners

inheritAttrs:默认值为true,继承所有的父组件属性(除props之外的所有属性),为false表示只继承class属性

  • $attrs:继承所有的父组件属性(除了prop传递的属性、class 和 style)
  • l i s t e n e r s :该属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以配合 ‘ v − o n = " listeners:该属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以配合 `v-on=" listeners:该属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以配合von="listeners"` 将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)
eventBus事件总线

eventBus事件总线适用于父子组件、非父子组件等之间的通信。这种组件通信方式会造成后期维护困难。vue3中移除了事件总线,取而代之的是插件,使用方式并无变化。

总结

子组件不能直接修改父组件传递的数据,这样做是维护父子组件之间形成的单向数据流。如果子组件随意更改父组件传递的数据,会导致数据流混乱,提高开发和维护成本

15.生命周期

生命周期指的是Vue组件从创建到销毁经历的一系列的过程

  • 创建前后
  • beforeCreate - 组件创建之前,无法获取data中的数据
  • created - 组件创建完成后,可以获取数据
  • 渲染前后
  • beforeMount - 组件挂载到DOM之前
  • mounted - 组件挂载完毕,可以获取DOM节点
  • 更新前后
  • beforeUpdate - 响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染
  • updated - 在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。此时 DOM 已经根据响应式数据的变化更新了。调用时,组件 DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用
  • 销毁前后
  • beforeDestroy - 实例销毁之前调用。这一步,实例仍然完全可用,this 仍能获取到实例
  • destroyed - 实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁

父子组件的生命周期执行顺序

  • 加载渲染过程
  • 父beforeCreate - 父created - 父beforeMount - 子beforeCreate - 子created - 子beforeMount - 子mounted - 父mounted
  • 更新过程
  • 父beforeUpdate - 子beforeUpdate - 子updated - 父updated
  • 销毁过程
  • 父beforeDestroy - 子beforeDestroy - 子destroyed - 父destroyed

created和mounted的区别

  • created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图
  • mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作

一般在哪个生命周期请求异步数据

我们可以在钩子函数createdbeforeMountmounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值

推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面加载时间,用户体验更好
  • SSR不支持 beforeMount 、mounted 钩子函数,放在 created 中有助于一致性
Vue 2中的生命周期钩子Vue 3选项式API的生命周期选项Vue 3 组合API中生命周期钩子描述
beforeCreatebeforeCreatesetup()创建前,此时datamethods的数据都还没有初始化
createdcreatedsetup()创建后,data中有值,尚未挂载,可以进行一些Ajax请求
beforeMountbeforeMountonBeforeMount挂载前,会找到虚拟DOM,编译成Render
mountedmountedonMounted挂载后,DOM已创建,可用于获取访问数据和DOM元素
beforeUpdatebeforeUpdateonBeforeUpdate更新前,可用于获取更新前各种状态
updatedupdatedonUpdated更新后,所有状态已是最新
beforeDestroybeforeUnmountonBeforeUnmount销毁前,可用于一些定时器或订阅的取消
destroyedunmountedonUnmounted销毁后,可用于一些定时器或订阅的取消
activatedactivatedonActivatedkeep-alive缓存的组件激活时
deactivateddeactivatedonDeactivatedkeep-alive缓存的组件停用时
errorCapturederrorCapturedonErrorCaptured捕获一个来自子孙组件的错误时调用
renderTrackedonRenderTracked调试钩子,响应式依赖被收集时调用
renderTriggeredonRenderTriggered调试钩子,响应式依赖被触发时调用
serverPrefetchonServerPrefetch组件实例在服务器上被渲染前调用

16.slot插槽

slot是Vue的内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内容的出口。插槽slot是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的

  • 默认插槽

子组件用``标签来确定渲染的位置,标签里面可以放DOM结构,当父组件使用的时候没有往插槽传入内容,标签内DOM结构就会显示在页面。父组件在使用的时候,直接在子组件的标签内写入内容即可

  • 具名插槽

子组件用name属性来表示插槽的名字,不传为默认插槽。父组件中在使用时在默认插槽的基础上加上slot属性,值为子组件插槽name属性值

  • 作用域插槽

子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件v-slot接受的对象上父组件中在使用时通过v-slot:(简写:#)获取子组件的信息,在内容中使用

小结:

  1. v-slot属性只能在`上使用,但在只有默认插槽时可以在组件标签上使用
  2. 默认插槽名为default,可以省略default直接写v-slot。缩写为#时不能不写参数,写成#default
  3. 可以通过解构获取v-slot={user},还可以重命名v-slot="{user: newName}"和定义默认值v-slot="{user = '默认值'}"

17.为什么data是一个函数而不是一个对象

保证每个组件内数据的独立性,防止出现变量污染。对象为引用类型,当复用组件时,由于数据对象都指向同一个data对象,当在一个组件中修改data时,其他重用的组件中的data会同时被修改;而使用返回对象的函数,由于每次返回的都是一个新对象(Object的实例),引用地址不同,则不会出现这个问题。

18.Vue-Router

对前端路由的理解

前端路由的核心,就在于改变视图的同时不会向后端发出请求;而是加载路由对应的组件。Vue-Router就是将组件映射到路由, 然后渲染出来的

  • 拦截用户的刷新操作,避免服务端盲目响应、返回不符合预期的资源内容。把刷新这个动作完全放到前端逻辑里消化调
  • 感知URL的变化,根据这些变化使用js生成不同的内容
什么是Vue-Router,有哪些组件

Vue-Router是Vue官方的路由管理器。它和Vue.js的核心深度集成,路径和组件的映射关系使得构建SPA(Single Page Application,单页面应用)变得易如反掌

  • router-link - 实质上最终会渲染成a链接
  • router-view - 子级路由显示
  • keep-alive - 包裹组件缓存
r o u t e 和 route和 routerouter
  • $route 是路由信息对象,包含了path、params、hash、query、fullPath、matched、name等路由信息参数
  • $router是路由的实例对象,包含了路由的跳转方法、钩子函数等内容
路由开发的优缺点

优点:

  • 整体不刷新页面,用户体验更好
  • 数据传递简单,开发效率高

缺点:

  • 学习成本高
  • 首次加载缓慢,不利于seo
使用方式
  1. 新建index.js路由入口文件
  2. 创建路由规则
  3. 创建路由对象
  4. 将路由对象挂载到Vue.use()中
  5. 将路由对象挂载到 Vue 实例上
Hash模式

基于浏览器的hashchange事件,当url发生变化时,通过 window.location.hash 获取地址上的hash值,并通过Router类,配置routes对象设置与hash值对应的组件内容

优点:

  • hash值会出现在url中,但是不会被包含在http请求中,因此hash值改变不会重新加载页面
  • hash改变会触发hashchange事件,能控制浏览器的前进后退
  • 兼容性好

缺点:

  • 地址中携带#,不美观
  • 只可修改#后面的部分,因此只能设置与当前URL同文档的URL
  • hash有体积限制,故只可以添加短字符串
  • 设置的新值必须与原来不同才会触发hashchange事件,并将记录添加到栈中
  • 每次URL的改变不属于一次http请求,所以不利于seo优化
History模式

基于H5新增的pushState()和replaceState()两个api,以及浏览器的popstate事件,地址变化时,通过 window.location.pathname 找到对应的组件,并通过构造Router类,配置routes对象设置pathname值与对应的组件内容

优点:

  • 没有#,相对美观
  • pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL
  • pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中
  • pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中
  • pushState() 可额外设置 title 属性供后续使用
  • 浏览器的进后退能触发浏览器的popstate事件,获取window.location.pathname来控制页面的变化

缺点:

  • URL的改变属于http请求,借助history.pushState实现页面的无刷新跳转,因此会重新请求服务器。所以前端的 URL 必须和实际向后端发起请求的 URL 一致。如果用户输入的URL回车或者浏览器刷新或者分享出去某个页面路径,用户点击后,URL与后端配置的页面请求URL不一致,则匹配不到任何静态资源,就会返回404页面。所以需要后台配置支持,覆盖所有情况的候选资源,如果 URL 匹配不到任何静态资源,则应该返回app 依赖的页面或者应用首页
  • 兼容性差,特定浏览器支持
路由hash模式和history模式的区别

hash 模式是一种把前端路由的路径用 # 拼接在真实 url 后面的模式。当 # 后面的路径发生变化时,浏览器并不会重新发起请求,而是会触发 onhashchange 事件。
hash模式的特点:

  • hash变化会触发网页跳转,即浏览器的前进和后退
  • hash 可以改变 url ,但是不会触发页面重新加载(hash的改变是记录在 window.history 中),即不会刷新页面。也就是说,所有页面的跳转都是在客户端进行操作。因此,这并不算是一次 http 请求,所以这种模式不利于 SEO 优化。hash 只能修改 # 后面的部分,所以只能跳转到与当前 url 同文档的 url
  • hash 通过触发 hashchange 事件,来监听 hash 的改变,借此实现无刷新跳转的功能
  • hash 永远不会提交到 server 端(可以理解为只在前端自生自灭)

history APIH5 提供的新特性,允许开发者直接更改前端路由,即更新浏览器 URL 地址而不重新发起请求

  • 新的 url 可以是与当前 url 同源的任意 url ,也可以是与当前 url 一样的地址,但是这样会导致的一个问题是,会把重复的这一次操作记录到栈当中
  • 通过 history.state ,添加任意类型的数据到记录中
  • 可以额外设置 title 属性,以便后续使用
  • 通过 pushStatereplaceState 来实现无刷新跳转的功能,需要后端配合
history模式下的404问题
  1. URL的改变属于http请求,借助history.pushState实现页面的无刷新跳转,因此会重新请求服务器
  2. 所以前端的 URL 必须和实际向后端发起请求的 URL 一致
编程式导航
方式作用
$router.push()跳转到指定的url,并在history中添加记录,点击回退返回到上一个页面
$router.replace()跳转到指定的url,但是history中不会添加记录,点击回退到上上个页面
$router.go(n)向前或者后跳转n个页面,n可以是正数也可以是负数
$router.back()后退、回到上一页
$router.forward()前进、回到下一页
路由传参的方式
  • query传参

query传参需要使用path来引入,页面跳转后参数将会出现在url中;也可以直降将参数以?xxx=xx&xxx=xx的形式拼接在路由地址中

this.$router.push({ path: '/example', query: { id: '1'}})   // 传递query参数
this.$route.query.id    // 获取query参数

以query的方式传参时,刷新页面不会导致参数丢失

  • params传参

params传参需要使用name来引入,页面跳转后参数不会出现在url中;在4.1.4版本开始,在未定义动态路由的情况下,将不能直接使用编程式导航传递params参数,目的是解决刷新页面参数丢失的问题

传参的方式总结

  • this.$router.push(path)
  • this.$router.push({path, query})
  • this.$router.push({name, params}) // 注意版本更新
  • this.$router.push({name, query})

19.Vue.$nextTick

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

nextTick 是 Vue 提供的一个全局 API,由于 Vue 的异步更新策略,导致我们对数据修改后不会直接体现在 DOM 上,此时如果想要立即获取更新后的 DOM 状态,就需要借助该方法。

Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue 将开启一个异步更新队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入队列一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。nextTick方法会在队列中加入一个回调函数,确保该函数在前面的 DOM 操作完成后才调用。

使用场景:

  1. 如果想要在修改数据后立刻得到更新后的DOM结构,可以使用Vue.nextTick()
  2. created生命周期中进行DOM操作

20.Vue 实例挂载过程中发生了什么?

挂载过程指的是 app.mount()过程,这是一个初始化过程,整体上做了两件事情:初始化建立更新机制

初始化会创建组件实例、初始化组件状态、创建各种响应式数据。

建立更新机制这一步会立即执行一次组件的更新函数,这会首次执行组件渲染函数并执行patchvnode 转换为 dom; 同时首次执行渲染函数会创建它内部响应式数据和组件更新函数之间的依赖关系,这使得以后数据发生变化时会执行对应的更新函数。

21.Vue 的模版编译原理

Vue 中有个独特的编译器模块,称为compiler,它的主要作用是将用户编写的template编译为js中可执行的render函数。
在Vue 中,编译器会先对template进行解析,这一步称为parse,结束之后得到一个JS对象,称之为抽象语法树AST;然后是对AST进行深加工的转换过程,这一步称为transform,最后将前面得到的AST生成JS代码,也就是render函数。

22.Vue 的响应式原理

  1. Vue 2 中的数据响应式会根据数据类型做不同的处理。如果是对象,则通过

    Object.defineProperty(obj,key,descriptor)
    

    拦截对象属性访问,当数据被访问或改变时,感知并作出反应;如果是数组,则通过覆盖数组原型的方法,扩展它的7个变更方法(push、pop、shift、unshift、splice、sort、reverse),使这些方法可以额外的做更新通知,从而做出响应。

    缺点:

    • 初始化时的递归遍历会造成性能损失;
    • 通知更新过程需要维护大量 dep 实例和 watcher 实例,额外占用内存较多;
    • 新增或删除对象属性无法拦截,需要通过 Vue.setdelete 这样的 API 才能生效;
    • 对于ES6中新产生的MapSet这些数据结构不支持。
  2. Vue 3 中利用ES6Proxy机制代理需要响应化的数据。可以同时支持对象和数组,动态属性增、删都可以拦截,新增数据结构均支持,对象嵌套属性运行时递归,用到时才代理,也不需要维护特别多的依赖关系,性能取得很大进步。

23.虚拟DOM

  1. 概念:
    虚拟DOM,顾名思义就是虚拟的DOM对象,它本身就是一个JS对象,只不过是通过不同的属性去描述一个视图结构。
  2. 虚拟DOM的好处:
    (1) 性能提升
    直接操作DOM是有限制的,一个真实元素上有很多属性,如果直接对其进行操作,同时会对很多额外的属性内容进行了操作,这是没有必要的。如果将这些操作转移到JS对象上,就会简单很多。另外,操作DOM的代价是比较昂贵的,频繁的操作DOM容易引起页面的重绘和回流。如果通过抽象VNode进行中间处理,可以有效减少直接操作DOM次数,从而减少页面的重绘和回流。
    (2) 方便跨平台实现
    同一VNode节点可以渲染成不同平台上对应的内容,比如:渲染在浏览器是DOM元素节点,渲染在Native(iOS、Android)变为对应的控件。Vue 3 中允许开发者基于VNode实现自定义渲染器(renderer),以便于针对不同平台进行渲染。
  3. 结构:
    没有统一的标准,一般包括tagpropschildren三项。
    tag:必选。就是标签,也可以是组件,或者函数。
    props:非必选。就是这个标签上的属性和方法。
    children:非必选。就是这个标签的内容或者子节点。如果是文本节点就是字符串;如果有子节点就是数组。换句话说,如果判断children是字符串的话,就表示一定是文本节点,这个节点肯定没有子元素。

24.diff 算法

  1. 概念:
    diff算法是一种对比算法,通过对比旧的虚拟DOM和新的虚拟DOM,得出是哪个虚拟节点发生了改变,找出这个虚拟节点并只更新这个虚拟节点所对应的真实节点,而不用更新其他未发生改变的节点,实现精准地更新真实DOM,进而提高效率。
  2. 对比方式:
    diff算法的整体策略是:深度优先,同层比较。比较只会在同层级进行, 不会跨层级比较;比较的过程中,循环从两边向中间收拢。
  • 首先判断两个节点的tag是否相同,不同则删除该节点重新创建节点进行替换。

  • tag相同时,先替换属性,然后对比子元素,分为以下几种情况:

    相同时,先替换属性,然后对比子元素,分为以下几种情况:

    • 新旧节点都有子元素时,采用双指针方式进行对比。新旧头尾指针进行比较,循环向中间靠拢,根据情况调用patchVnode进行patch重复流程、调用createElem创建一个新节点,从哈希表寻找 key一致的VNode节点再分情况操作。
    • 新节点有子元素,旧节点没有子元素,则将子元素虚拟节点转化成真实节点插入即可。
    • 新节点没有子元素,旧节点有子元素,则清空子元素,并设置为新节点的文本内容。
    • 新旧节点都没有子元素时,即都为文本节点,则直接对比文本内容,不同则更新。

25.Vue 2中的set方法?

set是Vue 2中的一个全局API。可手动添加响应式数据,解决数据变化视图未更新问题。当在项目中直接设置数组的某一项的值,或者直接设置对象的某个属性值,会发现页面并没有更新。这是因为Object.defineProperty()的限制,监听不到数据变化,可通过this.$set(数组或对象,数组下标或对象的属性名,更新后的值)解决。

26.keep-alive 是什么?

  • 作用:实现组件缓存,保持组件的状态,避免反复渲染导致的性能问题。
  • 工作原理:Vue.js 内部将 DOM 节点,抽象成了一个个的 VNode 节点,keep-alive组件的缓存也是基于 VNode 节点的。它将满足条件的组件在 cache 对象中缓存起来,重新渲染的时候再将 VNode 节点从 cache 对象中取出并渲染。
  • 可以设置以下属性:
    include:字符串或正则,只有名称匹配的组件会被缓存。
    exclude:字符串或正则,任何名称匹配的组件都不会被缓存。
    max:数字,最多可以缓存多少组件实例。
    匹配首先检查组件的name选项,如果name选项不可用,则匹配它的局部注册名称(父组件 components选项的键值),匿名组件不能被匹配。

设置了keep-alive缓存的组件,会多出两个生命周期钩子:activateddeactivated
首次进入组件时:beforeCreate --> created --> beforeMount --> mounted --> activated --> beforeUpdate --> updated --> deactivated
再次进入组件时:activated --> beforeUpdate --> updated --> deactivated

27.mixin

mixin(混入), 它提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。

使用场景: 不同组件中经常会用到一些相同或相似的代码,这些代码的功能相对独立。可以通过mixin 将相同或相似的代码提出来。

缺点:

  1. 变量来源不明确
  2. 多 mixin 可能会造成命名冲突(解决方式:Vue 3的组合API)
  3. mixin 和组件出现多对多的关系,使项目复杂度变高。

28.对 SPA 的理解?

  1. 概念:
    SPA(Single-page application),即单页面应用,它是一种网络应用程序或网站的模型,通过动态重写当前页面来与用户交互,这种方法避免了页面之间切换时打断用户体验。在SPA中,所有必要的代码(HTML、JavaScript 和 CSS)都通过单个页面的加载而检索,或者根据需要(通常是响应用户操作)动态装载适当的资源并添加到页面。页面在任何时间点都不会重新加载,也不会将控制转移到其他页面。举个例子,就像一个杯子,上午装的是牛奶,中午装的是咖啡,下午装的是茶,变得始终是内容,杯子始终不变。

  2. SPAMPA的区别:
    MPA(Muti-page application),即多页面应用。在MPA中,每个页面都是一个主页面,都是独立的,每当访问一个页面时,都需要重新加载 Html、CSS、JS 文件,公共文件则根据需求按需加载。

    SPAMPA
    组成一个主页面和多个页面片段多个主页面
    url模式hash模式history模式
    SEO搜索引擎优化难实现,可使用SSR方式改善容易实现
    数据传递容易通过url、cookie、localStorage等传递
    页面切换速度快,用户体验良好切换加载资源,速度慢,用户体验差
    维护成本相对容易相对复杂
  3. SPA的优缺点:
    优点:

    • 具有桌面应用的即时性、网站的可移植性和可访问性
    • 用户体验好、快,内容的改变不需要重新加载整个页面
    • 良好的前后端分离,分工更明确

    缺点:

    • 不利于搜索引擎的抓取
    • 首次渲染速度相对较慢

29.子组件是否可以直接改变父组件的数据?

  1. 所有的prop都遵循着单项绑定原则,props因父组件的更新而变化,自然地将新状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。
    另外,每次父组件更新后,所有的子组件中的props都会被更新为最新值,这就意味着不应该子组件中去修改一个prop,若这么做了,Vue 会在控制台上抛出警告。
  2. 实际开发过程中通常有两个场景导致要修改prop
    • prop被用于传入初始值,而子组件想在之后将其作为一个局部数据属性。这种情况下,最好是新定义一个局部数据属性,从props获取初始值即可。
    • 需要对传入的prop值做进一步转换。最好是基于该prop值定义一个计算属性。
  3. 实践中,如果确实要更改父组件属性,应emit一个事件让父组件变更。当对象或数组作为props被传入时,虽然子组件无法更改props绑定,但仍然可以更改对象或数组内部的值。这是因为JS的对象和数组是按引用传递,而对于 Vue 来说,禁止这样的改动虽然可能,但是有很大的性能损耗,比较得不偿失。

30.动态路由?

很多时候,我们需要将给定匹配模式的路由映射到同一个组件,这种情况就需要定义动态路由。例如,我们有一个 User组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用动态路径参数(dynamic segment)来达到这个效果:{path: '/user/:id', compenent: User},其中:id就是动态路径参数。

31.对Vuex的理解?

  1. 概念:
    Vuex 是 Vue 专用的状态管理库,它以全局方式集中管理应用的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
  2. 解决的问题:
    Vuex 主要解决的问题是多组件之间状态共享。利用各种通信方式,虽然也能够实现状态共享,但是往往需要在多个组件之间保持状态的一致性,这种模式很容易出问题,也会使程序逻辑变得复杂。Vuex 通过把组件的共享状态抽取出来,以全局单例模式管理,这样任何组件都能用一致的方式获取和修改状态,响应式的数据也能够保证简洁的单向流动,使代码变得更具结构化且易于维护。
  3. 什么时候用:
    Vuex 并非是必须的,它能够管理状态,但同时也带来更多的概念和框架。如果我们不打算开发大型单页应用或应用里没有大量全局的状态需要维护,完全没有使用Vuex的必要,一个简单的 store 模式就够了。反之,Vuex将是自然而然的选择。
  4. 用法:
    Vuex 将全局状态放入state对象中,它本身是一颗状态树,组件中使用store实例的state访问这些状态;然后用配套的mutation方法修改这些状态,并且只能用mutation修改状态,在组件中调用commit方法提交mutation;如果应用中有异步操作或复杂逻辑组合,需要编写action,执行结束如果有状态修改仍需提交mutation,组件中通过dispatch派发action。最后是模块化,通过modules选项组织拆分出去的各个子模块,在访问状态(state)时需注意添加子模块的名称,如果子模块有设置namespace,那么提交mutation和派发action时还需要额外的命名空间前缀。

32.页面刷新后Vuex 状态丢失怎么解决?

Vuex 只是在内存中保存状态,刷新后就会丢失,如果要持久化就需要保存起来。

localStorage就很合适,提交mutation的时候同时存入localStorage,在store中把值取出来作为state的初始值即可。

也可以使用第三方插件,推荐使用vuex-persist插件,它是为 Vuex 持久化储存而生的一个插件,不需要你手动存取storage,而是直接将状态保存至 cookie 或者 localStorage中。

33.关于 Vue SSR 的理解?

SSR服务端渲染(Server Side Render),就是将 Vue 在客户端把标签渲染成 html 的工作放在服务端完成,然后再把 html 直接返回给客户端。

  • 优点:
    有着更好的 SEO,并且首屏加载速度更快。
  • 缺点:
    开发条件会受限制,服务器端渲染只支持 beforeCreate 和 created 两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于 Node.js 的运行环境。服务器会有更大的负载需求。

34.了解哪些 Vue 的性能优化方法?

  • 路由懒加载。有效拆分应用大小,访问时才异步加载。
  • keep-alive缓存页面。避免重复创建组件实例,且能保留缓存组件状态。
  • v-for遍历避免同时使用v-if。实际上在 Vue 3 中已经是一个错误用法了。
  • 长列表性能优化,可采用虚拟列表。
  • v-once。不再变化的数据使用v-once
  • 事件销毁。组件销毁后把全局变量和定时器销毁。
  • 图片懒加载。
  • 第三方插件按需引入。
  • 子组件分割。较重的状态组件适合拆分。
  • 服务端渲染。

35.vue 中使用了哪些设计模式

1.工厂模式 - 传入参数即可创建实例

虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode

2.单例模式 - 整个程序有且仅有一个实例

vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉

3.发布-订阅模式 (vue 事件机制)

4.观察者模式 (响应式数据原理)

5.装饰模式: (@装饰器的用法)

6.策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略

36.Vue 的生命周期方法有哪些 一般在哪一步发请求

beforeCreate 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问

created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有el,如果非要想与Dom进行交互,可以通过vm.el,如果非要想与 Dom 进行交互,可以通过 vm.el,如果非要想与Dom进行交互,可以通过vm.nextTick 来访问 Dom

beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。

mounted 在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点

beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁(patch)之前。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程

updated 发生在更新完成之后,当前阶段组件 Dom 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新,该钩子在服务器端渲染期间不被调用。

beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。我们可以在这时进行善后收尾工作,比如清除计时器。

destroyed Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。

activated keep-alive 专属,组件被激活时调用

deactivated keep-alive 专属,组件被销毁时调用

异步请求在哪一步发起?

可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。

如果异步请求不需要依赖 Dom 推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面 loading 时间;
  • ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;

37.vue3.0 特性你有什么了解的吗?

Vue 3.0 正走在发布的路上,Vue 3.0 的目标是让 Vue 核心变得更小、更快、更强大,因此 Vue 3.0 增加以下这些新特性:

(1)监测机制的改变

3.0 将带来基于代理 Proxy 的 observer 实现,提供全语言覆盖的反应性跟踪。这消除了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制:

  • 只能监测属性,不能监测对象
  • 检测属性的添加和删除;
  • 检测数组索引和长度的变更;
  • 支持 Map、Set、WeakMap 和 WeakSet。

新的 observer 还提供了以下特性:

  • 用于创建 observable 的公开 API。这为中小规模场景提供了简单轻量级的跨组件状态管理解决方案。
  • 默认采用惰性观察。在 2.x 中,不管反应式数据有多大,都会在启动时被观察到。如果你的数据集很大,这可能会在应用启动时带来明显的开销。在 3.x 中,只观察用于渲染应用程序最初可见部分的数据。
  • 更精确的变更通知。在 2.x 中,通过 Vue.set 强制添加新属性将导致依赖于该对象的 watcher 收到变更通知。在 3.x 中,只有依赖于特定属性的 watcher 才会收到通知。
  • 不可变的 observable:我们可以创建值的“不可变”版本(即使是嵌套属性),除非系统在内部暂时将其“解禁”。这个机制可用于冻结 prop 传递或 Vuex 状态树以外的变化。
  • 更好的调试功能:我们可以使用新的 renderTracked 和 renderTriggered 钩子精确地跟踪组件在什么时候以及为什么重新渲染。

(2)模板

模板方面没有大的变更,只改了作用域插槽,2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。

同时,对于 render 函数的方面,vue3.0 也会进行一系列更改来方便习惯直接使用 api 来生成 vdom 。

(3)对象式的组件声明方式

vue2.x 中的组件是通过声明的方式传入一系列 option,和 TypeScript 的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦。3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 TypeScript 的结合变得很容易。

此外,vue 的源码也改用了 TypeScript 来写。其实当代码的功能复杂之后,必须有一个静态类型系统来做一些辅助管理。现在 vue3.0 也全面改用 TypeScript 来重写了,更是使得对外暴露的 api 更容易结合 TypeScript。静态类型系统对于复杂代码的维护确实很有必要。

(4)其它方面的更改

vue3.0 的改变是全面的,上面只涉及到主要的 3 个方面,还有一些其他的更改:

  • 支持自定义渲染器,从而使得 weex 可以通过自定义渲染器的方式来扩展,而不是直接 fork 源码来改的方式。
  • 支持 Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。
  • 基于 treeshaking 优化,提供了更多的内置功能。

38.为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?

Object.defineProperty 本身有一定的监控到数组下标变化的能力,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性。为了解决这个问题,经过 vue 内部处理后可以使用以下几种方法来监听数组

push();
pop();
shift();
unshift();
splice();
sort();
reverse();

由于只针对了以上 7 种方法进行了 hack 处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。

Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x 里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。

Proxy 可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

39.Vue3.0 和 2.0 的响应式原理区别

Vue3.x 改用 Proxy 替代 Object.defineProperty。因为 Proxy 可以直接监听对象和数组的变化,并且有多达 13 种拦截方法。

相关代码如下

import { mutableHandlers } from "./baseHandlers"; // 代理相关逻辑
import { isObject } from "./util"; // 工具方法

export function reactive(target) {
  // 根据不同参数创建不同响应式对象
  return createReactiveObject(target, mutableHandlers);
}
function createReactiveObject(target, baseHandler) {
  if (!isObject(target)) {
    return target;
  }
  const observed = new Proxy(target, baseHandler);
  return observed;
}

const get = createGetter();
const set = createSetter();

function createGetter() {
  return function get(target, key, receiver) {
    // 对获取的值进行放射
    const res = Reflect.get(target, key, receiver);
    console.log("属性获取", key);
    if (isObject(res)) {
      // 如果获取的值是对象类型,则返回当前对象的代理对象
      return reactive(res);
    }
    return res;
  };
}
function createSetter() {
  return function set(target, key, value, receiver) {
    const oldValue = target[key];
    const hadKey = hasOwn(target, key);
    const result = Reflect.set(target, key, value, receiver);
    if (!hadKey) {
      console.log("属性新增", key, value);
    } else if (hasChanged(value, oldValue)) {
      console.log("属性值被修改", key, value);
    }
    return result;
  };
}
export const mutableHandlers = {
  get, // 当获取属性时调用此方法
  set, // 当修改属性时调用此方法
};

40.Vue 项目进行哪些优化?

(1)代码层面的优化

  • v-if 和 v-show 区分使用场景
  • computed 和 watch 区分使用场景
  • v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
  • 长列表性能优化
  • 事件的销毁
  • 图片资源懒加载
  • 路由懒加载
  • 第三方插件的按需引入
  • 优化无限列表性能
  • 服务端渲染 SSR or 预渲染

(2)Webpack 层面的优化

  • Webpack 对图片进行压缩
  • 减少 ES6 转为 ES5 的冗余代码
  • 提取公共代码
  • 模板预编译
  • 提取组件的 CSS
  • 优化 SourceMap
  • 构建结果输出分析
  • Vue 项目的编译优化

(3)基础的 Web 技术的优化

  • 开启 gzip 压缩
  • 浏览器缓存
  • CDN 的使用
  • 使用 Chrome Performance 查找性能瓶颈

41.Vue中v-html会导致哪些问题

  • 可能会导致 xss 攻击
  • v-html 会替换掉标签内部的子元素
let template = require('vue-template-compiler'); 
let r = template.compile(`<div v-html="'<span>hello</span>'"></div>`) 

// with(this){return _c('div',{domProps: {"innerHTML":_s('<span>hello</span>')}})} 
console.log(r.render);

// _c 定义在core/instance/render.js 
// _s 定义在core/instance/render-helpers/index,js
if (key === 'textContent' || key === 'innerHTML') { 
    if (vnode.children) vnode.children.length = 0 
    if (cur === oldProps[key]) continue // #6601 work around Chrome version <= 55 bug where single textNode // replaced by innerHTML/textContent retains its parentNode property 
    if (elm.childNodes.length === 1) { 
        elm.removeChild(elm.childNodes[0]) 
    } 
}

42.Vue与React的区别

相同点:

  1. Virtual DOM。其中最大的一个相似之处就是都使用了Virtual DOM。(当然Vue是在Vue2.x才引用的)也就是能让我们通过操作数据的方式来改变真实的DOM状态。因为其实Virtual DOM的本质就是一个JS对象,它保存了对真实DOM的所有描述,是真实DOM的一个映射,所以当我们在进行频繁更新元素的时候,改变这个JS对象的开销远比直接改变真实DOM要小得多。
  2. 组件化的开发思想。第二点来说就是它们都提倡这种组件化的开发思想,也就是建议将应用分拆成一个个功能明确的模块,再将这些模块整合在一起以满足我们的业务需求。
  3. PropsVueReact中都有props的概念,允许父组件向子组件传递数据。
  4. 构建工具、Chrome插件、配套框架。还有就是它们的构建工具以及Chrome插件、配套框架都很完善。比如构建工具,React中可以使用CRAVue中可以使用对应的脚手架vue-cli。对于配套框架Vue中有vuex、vue-routerReact中有react-router、redux

不同点

  1. 模版的编写。最大的不同就是模版的编写,Vue鼓励你去写近似常规HTML的模板,React推荐你使用JSX去书写。
  2. 状态管理与对象属性。在React中,应用的状态是比较关键的概念,也就是state对象,它允许你使用setState去更新状态。但是在Vue中,state对象并不是必须的,数据是由data属性在Vue对象中进行管理。
  3. 虚拟DOM的处理方式不同。Vue中的虚拟DOM控制了颗粒度,组件层面走watcher通知,而组件内部走vdomdiff,这样,既不会有太多watcher,也不会让vdom的规模过大。而React走了类似于CPU调度的逻辑,把vdom这棵树,微观上变成了链表,然后利用浏览器的空闲时间来做diff

43.函数式组件优势和原理

函数组件的特点

  1. 函数式组件需要在声明组件是指定 functional:true
  2. 不需要实例化,所以没有this,this通过render函数的第二个参数context来代替
  3. 没有生命周期钩子函数,不能使用计算属性,watch
  4. 不能通过$emit 对外暴露事件,调用事件只能通过context.listeners.click的方式调用外部传入的事件
  5. 因为函数式组件是没有实例化的,所以在外部通过ref去引用组件时,实际引用的是HTMLElement
  6. 函数式组件的props可以不用显示声明,所以没有在props里面声明的属性都会被自动隐式解析为prop,而普通组件所有未声明的属性都解析到$attrs里面,并自动挂载到组件根元素上面(可以通过inheritAttrs属性禁止)

优点

  1. 由于函数式组件不需要实例化,无状态,没有生命周期,所以渲染性能要好于普通组件
  2. 函数式组件结构比较简单,代码结构更清晰

使用场景:

  • 一个简单的展示组件,作为容器组件使用 比如 router-view 就是一个函数式组件
  • “高阶组件”——用于接收一个组件作为参数,返回一个被包装过的组件

例子

Vue.component('functional',{ // 构造函数产生虚拟节点的
    functional:true, // 函数式组件 // data={attrs:{}}
    render(h){
        return h('div','test')
    }
})
const vm = new Vue({
    el: '#app'
})

源码相关

// functional component
if (isTrue(Ctor.options.functional)) { // 带有functional的属性的就是函数式组件
  return createFunctionalComponent(Ctor, propsData, data, context, children)
}

// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on // 处理事件
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn // 处理原生事件

// install component management hooks onto the placeholder node
installComponentHooks(data) // 安装组件相关钩子 (函数式组件没有调用此方法,从而性能高于普通组件)

44.Vue-router 路由钩子在生命周期的体现

一、Vue-Router导航守卫

有的时候,需要通过路由来进行一些操作,比如最常见的登录权限验证,当用户满足条件时,才让其进入导航,否则就取消跳转,并跳到登录页面让其登录。 为此有很多种方法可以植入路由的导航过程:全局的,单个路由独享的,或者组件级的

  1. 全局路由钩子

vue-router全局有三个路由钩子;

  • router.beforeEach 全局前置守卫 进入路由之前
  • router.beforeResolve 全局解析守卫(2.5.0+)在 beforeRouteEnter 调用之后调用
  • router.afterEach 全局后置钩子 进入路由之后

具体使用∶

  • beforeEach(判断是否登录了,没登录就跳转到登录页)
router.beforeEach((to, from, next) => {  
    let ifInfo = Vue.prototype.$common.getSession('userData');  // 判断是否登录的存储信息
    if (!ifInfo) { 
        // sessionStorage里没有储存user信息    
        if (to.path == '/') { 
            //如果是登录页面路径,就直接next()      
            next();    
        } else { 
            //不然就跳转到登录      
            Message.warning("请重新登录!");     
            window.location.href = Vue.prototype.$loginUrl;    
        }  
    } else {    
        return next();  
    }
})
  • afterEach (跳转之后滚动条回到顶部)
router.afterEach((to, from) => {  
    // 跳转之后滚动条回到顶部  
    window.scrollTo(0,0);
});
  1. 单个路由独享钩子

beforeEnter 如果不想全局配置守卫的话,可以为某些路由单独配置守卫,有三个参数∶ to、from、next

export default [    
    {        
        path: '/',        
        name: 'login',        
        component: login,        
        beforeEnter: (to, from, next) => {          
            console.log('即将进入登录页面')          
            next()        
        }    
    }
]
  1. 组件内钩子

beforeRouteUpdate、beforeRouteEnter、beforeRouteLeave

这三个钩子都有三个参数∶to、from、next

  • beforeRouteEnter∶ 进入组件前触发
  • beforeRouteUpdate∶ 当前地址改变并且改组件被复用时触发,举例来说,带有动态参数的路径foo/∶id,在 /foo/1 和 /foo/2 之间跳转的时候,由于会渲染同样的foa组件,这个钩子在这种情况下就会被调用
  • beforeRouteLeave∶ 离开组件被调用

注意点,beforeRouteEnter组件内还访问不到this,因为该守卫执行前组件实例还没有被创建,需要传一个回调给 next来访问,例如:

beforeRouteEnter(to, from, next) {      
    next(target => {        
        if (from.path == '/classProcess') {          
            target.isFromProcess = true        
        }      
    })    
}
二、Vue路由钩子在生命周期函数的体现
  1. 完整的路由导航解析流程(不包括其他生命周期)
  • 触发进入其他路由。
  • 调用要离开路由的组件守卫beforeRouteLeave
  • 调用局前置守卫∶ beforeEach
  • 在重用的组件里调用 beforeRouteUpdate
  • 调用路由独享守卫 beforeEnter。
  • 解析异步路由组件。
  • 在将要进入的路由组件中调用 beforeRouteEnter
  • 调用全局解析守卫 beforeResolve
  • 导航被确认。
  • 调用全局后置钩子的 afterEach 钩子。
  • 触发DOM更新(mounted)。
  • 执行beforeRouteEnter 守卫中传给 next 的回调函数
  1. 触发钩子的完整顺序

路由导航、keep-alive、和组件生命周期钩子结合起来的,触发顺序,假设是从a组件离开,第一次进入b组件∶

  • beforeRouteLeave:路由组件的组件离开路由前钩子,可取消路由离开。
  • beforeEach:路由全局前置守卫,可用于登录验证、全局路由loading等。
  • beforeEnter:路由独享守卫
  • beforeRouteEnter:路由组件的组件进入路由前钩子。
  • beforeResolve:路由全局解析守卫
  • afterEach:路由全局后置钩子
  • beforeCreate:组件生命周期,不能访问tAis。
  • created;组件生命周期,可以访问tAis,不能访问dom。
  • beforeMount:组件生命周期
  • deactivated:离开缓存组件a,或者触发a的beforeDestroy和destroyed组件销毁钩子。
  • mounted:访问/操作dom。
  • activated:进入缓存组件,进入a的嵌套子组件(如果有的话)。
  • 执行beforeRouteEnter回调函数next。
  1. 导航行为被触发到导航完成的整个过程
  • 导航行为被触发,此时导航未被确认。
  • 在失活的组件里调用离开守卫 beforeRouteLeave。
  • 调用全局的 beforeEach守卫。
  • 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
  • 在路由配置里调用 beforeEnteY。
  • 解析异步路由组件(如果有)。
  • 在被激活的组件里调用 beforeRouteEnter。
  • 调用全局的 beforeResolve 守卫(2.5+),标示解析阶段完成。
  • 导航被确认。
  • 调用全局的 afterEach 钩子。
  • 非重用组件,开始组件实例的生命周期:beforeCreate&created、beforeMount&mounted
  • 触发 DOM 更新。
  • 用创建好的实例调用 beforeRouteEnter守卫中传给 next 的回调函数。
  • 导航完成

45.Vue-router 导航守卫有哪些

  • 全局前置/钩子:beforeEach、beforeResolve、afterEach
  • 路由独享的守卫:beforeEnter
  • 组件内的守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave

46.mixin 和 mixins 区别

mixin 用于全局混入,会影响到每个组件实例,通常插件都是这样做初始化的。

Vue.mixin({
  beforeCreate() {
    // ...逻辑        // 这种方式会影响到每个组件的 beforeCreate 钩子函数
  },
});

虽然文档不建议在应用中直接使用 mixin,但是如果不滥用的话也是很有帮助的,比如可以全局混入封装好的 ajax 或者一些工具函数等等。

mixins 应该是最常使用的扩展组件的方式了。如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过 mixins 混入代码,比如上拉下拉加载数据这种逻辑等等。
另外需要注意的是 mixins 混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并。

47.vuex是什么?怎么使用?哪种功能场景使用它?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。vuex 就是一个仓库,仓库里放了很多对象。其中 state 就是数据源存放地,对应于一般 vue 对象里面的 data 里面存放的数据是响应式的,vue 组件从 store 读取数据,若是 store 中的数据发生改变,依赖这相数据的组件也会发生更新它通过 mapState 把全局的 stategetters 映射到当前组件的 computed 计算属性

  • vuex 一般用于中大型 web 单页应用中对应用的状态进行管理,对于一些组件间关系较为简单的小型应用,使用 vuex 的必要性不是很大,因为完全可以用组件 prop 属性或者事件来完成父子组件之间的通信,vuex 更多地用于解决跨组件通信以及作为数据中心集中式存储数据。
  • 使用Vuex解决非父子组件之间通信问题 vuex 是通过将 state 作为数据中心、各个组件共享 state 实现跨组件通信的,此时的数据完全独立于组件,因此将组件间共享的数据置于 State 中能有效解决多层级组件嵌套的跨组件通信问题

vuexState 在单页应用的开发中本身具有一个“数据库”的作用,可以将组件中用到的数据存储在 State 中,并在 Action 中封装数据读写的逻辑。这时候存在一个问题,一般什么样的数据会放在 State 中呢? 目前主要有两种数据会使用 vuex 进行管理:

  • 组件之间全局共享的数据
  • 通过后端异步请求的数据

包括以下几个模块

  • stateVuex 使用单一状态树,即每个应用将仅仅包含一个store 实例。里面存放的数据是响应式的,vue 组件从 store 读取数据,若是 store 中的数据发生改变,依赖这相数据的组件也会发生更新。它通过 mapState 把全局的 stategetters 映射到当前组件的 computed 计算属性
  • mutations:更改Vuexstore中的状态的唯一方法是提交mutation
  • gettersgetter 可以对 state 进行计算操作,它就是 store 的计算属性虽然在组件内也可以做计算属性,但是 getters 可以在多给件之间复用如果一个状态只在一个组件内使用,是可以不用 getters
  • actionaction 类似于 muation, 不同在于:action 提交的是 mutation,而不是直接变更状态action 可以包含任意异步操作
  • modules:面对复杂的应用程序,当管理的状态比较多时;我们需要将vuexstore对象分割成模块(modules)

48.实现双向绑定

我们还是以Vue为例,先来看看Vue中的双向绑定流程是什么的

  1. new Vue()首先执行初始化,对data执行响应化处理,这个过程发生Observe
  2. 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile
  3. 同时定义⼀个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
  4. 由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher
  5. 将来data中数据⼀旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数

流程图如下:

img

先来一个构造函数:执行初始化,对data执行响应化处理

class Vue {  
  constructor(options) {  
    this.$options = options;  
    this.$data = options.data;  

    // 对data选项做响应式处理  
    observe(this.$data);  

    // 代理data到vm上  
    proxy(this);  

    // 执行编译  
    new Compile(options.el, this);  
  }  
}  

data选项执行响应化具体操作

function observe(obj) {  
  if (typeof obj !== "object" || obj == null) {  
    return;  
  }  
  new Observer(obj);  
}  

class Observer {  
  constructor(value) {  
    this.value = value;  
    this.walk(value);  
  }  
  walk(obj) {  
    Object.keys(obj).forEach((key) => {  
      defineReactive(obj, key, obj[key]);  
    });  
  }  
}  

编译Compile

对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数

img

class Compile {  
  constructor(el, vm) {  
    this.$vm = vm;  
    this.$el = document.querySelector(el);  // 获取dom  
    if (this.$el) {  
      this.compile(this.$el);  
    }  
  }  
  compile(el) {  
    const childNodes = el.childNodes;   
    Array.from(childNodes).forEach((node) => { // 遍历子元素  
      if (this.isElement(node)) {   // 判断是否为节点  
        console.log("编译元素" + node.nodeName);  
      } else if (this.isInterpolation(node)) {  
        console.log("编译插值⽂本" + node.textContent);  // 判断是否为插值文本 {{}}  
      }  
      if (node.childNodes && node.childNodes.length > 0) {  // 判断是否有子元素  
        this.compile(node);  // 对子元素进行递归遍历  
      }  
    });  
  }  
  isElement(node) {  
    return node.nodeType == 1;  
  }  
  isInterpolation(node) {  
    return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);  
  }  
}  

依赖收集

视图中会用到data中某key,这称为依赖。同⼀个key可能出现多次,每次都需要收集出来用⼀个Watcher来维护它们,此过程称为依赖收集多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知

img

实现思路

  1. defineReactive时为每⼀个key创建⼀个Dep实例
  2. 初始化视图时读取某个key,例如name1,创建⼀个watcher1
  3. 由于触发name1getter方法,便将watcher1添加到name1对应的Dep
  4. name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新
// 负责更新视图  
class Watcher {  
  constructor(vm, key, updater) {  
    this.vm = vm  
    this.key = key  
    this.updaterFn = updater  

    // 创建实例时,把当前实例指定到Dep.target静态属性上  
    Dep.target = this  
    // 读一下key,触发get  
    vm[key]  
    // 置空  
    Dep.target = null  
  }  

  // 未来执行dom更新函数,由dep调用的  
  update() {  
    this.updaterFn.call(this.vm, this.vm[this.key])  
  }  
}  

声明Dep

class Dep {  
  constructor() {  
    this.deps = [];  // 依赖管理  
  }  
  addDep(dep) {  
    this.deps.push(dep);  
  }  
  notify() {   
    this.deps.forEach((dep) => dep.update());  
  }  
} 

创建watcher时触发getter

class Watcher {  
  constructor(vm, key, updateFn) {  
    Dep.target = this;  
    this.vm[this.key];  
    Dep.target = null;  
  }  
}  

依赖收集,创建Dep实例

function defineReactive(obj, key, val) {  
  this.observe(val);  
  const dep = new Dep();  
  Object.defineProperty(obj, key, {  
    get() {  
      Dep.target && dep.addDep(Dep.target);// Dep.target也就是Watcher实例  
      return val;  
    },  
    set(newVal) {  
      if (newVal === val) return;  
      dep.notify(); // 通知dep执行更新方法  
    },  
  });  
}  

49.watch 原理

watch 本质上是为每个监听属性 setter 创建了一个 watcher,当被监听的属性更新时,调用传入的回调函数。常见的配置选项有 deepimmediate,对应原理如下

  • deep:深度监听对象,为对象的每一个属性创建一个 watcher,从而确保对象的每一个属性更新时都会触发传入的回调函数。主要原因在于对象属于引用类型,单个属性的更新并不会触发对象 setter,因此引入 deep 能够很好地解决监听对象的问题。同时也会引入判断机制,确保在多个属性更新时回调函数仅触发一次,避免性能浪费。
  • immediate:在初始化时直接调用回调函数,可以通过在 created 阶段手动调用回调函数实现相同的效果

50.Vue computed 实现

  • 建立与其他属性(如:dataStore)的联系;
  • 属性改变后,通知计算属性重新计算

实现时,主要如下

  • 初始化 data, 使用 Object.defineProperty 把这些属性全部转为 getter/setter
  • 初始化 computed, 遍历 computed 里的每个属性,每个 computed 属性都是一个 watch 实例。每个属性提供的函数作为属性的 getter,使用 Object.defineProperty 转化。
  • Object.defineProperty getter 依赖收集。用于依赖发生变化时,触发属性重新计算。
  • 若出现当前 computed 计算属性嵌套其他 computed 计算属性时,先进行其他

51.Vue中如何扩展一个组件

此题属于实践题,考察大家对vue常用api使用熟练度,答题时不仅要列出这些解决方案,同时最好说出他们异同

答题思路:

  • 按照逻辑扩展和内容扩展来列举
    • 逻辑扩展有:mixinsextendscomposition api
    • 内容扩展有slots
  • 分别说出他们使用方法、场景差异和问题。
  • 作为扩展,还可以说说vue3中新引入的composition api带来的变化

回答范例:

  1. 常见的组件扩展方法有:mixinsslotsextends
  2. 混入mixins是分发 Vue 组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项
// 复用代码:它是一个配置对象,选项和组件里面一样
const mymixin = {
   methods: {
      dosomething(){}
   }
}
// 全局混入:将混入对象传入
Vue.mixin(mymixin)

// 局部混入:做数组项设置到mixins选项,仅作用于当前组件
const Comp = {
   mixins: [mymixin]
}

3.插槽主要用于vue组件中的内容分发,也可以用于组件扩展

子组件Child

<div>
  <slot>这个内容会被父组件传递的内容替换</slot>
</div>

父组件Parent

<div>
   <Child>来自父组件内容</Child>
</div>

如果要精确分发到不同位置可以使用具名插槽,如果要使用子组件中的数据可以使用作用域插槽

4.组件选项中还有一个不太常用的选项extends,也可以起到扩展组件的目的

// 扩展对象
const myextends = {
   methods: {
      dosomething(){}
   }
}
// 组件扩展:做数组项设置到extends选项,仅作用于当前组件
// 跟混入的不同是它只能扩展单个对象
// 另外如果和混入发生冲突,该选项优先级较高,优先起作用
const Comp = {
   extends: myextends
}

5.混入的数据和方法不能明确判断来源且可能和当前组件内变量产生命名冲突,vue3中引入的composition api,可以很好解决这些问题,利用独立出来的响应式模块可以很方便的编写独立逻辑并提供响应式的数据,然后在setup选项中组合使用,增强代码的可读性和维护性。例如

// 复用逻辑1
function useXX() {}
// 复用逻辑2
function useYY() {}
// 逻辑组合
const Comp = {
   setup() {
      const {xx} = useXX()
      const {yy} = useYY()
      return {xx, yy}
   }
}

52.双向绑定的原理是什么

我们都知道 Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成

  • 数据层(Model):应用的数据及业务逻辑
  • 视图层(View):应用的展示效果,各类UI组件
  • 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来

而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理

理解ViewModel

它的主要职责就是:

  • 数据变化后更新视图
  • 视图变化后更新数据

当然,它还有两个主要部分组成

  • 监听器(Observer):对所有数据的属性进行监听
  • 解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数

53.Vue为什么需要虚拟DOM?优缺点有哪些

由于在浏览器中操作 DOM是很昂贵的。频繁的操作 DOM,会产生一定的性能问题。这就是虚拟 Dom 的产生原因。Vue2Virtual DOM 借鉴了开源库 snabbdom 的实现。Virtual DOM 本质就是用一个原生的 JS 对象去描述一个 DOM 节点,是对真实 DOM 的一层抽象

优点:

  • 保证性能下限 : 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
  • 无需手动操作 DOM : 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
  • 跨平台 : 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。

缺点:

  • 无法进行极致优化:虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
  • 首次渲染大量DOM时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢。

虚拟 DOM 实现原理?

虚拟 DOM 的实现原理主要包括以下 3 部分:

  • JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;
  • diff 算法 — 比较两棵虚拟 DOM 树的差异;
  • pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。

说说你对虚拟 DOM 的理解?回答范例

思路

  • vdom是什么
  • 引入vdom的好处
  • vdom如何生成,又如何成为dom
  • 在后续的diff中的作用

回答范例

  1. 虚拟dom顾名思义就是虚拟的dom对象,它本身就是一个 JavaScript 对象,只不过它是通过不同的属性去描述一个视图结构
  2. 通过引入vdom我们可以获得如下好处:
  • 将真实元素节点抽象成 VNode,有效减少直接操作 dom 次数,从而提高程序性能
    • 直接操作 dom 是有限制的,比如:diffclone 等操作,一个真实元素上有许多的内容,如果直接对其进行 diff 操作,会去额外 diff 一些没有必要的内容;同样的,如果需要进行 clone 那么需要将其全部内容进行复制,这也是没必要的。但是,如果将这些操作转移到 JavaScript 对象上,那么就会变得简单了
    • 操作 dom 是比较昂贵的操作,频繁的dom操作容易引起页面的重绘和回流,但是通过抽象 VNode 进行中间处理,可以有效减少直接操作dom的次数,从而减少页面重绘和回流
  • 方便实现跨平台
    • 同一 VNode 节点可以渲染成不同平台上的对应的内容,比如:渲染在浏览器是 dom 元素节点,渲染在 Native( iOS、Android)变为对应的控件、可以实现 SSR 、渲染到 WebGL 中等等
    • Vue3 中允许开发者基于 VNode 实现自定义渲染器(renderer),以便于针对不同平台进行渲染
  1. vdom如何生成?在vue中我们常常会为组件编写模板 - template, 这个模板会被编译器 - compiler编译为渲染函数,在接下来的挂载(mount)过程中会调用render函数,返回的对象就是虚拟dom。但它们还不是真正的dom,所以会在后续的patch过程中进一步转化为dom

img

  1. 挂载过程结束后,vue程序进入更新流程。如果某些响应式数据发生变化,将会引起组件重新render,此时就会生成新的vdom,和上一次的渲染结果diff就能得到变化的地方,从而转换为最小量的dom操作,高效更新视图

为什么要用vdom?案例解析

现在有一个场景,实现以下需求:

[    
  { name: "张三", age: "20", address: "北京"},    
  { name: "李四", age: "21", address: "武汉"},    
  { name: "王五", age: "22", address: "杭州"},
]

将该数据展示成一个表格,并且随便修改一个信息,表格也跟着修改。 用jQuery实现如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

<body>
  <div id="container"></div>
  <button id="btn-change">改变</button>

  <script src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script>
  <script>
    const data = [{
        name: "张三",
        age: "20",
        address: "北京"
      },
      {
        name: "李四",
        age: "21",
        address: "武汉"
      },
      {
        name: "王五",
        age: "22",
        address: "杭州"
      },
    ];
    //渲染函数
    function render(data) {
      const $container = $('#container');
      $container.html('');
      const $table = $('<table>');
      // 重绘一次
      $table.append($('<tr><td>name</td><td>age</td><td>address</td></tr>'));
      data.forEach(item => {
        //每次进入都重绘
        $table.append($(`<tr><td>${item.name}</td><td>${item.age}</td><td>${item.address}</td></tr>`))
      })
      $container.append($table);
    }

    $('#btn-change').click(function () {
      data[1].age = 30;
      data[2].address = '深圳';
      render(data);
    });
  </script>
</body>
</html>

  • 这样点击按钮,会有相应的视图变化,但是你审查以下元素,每次改动之后,table标签都得重新创建,也就是说table下面的每一个栏目,不管是数据是否和原来一样,都得重新渲染,这并不是理想中的情况,当其中的一栏数据和原来一样,我们希望这一栏不要重新渲染,因为DOM重绘相当消耗浏览器性能。
  • 因此我们采用JS对象模拟的方法,将DOM的比对操作放在JS层,减少浏览器不必要的重绘,提高效率。
  • 当然有人说虚拟DOM并不比真实的DOM快,其实也是有道理的。当上述table中的每一条数据都改变时,显然真实的DOM操作更快,因为虚拟DOM还存在jsdiff算法的比对过程。所以,上述性能优势仅仅适用于大量数据的渲染并且改变的数据只是一小部分的情况。

如下DOM结构:

<ul id="list">
    <li class="item">Item1</li>
    <li class="item">Item2</li>
</ul>

映射成虚拟DOM就是这样:

{
  tag: "ul",
  attrs: {
    id:"list"
  },
  children: [
    {
      tag: "li",
      attrs: { className: "item" },
      children: ["Item1"]
    }, {
      tag: "li",
      attrs: { className: "item" },
      children: ["Item2"]
    }
  ]
} 

使用snabbdom实现vdom

这是一个简易的实现vdom功能的库,相比vuereact,对于vdom这块更加简易,适合我们学习vdomvdom里面有两个核心的api,一个是h函数,一个是patch函数,前者用来生成vdom对象,后者的功能在于做虚拟dom的比对和将vdom挂载到真实DOM

简单介绍一下这两个函数的用法:

h('标签名', {属性}, [子元素])
h('标签名', {属性}, [文本])
patch(container, vnode) // container为容器DOM元素
patch(vnode, newVnode)

现在我们就来用snabbdom重写一下刚才的例子:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <div id="container"></div>
  <button id="btn-change">改变</button>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.min.js"></script>
  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
  <script>
    let snabbdom = window.snabbdom;

    // 定义patch
    let patch = snabbdom.init([
      snabbdom_class,
      snabbdom_props,
      snabbdom_style,
      snabbdom_eventlisteners
    ]);

    //定义h
    let h = snabbdom.h;

    const data = [{
        name: "张三",
        age: "20",
        address: "北京"
      },
      {
        name: "李四",
        age: "21",
        address: "武汉"
      },
      {
        name: "王五",
        age: "22",
        address: "杭州"
      },
    ];
    data.unshift({name: "姓名", age: "年龄", address: "地址"});

    let container = document.getElementById('container');
    let vnode;
    const render = (data) => {
      let newVnode = h('table', {}, data.map(item => { 
        let tds = [];
        for(let i in item) {
          if(item.hasOwnProperty(i)) {
            tds.push(h('td', {}, item[i] + ''));
          }
        }
        return h('tr', {}, tds);
      }));

      if(vnode) {
          patch(vnode, newVnode);
      } else {
          patch(container, newVnode);
      }
      vnode = newVnode;
    }

    render(data);

    let btnChnage = document.getElementById('btn-change');
    btnChnage.addEventListener('click', function() {
      data[1].age = 30;
      data[2].address = "深圳";
      //re-render
      render(data);
    })
  </script>
</body>
</html>

img

你会发现, 只有改变的栏目才闪烁,也就是进行重绘 ,数据没有改变的栏目还是保持原样,这样就大大节省了浏览器重新渲染的开销

vue中使用h函数生成虚拟DOM返回

const vm = new Vue({
  el: '#app',
  data: {
    user: {name:'poetry'}
  },
  render(h){
    // h()
    // h(App)
    // h('div',[])
    let vnode = h('div',{},'hello world');
    return vnode
  }
});

54.使用vue渲染大量数据时应该怎么优化?说下你的思路!

分析

企业级项目中渲染大量数据的情况比较常见,因此这是一道非常好的综合实践题目。

回答

  1. 在大型企业级项目中经常需要渲染大量数据,此时很容易出现卡顿的情况。比如大数据量的表格、树
  2. 处理时要根据情况做不同处理:
  • 可以采取分页的方式获取,避免渲染大量数据
  • vue-virtual-scroller (opens new window)等虚拟滚动方案,只渲染视口范围内的数据
  • 如果不需要更新,可以使用v-once方式只渲染一次
  • 通过v-memo (opens new window)可以缓存结果,结合v-for使用,避免数据变化时不必要的VNode创建
  • 可以采用懒加载方式,在用户需要的时候再加载数据,比如tree组件子树的懒加载

3.还是要看具体需求,首先从设计上避免大数据获取和渲染;实在需要这样做可以采用虚表的方式优化渲染;最后优化更新,如果不需要更新可以v-once处理,需要更新可以v-memo进一步优化大数据更新性能。其他可以采用的是交互方式优化,无线滚动、懒加载等方案

55.怎么缓存当前的组件?缓存后怎么更新

缓存组件使用keep-alive组件,这是一个非常常见且有用的优化手段,vue3keep-alive有比较大的更新,能说的点比较多

思路

  • 缓存用keep-alive,它的作用与用法
  • 使用细节,例如缓存指定/排除、结合routertransition
  • 组件缓存后更新可以利用activated或者beforeRouteEnter
  • 原理阐述

回答范例

  1. 开发中缓存组件使用keep-alive组件,keep-alivevue内置组件,keep-alive包裹动态组件component时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM
<keep-alive>
  <component :is="view"></component>
</keep-alive>

  1. 结合属性includeexclude可以明确指定缓存哪些组件或排除缓存指定组件。vue3中结合vue-router时变化较大,之前是keep-alive包裹router-view,现在需要反过来用router-view包裹keep-alive
<router-view v-slot="{ Component }">
  <keep-alive>
    <component :is="Component"></component>
  </keep-alive>
</router-view>

  1. 缓存后如果要获取数据,解决方案可以有以下两种
  • beforeRouteEnter:在有vue-router的项目,每次进入路由的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){
  next(vm=>{
    console.log(vm)
    // 每次进入路由执行
    vm.getData()  // 获取数据
  })
},

  • actived:在keep-alive缓存的组件被激活的时候,都会执行actived钩子
activated(){
    this.getData() // 获取数据
},

  1. keep-alive是一个通用组件,它内部定义了一个map,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component组件对应组件的vnode,如果该组件在map中存在就直接返回它。由于componentis属性是个响应式数据,因此只要它变化,keep-aliverender函数就会重新执行

56.Vue.extend 作用和原理

官方解释:Vue.extend 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。

其实就是一个子类构造器 是 Vue 组件的核心 api 实现思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 mergeOptions 把传入组件的 options 和父类的 options 进行了合并

  • extend是构造一个组件的语法器。然后这个组件你可以作用到Vue.component这个全局注册方法里还可以在任意vue模板里使用组件。 也可以作用到vue实例或者某个组件中的components属性中并在内部使用apple组件。
  • Vue.component你可以创建 ,也可以取组件。

相关代码如下

export default function initExtend(Vue) {
  let cid = 0; //组件的唯一标识
  // 创建子类继承Vue父类 便于属性扩展
  Vue.extend = function (extendOptions) {
    // 创建子类的构造函数 并且调用初始化方法
    const Sub = function VueComponent(options) {
      this._init(options); //调用Vue初始化方法
    };
    Sub.cid = cid++;
    Sub.prototype = Object.create(this.prototype); // 子类原型指向父类
    Sub.prototype.constructor = Sub; //constructor指向自己
    Sub.options = mergeOptions(this.options, extendOptions); //合并自己的options和父类的options
    return Sub;
  };
}

57.Vue中的过滤器了解吗?过滤器的应用场景有哪些?

过滤器实质不改变原始数据,只是对数据进行加工处理后返回过滤后的数据再进行调用处理,我们也可以理解其为一个纯函数

Vue 允许你自定义过滤器,可被用于一些常见的文本格式化

ps: Vue3中已废弃filter

如何用

vue中的过滤器可以用在两个地方:双花括号插值和 v-bind 表达式,过滤器应该被添加在 JavaScript表达式的尾部,由“管道”符号指示:

<!-- 在双花括号中 -->
{ message | capitalize }

<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>

定义filter

在组件的选项中定义本地的过滤器

filters: {
  capitalize: function (value) {
    if (!value) return ''
    value = value.toString()
    return value.charAt(0).toUpperCase() + value.slice(1)
  }
}

定义全局过滤器:

Vue.filter('capitalize', function (value) {
  if (!value) return ''
  value = value.toString()
  return value.charAt(0).toUpperCase() + value.slice(1)
})

new Vue({
  // ...
})

注意:当全局过滤器和局部过滤器重名时,会采用局部过滤器

过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。在上述例子中,capitalize 过滤器函数将会收到 message 的值作为第一个参数

过滤器可以串联:

{ message | filterA | filterB }

在这个例子中,filterA 被定义为接收单个参数的过滤器函数,表达式 message 的值将作为参数传入到函数中。然后继续调用同样被定义为接收单个参数的过滤器函数 filterB,将 filterA 的结果传递到 filterB 中。

过滤器是 JavaScript函数,因此可以接收参数:

{{ message | filterA('arg1', arg2) }}

这里,filterA 被定义为接收三个参数的过滤器函数。

其中 message 的值作为第一个参数,普通字符串 'arg1' 作为第二个参数,表达式 arg2 的值作为第三个参数

举个例子:

<div id="app">
  <p>{{ msg | msgFormat('疯狂','--')}}</p>
</div>

<script>
    // 定义一个 Vue 全局的过滤器,名字叫做  msgFormat
    Vue.filter('msgFormat', function(msg, arg, arg2) {
        // 字符串的  replace 方法,第一个参数,除了可写一个 字符串之外,还可以定义一个正则
        return msg.replace(/单纯/g, arg+arg2)
    })
</script>

小结:

  • 部过滤器优先于全局过滤器被调用
  • 一个表达式可以使用多个过滤器。过滤器之间需要用管道符“|”隔开。其执行顺序从左往右
应用场景

平时开发中,需要用到过滤器的地方有很多,比如单位转换数字打点文本格式化时间格式化之类的等

比如我们要实现将30000 => 30,000,这时候我们就需要使用过滤器

Vue.filter('toThousandFilter', function (value) {
  if (!value) return ''
  value = value.toString()
  return .replace(str.indexOf('.') > -1 ? /(\d)(?=(\d{3})+\.)/g : /(\d)(?=(?:\d{3})+$)/g, '$1,')
})

原理分析

使用过滤器

{{ message | capitalize }}

在模板编译阶段过滤器表达式将会被编译为过滤器函数,主要是用过parseFilters,我们放到最后讲

_s(_f('filterFormat')(message))

首先分析一下_f

_f 函数全名是:resolveFilter,这个函数的作用是从this.$options.filters中找出注册的过滤器并返回

// 变为
this.$options.filters['filterFormat'](message) // message为参数

关于resolveFilter

import { indentity,resolveAsset } from 'core/util/index' 

export function resolveFilter(id){
  return resolveAsset(this.$options,'filters',id,true) || identity
}

内部直接调用resolveAsset,将option对象,类型,过滤器id,以及一个触发警告的标志作为参数传递,如果找到,则返回过滤器;

resolveAsset的代码如下:

export function resolveAsset(options,type,id,warnMissing){ // 因为我们找的是过滤器,所以在 resolveFilter函数中调用时 type 的值直接给的 'filters',实际这个函数还可以拿到其他很多东西
  if(typeof id !== 'string'){ // 判断传递的过滤器id 是不是字符串,不是则直接返回
      return 
  }
  const assets = options[type]  // 将我们注册的所有过滤器保存在变量中
  // 接下来的逻辑便是判断id是否在assets中存在,即进行匹配
  if(hasOwn(assets,id)) return assets[id] // 如找到,直接返回过滤器
  // 没有找到,代码继续执行
  const camelizedId  = camelize(id) // 万一你是驼峰的呢
  if(hasOwn(assets,camelizedId)) return assets[camelizedId]
  // 没找到,继续执行
  const PascalCaseId = capitalize(camelizedId) // 万一你是首字母大写的驼峰呢
  if(hasOwn(assets,PascalCaseId)) return assets[PascalCaseId]
  // 如果还是没找到,则检查原型链(即访问属性)
  const result = assets[id] || assets[camelizedId] || assets[PascalCaseId]
  // 如果依然没找到,则在非生产环境的控制台打印警告
  if(process.env.NODE_ENV !== 'production' && warnMissing && !result){
    warn('Failed to resolve ' + type.slice(0,-1) + ': ' + id, options)
  }
  // 无论是否找到,都返回查找结果
  return result
}

下面再来分析一下_s

_s 函数的全称是 toString,过滤器处理后的结果会当作参数传递给 toString函数,最终 toString函数执行后的结果会保存到Vnode中的text属性中,渲染到视图中

function toString(value){
  return value == null
  ? ''
  : typeof value === 'object'
    ? JSON.stringify(value,null,2)// JSON.stringify()第三个参数可用来控制字符串里面的间距
    : String(value)
}

最后,在分析下parseFilters,在模板编译阶段使用该函数阶段将模板过滤器解析为过滤器函数调用表达式

function parseFilters (filter) {
    let filters = filter.split('|')
    let expression = filters.shift().trim() // shift()删除数组第一个元素并将其返回,该方法会更改原数组
    let i
    if (filters) {
        for(i = 0;i < filters.length;i++){
            experssion = warpFilter(expression,filters[i].trim()) // 这里传进去的expression实际上是管道符号前面的字符串,即过滤器的第一个参数
        }
    }
    return expression
}
// warpFilter函数实现
function warpFilter(exp,filter){
    // 首先判断过滤器是否有其他参数
    const i = filter.indexof('(')
    if(i<0){ // 不含其他参数,直接进行过滤器表达式字符串的拼接
        return `_f("${filter}")(${exp})`
    }else{
        const name = filter.slice(0,i) // 过滤器名称
        const args = filter.slice(i+1) // 参数,但还多了 ‘)’
        return `_f('${name}')(${exp},${args}` // 注意这一步少给了一个 ')'
    }
}

小结:

  • 在编译阶段通过parseFilters将过滤器编译成函数调用(串联过滤器则是一个嵌套的函数调用,前一个过滤器执行的结果是后一个过滤器函数的参数)
  • 编译后通过调用resolveFilter函数找到对应过滤器并返回结果
  • 执行结果作为参数传递给toString函数,而toString执行后,其结果会保存在Vnodetext属性中,渲染到视图

58.组件通信

组件通信的方式如下:

(1) props / $emit

父组件通过props向子组件传递数据,子组件通过$emit和父组件通信

1. 父组件向子组件传值
  • props只能是父组件向子组件进行传值,props使得父子组件之间形成了一个单向下行绑定。子组件的数据会随着父组件不断更新。
  • props 可以显示定义一个或一个以上的数据,对于接收的数据,可以是各种数据类型,同样也可以传递一个函数。
  • props属性名规则:若在props中使用驼峰形式,模板中需要使用短横线的形式
// 父组件
<template>
  <div id="father">
    <son :msg="msgData" :fn="myFunction"></son>
  </div>
</template>

<script>
import son from "./son.vue";
export default {
  name: father,
  data() {
    msgData: "父组件数据";
  },
  methods: {
    myFunction() {
      console.log("vue");
    },
  },
  components: { son },
};
</script>
// 子组件
<template>
  <div id="son">
    <p>{{ msg }}</p>
    <button @click="fn">按钮</button>
  </div>
</template>
<script>
export default { name: "son", props: ["msg", "fn"] };
</script>
2. 子组件向父组件传值
  • $emit绑定一个自定义事件,当这个事件被执行的时就会将参数传递给父组件,而父组件通过v-on监听并接收参数。
// 父组件
<template>
  <div class="section">
    <com-article
      :articles="articleList"
      @onEmitIndex="onEmitIndex"
    ></com-article>
    <p>{{ currentIndex }}</p>
  </div>
</template>

<script>
import comArticle from "./test/article.vue";
export default {
  name: "comArticle",
  components: { comArticle },
  data() {
    return { currentIndex: -1, articleList: ["红楼梦", "西游记", "三国演义"] };
  },
  methods: {
    onEmitIndex(idx) {
      this.currentIndex = idx;
    },
  },
};
</script>
//子组件
<template>
  <div>
    <div
      v-for="(item, index) in articles"
      :key="index"
      @click="emitIndex(index)"
    >
      {{ item }}
    </div>
  </div>
</template>

<script>
export default {
  props: ["articles"],
  methods: {
    emitIndex(index) {
      this.$emit("onEmitIndex", index); // 触发父组件的方法,并传递参数index
    },
  },
};
</script>

(2)eventBus事件总线($emit / $on

eventBus事件总线适用于父子组件非父子组件等之间的通信,使用步骤如下: (1)创建事件中心管理组件之间的通信

// event-bus.js

import Vue from 'vue'
export const EventBus = new Vue()

(2)发送事件 假设有两个兄弟组件firstComsecondCom

<template>
  <div>
    <first-com></first-com>
    <second-com></second-com>
  </div>
</template>

<script>
import firstCom from "./firstCom.vue";
import secondCom from "./secondCom.vue";
export default { components: { firstCom, secondCom } };
</script>

firstCom组件中发送事件:

<template>
  <div>
    <button @click="add">加法</button>
  </div>
</template>

<script>
import { EventBus } from "./event-bus.js"; // 引入事件中心

export default {
  data() {
    return { num: 0 };
  },
  methods: {
    add() {
      EventBus.$emit("addition", { num: this.num++ });
    },
  },
};
</script>

(3)接收事件secondCom组件中发送事件:

<template>
  <div>求和: {{ count }}</div>
</template>

<script>
import { EventBus } from "./event-bus.js";
export default {
  data() {
    return { count: 0 };
  },
  mounted() {
    EventBus.$on("addition", (param) => {
      this.count = this.count + param.num;
    });
  },
};
</script>

在上述代码中,这就相当于将num值存贮在了事件总线中,在其他组件中可以直接访问。事件总线就相当于一个桥梁,不用组件通过它来通信。

虽然看起来比较简单,但是这种方法也有不变之处,如果项目过大,使用这种方式进行通信,后期维护起来会很困难。

(3)依赖注入(provide / inject)

这种方式就是Vue中的依赖注入,该方法用于父子组件之间的通信。当然这里所说的父子不一定是真正的父子,也可以是祖孙组件,在层数很深的情况下,可以使用这种方法来进行传值。就不用一层一层的传递了。

provide / inject是Vue提供的两个钩子,和datamethods是同级的。并且provide的书写形式和data一样。

  • provide 钩子用来发送数据或方法
  • inject钩子用来接收数据或方法

在父组件中:

provide() { 
    return {     
        num: this.num  
    };
}

在子组件中:

inject: ['num']

还可以这样写,这样写就可以访问父组件中的所有属性:

provide() {
 return {
    app: this
  };
}
data() {
 return {
    num: 1
  };
}

inject: ['app']
console.log(this.app.num)

注意: 依赖注入所提供的属性是非响应式的。

(3)ref / $refs

这种方式也是实现父子组件之间的通信。

ref: 这个属性用在子组件上,它的引用就指向了子组件的实例。可以通过实例来访问组件的数据和方法。

在子组件中:

export default {
  data () {
    return {
      name: 'JavaScript'
    }
  },
  methods: {
    sayHello () {
      console.log('hello')
    }
  }
}

在父组件中:

<template>
  <child ref="child"></component-a>
</template>
<script>
import child from "./child.vue";
export default {
  components: { child },
  mounted() {
    console.log(this.$refs.child.name); // JavaScript
    this.$refs.child.sayHello(); // hello
  },
};
</script>

(4)$parent / $children

  • 使用$parent可以让组件访问父组件的实例(访问的是上一级父组件的属性和方法)
  • 使用$children可以让组件访问子组件的实例,但是,$children并不能保证顺序,并且访问的数据也不是响应式的。

在子组件中:

<template>
  <div>
    <span>{{ message }}</span>
    <p>获取父组件的值为: {{ parentVal }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return { message: "Vue" };
  },
  computed: {
    parentVal() {
      return this.$parent.msg;
    },
  },
};
</script>

在父组件中:

// 父组件中
<template>
  <div class="hello_world">
    <div>{{ msg }}</div>
    <child></child>
    <button @click="change">点击改变子组件值</button>
  </div>
</template>

<script>
import child from "./child.vue";
export default {
  components: { child },
  data() {
    return { msg: "Welcome" };
  },
  methods: {
    change() {
      // 获取到子组件
      this.$children[0].message = "JavaScript";
    },
  },
};
</script>

在上面的代码中,子组件获取到了父组件的parentVal值,父组件改变了子组件中message的值。 需要注意:

  • 通过$parent访问到的是上一级父组件的实例,可以使用$root来访问根组件的实例
  • 在组件中使用$children拿到的是所有的子组件的实例,它是一个数组,并且是无序的
  • 在根组件#app上拿$parent得到的是new Vue()的实例,在这实例上再拿$parent得到的是undefined,而在最底层的子组件拿$children是个空数组
  • $children 的值是数组,而$parent是个对象

(5)$attrs / $listeners

考虑一种场景,如果A是B组件的父组件,B是C组件的父组件。如果想要组件A给组件C传递数据,这种隔代的数据,该使用哪种方式呢?

如果是用props/$emit来一级一级的传递,确实可以完成,但是比较复杂;如果使用事件总线,在多人开发或者项目较大的时候,维护起来很麻烦;如果使用Vuex,的确也可以,但是如果仅仅是传递数据,那可能就有点浪费了。

针对上述情况,Vue引入了$attrs / $listeners,实现组件之间的跨代通信。

先来看一下inheritAttrs,它的默认值true,继承所有的父组件属性除props之外的所有属性;inheritAttrs:false 只继承class属性 。

  • $attrs:继承所有的父组件属性(除了prop传递的属性、class 和 style ),一般用在子组件的子元素上
  • $listeners:该属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以配合 v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)

A组件(APP.vue):

<template>
  <div id="app">
    //此处监听了两个事件,可以在B组件或者C组件中直接触发
    <child1
      :p-child1="child1"
      :p-child2="child2"
      @test1="onTest1"
      @test2="onTest2"
    ></child1>
  </div>
</template>
<script>
import Child1 from "./Child1.vue";
export default {
  components: { Child1 },
  methods: {
    onTest1() {
      console.log("test1 running");
    },
    onTest2() {
      console.log("test2 running");
    },
  },
};
</script>

B组件(Child1.vue):

<template>
  <div class="child-1">
    <p>props: {{ pChild1 }}</p>
    <p>$attrs: {{ $attrs }}</p>
    <child2 v-bind="$attrs" v-on="$listeners"></child2>
  </div>
</template>
<script>
import Child2 from "./Child2.vue";
export default {
  props: ["pChild1"],
  components: { Child2 },
  inheritAttrs: false,
  mounted() {
    this.$emit("test1"); // 触发APP.vue中的test1方法
  },
};
</script>

C 组件 (Child2.vue):

<template>
  <div class="child-2">
    <p>props: {{ pChild2 }}</p>
    <p>$attrs: {{ $attrs }}</p>
  </div>
</template>
<script>
export default {
  props: ["pChild2"],
  inheritAttrs: false,
  mounted() {
    this.$emit("test2"); // 触发APP.vue中的test2方法
  },
};
</script>

在上述代码中:

  • C组件中能直接触发test的原因在于 B组件调用C组件时 使用 v-on 绑定了$listeners 属性
  • 在B组件中通过v-bind 绑定$attrs属性,C组件可以直接获取到A组件中传递下来的props(除了B组件中props声明的)

(6)总结

(1)父子组件间通信

  • 子组件通过 props 属性来接受父组件的数据,然后父组件在子组件上注册监听事件,子组件通过 emit 触发事件来向父组件发送数据。
  • 通过 ref 属性给子组件设置一个名字。父组件通过 $refs 组件名来获得子组件,子组件通过 $parent 获得父组件,这样也可以实现通信。
  • 使用 provide/inject,在父组件中通过 provide提供变量,在子组件中通过 inject 来将变量注入到组件中。不论子组件有多深,只要调用了 inject 那么就可以注入 provide中的数据。

(2)兄弟组件间通信

  • 使用 eventBus 的方法,它的本质是通过创建一个空的 Vue 实例来作为消息传递的对象,通信的组件引入这个实例,通信的组件通过在这个实例上监听和触发事件,来实现消息的传递。
  • 通过 $parent/$refs 来获取到兄弟组件,也可以进行通信。

(3)任意组件之间

  • 使用 eventBus ,其实就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。

如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候采用上面这一些方法可能不利于项目的维护。这个时候可以使用 vuex ,vuex 的思想就是将这一些公共的数据抽离出来,将它作为一个全局的变量来管理,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的。

58.什么是 mixin ?

  • Mixin 使我们能够为 Vue 组件编写可插拔和可重用的功能。
  • 如果希望在多个组件之间重用一组组件选项,例如生命周期 hook、 方法等,则可以将其编写为 mixin,并在组件中简单的引用它。
  • 然后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。

59.Vue.mixin的使用场景和原理

  • 在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立,可以通过 Vuemixin 功能抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用 mergeOptions 方法进行合并,采用策略模式针对不同的属性进行合并。当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”;如果混入的数据和本身组件的数据冲突,会以组件的数据为准
  • mixin有很多缺陷如:命名冲突、依赖问题、数据来源问题

基本使用

<script>
    // Vue.options
    Vue.mixin({ // 如果他是对象 每个组件都用mixin里的对象进行合并
        data(){
            return {a: 1,b: 2}
        }
    });
    // Vue.extend
    Vue.component('my',{ // 组件必须是函数 Vue.extend  => render(xxx)
        data(){
            return {x:1}
        }
    }) 
    // 没有 new 没有实例  _init()
    // const vm = this
    new Vue({
        el:'#app',
        data(){ // 根可以不是函数 
            return {c:3}
        }
    })
</script>

相关源码

export default function initMixin(Vue){
  Vue.mixin = function (mixin) {
    //   合并对象
      this.options=mergeOptions(this.options,mixin)
  };
}
};

// src/util/index.js
// 定义生命周期
export const LIFECYCLE_HOOKS = [
  "beforeCreate",
  "created",
  "beforeMount",
  "mounted",
  "beforeUpdate",
  "updated",
  "beforeDestroy",
  "destroyed",
];

// 合并策略
const strats = {};
// mixin核心方法
// mixin核心方法
export function mergeOptions(parent, child) {
  const options = {};
  // 遍历父亲
  for (let k in parent) {
    mergeFiled(k);
  }
  // 父亲没有 儿子有
  for (let k in child) {
    if (!parent.hasOwnProperty(k)) {
      mergeFiled(k);
    }
  }

  //真正合并字段方法
  function mergeFiled(k) {
    // strats合并策略
    if (strats[k]) {
      options[k] = strats[k](parent[k], child[k]);
    } else {
      // 默认策略
      options[k] = child[k] ? child[k] : parent[k];
    }
  }
  return options;
}

60.关于Vue中el,template,render,$mount的渲染

  • 渲染根节点:
    • 先判断有无el属性,有的话直接获取el根节点,没有的话调用$mount去获取根节点。
  • 渲染模板:
    • 有render:这时候优先执行render函数,render优先级 > template。
    • 无render:有template时拿template去解析成render函数的所需的格式,并使用调用render函数渲染。无template时拿el根节点的outerHTML去解析成render函数的所需的格式,并使用调用render函数渲染

渲染的方式:无论什么情况,最后都统一是要使用render函数渲染
失败

61.怎么实现路由懒加载呢

这是一道应用题。当打包应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问时才加载对应组件,这样就会更加高效

// 将
// import UserDetails from './views/UserDetails'
// 替换为
const UserDetails = () => import('./views/UserDetails')

const router = createRouter({
  // ...
  routes: [{ path: '/users/:id', component: UserDetails }],
})

回答范例

  1. 当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。利用路由懒加载我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样会更加高效,是一种优化手段
  2. 一般来说,对所有的路由都使用动态导入是个好主意
  3. component选项配置一个返回 Promise 组件的函数就可以定义懒加载路由。例如:{ path: '/users/:id', component: () => import('./views/UserDetails') }
  4. 结合注释 () => import(/* webpackChunkName: "group-user" */ './UserDetails.vue') 可以做webpack代码分块

62.Vue-router 除了 router-link 怎么实现跳转

声明式导航

<router-link to="/about">Go to About</router-link>

编程式导航

// literal string path
router.push('/users/1')

// object with path
router.push({ path: '/users/1' })

// named route with params to let the router build the url
router.push({ name: 'user', params: { username: 'test' } })

回答范例

  • vue-router导航有两种方式:声明式导航和编程方式导航
  • 声明式导航方式使用router-link组件,添加to属性导航;编程方式导航更加灵活,可传递调用router.push(),并传递path字符串或者RouteLocationRaw对象,指定pathnameparams等信息
  • 如果页面中简单表示跳转链接,使用router-link最快捷,会渲染一个a标签;如果页面是个复杂的内容,比如商品信息,可以添加点击事件,使用编程式导航
  • 实际上内部两者调用的导航函数是一样的

63.Vue中组件和插件有什么区别

1. 组件是什么

组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在Vue中每一个.vue文件都可以视为一个组件

组件的优势

  • 降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现
  • 调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单
  • 提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级

2. 插件是什么

插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:

  • 添加全局方法或者属性。如: vue-custom-element
  • 添加全局资源:指令/过滤器/过渡等。如 vue-touch
  • 通过全局混入来添加一些组件选项。如vue-router
  • 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
  • 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如vue-router

3. 两者的区别

两者的区别主要表现在以下几个方面:

  • 编写形式
  • 注册形式
  • 使用场景

3.1 编写形式

编写组件

编写一个组件,可以有很多方式,我们最常见的就是vue单文件的这种格式,每一个.vue文件我们都可以看成是一个组件

vue文件标准格式

<template>
</template>
<script>
export default{ 
    ...
}
</script>
<style>
</style>

我们还可以通过template属性来编写一个组件,如果组件内容多,我们可以在外部定义template组件内容,如果组件内容并不多,我们可直接写在template属性上

<template id="testComponent">     // 组件显示的内容
    <div>component!</div>   
</template>

Vue.component('componentA',{ 
    template: '#testComponent'  
    template: `<div>component</div>`  // 组件内容少可以通过这种形式
})

编写插件

vue插件的实现应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象

MyPlugin.install = function (Vue, options) {
  // 1. 添加全局方法或 property
  Vue.myGlobalMethod = function () {
    // 逻辑...
  }

  // 2. 添加全局资源
  Vue.directive('my-directive', {
    bind (el, binding, vnode, oldVnode) {
      // 逻辑...
    }
    ...
  })

  // 3. 注入组件选项
  Vue.mixin({
    created: function () {
      // 逻辑...
    }
    ...
  })

  // 4. 添加实例方法
  Vue.prototype.$myMethod = function (methodOptions) {
    // 逻辑...
  }
}

3.2 注册形式

组件注册

vue组件注册主要分为全局注册局部注册

全局注册通过Vue.component方法,第一个参数为组件的名称,第二个参数为传入的配置项

Vue.component('my-component-name', { /* ... */ })

局部注册只需在用到的地方通过components属性注册一个组件

const component1 = {...} // 定义一个组件

export default {
    components:{
        component1   // 局部注册
    }
}

插件注册

插件的注册通过Vue.use()的方式进行注册(安装),第一个参数为插件的名字,第二个参数是可选择的配置项

Vue.use(插件名字,{ /* ... */} )

注意的是:

注册插件的时候,需要在调用 new Vue() 启动应用之前完成

Vue.use会自动阻止多次注册相同插件,只会注册一次

4. 使用场景

  • 组件 (Component) 是用来构成你的 App 的业务模块,它的目标是 App.vue
  • 插件 (Plugin) 是用来增强你的技术栈的功能模块,它的目标是 Vue 本身

简单来说,插件就是指对Vue的功能的增强或补充

64.怎么监听vuex数据的变化

分析

  • vuex数据状态是响应式的,所以状态变视图跟着变,但是有时还是需要知道数据状态变了从而做一些事情。
  • 既然状态都是响应式的,那自然可以watch,另外vuex也提供了订阅的API:store.subscribe()

回答范例

  1. 我知道几种方法:
  • 可以通过watch选项或者watch方法监听状态
  • 可以使用vuex提供的API:store.subscribe()
  1. watch选项方式,可以以字符串形式监听$store.state.xxsubscribe方式,可以调用store.subscribe(cb),回调函数接收mutation对象和state对象,这样可以进一步判断mutation.type是否是期待的那个,从而进一步做后续处理。
  2. watch方式简单好用,且能获取变化前后值,首选;subscribe方法会被所有commit行为触发,因此还需要判断mutation.type,用起来略繁琐,一般用于vuex插件中

实践

watch方式

const app = createApp({
    watch: {
      '$store.state.counter'() {
        console.log('counter change!');
      }
    }
})

subscribe方式:

store.subscribe((mutation, state) => {
    if (mutation.type === 'add') {
      console.log('counter change in subscribe()!');
    }
})

react

1.如何创建一个react的项目(使用脚手架)

  • 安装cr脚手架:npm install -g create-react-app
  • 进入文件夹:create-react-app 项目名称
  • 进入项目:cd 项目名称
  • 运行项目:npm start

2.如何不使用脚手架创建一个项目

  1. yarn init 初始化package.json文件
  2. 安装react和react-dom
  3. 配置webpack
    • 配置babel支持ES6
    • 配置@babel/preset-react支持react
    • 支持ts:ts-loader @types/react @types/react-dom
    • 支持antd
    • 支持less:less-loader,css-loader,style-loader
    • 配置plugins,常用的有html-webpack-plugin(当使用 webpack 打包时,创建一个 html 文件,并把 webpack 打包后的静态文件自动插入到这个 html 文件当中。)和 clean-webpack-plugin(是一个清除文件的插件。 在每次打包后,磁盘空间会存有打包后的资源,在再次打包的时候,我们需要先把本地已有的打包后的资源清空,来减少它们对磁盘空间的占用。 插件clean-webpack-plugin就可以帮我们做这个事情)
  4. 安装router
  5. 安装redux

3.对于React 框架的理解(React的特性有哪些)

React是一个用于构建用户界面的 JavaScript 库,只提供了 UI 层面的解决方案。

它有以下特性:

  • 组件化:将界面成了各个独立的小块,每一个块就是组件,这些组件之间可以组合、嵌套,构成整体页面,提高代码的复用率和开发效率。

  • 数据驱动视图:

    • React通过setState实现数据驱动视图,通过setState来引发一次组件的更新过程从而实现页面的重新渲染。
    • 数据驱动视图是我们只需要关注数据的变化,不用再去操作dom。同时也提升了性能。
  • JSX 语法:用于声明组件结构,是一个 JavaScript 的语法扩展。

  • 单向数据绑定:从高阶组件到低阶组件的单向数据流,单向响应的数据流会比双向绑定的更安全,速度更快

  • 虚拟 DOM:使用虚拟 DOM 来有效地操作 DOM

  • 声明式编程:

    如实现一个标记的地图: 通过命令式创建地图、创建标记、以及在地图上添加的标记的步骤如下:

    // 创建地图
    const map = new Map.map(document.getElementById("map"), {
      zoom: 4,
      center: { lat, lng },
    });
    
    // 创建标记
    const marker = new Map.marker({
      position: { lat, lng },
      title: "Hello Marker",
    });
    
    // 地图上添加标记
    marker.setMap(map);
    

    而用 React 实现上述功能则如下:

    <Map zoom={4} center={(lat, lng)}>
      <Marker position={(lat, lng)} title={"Hello Marker"} />
    </Map>
    

    声明式编程方式使得 React 组件很容易使用,最终的代码简单易于维护

4.对于React虚拟DOM的理解

  • js对象,保存在内存中
  • 是对真实DOM结构的映射

虚拟 DOM 的工作流程:

挂载阶段:React 将结合 JSX 的描述,构建出虚拟 DOM 树,然后通过 ReactDOM.render 实现虚拟 DOM 到真实 DOM 的映射(触发渲染流水线);

更新阶段:页面的变化先作用于虚拟 DOM,虚拟 DOM 将在 JS 层借助算法先对比出具体有哪些真实 DOM 需要被改变,然后再将这些改变作用于真实 DOM。

虚拟 DOM 解决的关键问题有以下三个:

  • 减少 DOM 操作:虚拟 DOM 可以将多次 DOM 操作合并为一次操作
  • 研发体验/研发效率的问题:虚拟 DOM 的出现,为数据驱动视图这一思想提供了高度可用的载体,使得前端开发能够基于函数式 UI 的编程方式实现高效的声明式编程。
  • 跨平台的问题:虚拟 DOM 是对真实渲染内容的一层抽象。同一套虚拟 DOM,可以对接不同平台的渲染逻辑,从而实现“一次编码,多端运行”

既然是虚拟 DOM,那就意味着它和渲染到页面上的真实 DOM 之间还有一定的距离,这个距离通过 ReactDOM.render 方法填充:

ReactDOM.render(
    // 需要渲染的元素(ReactElement)
    element, 
    // 元素挂载的目标容器(一个真实DOM)
    container,
    // 回调函数,可选参数,可以用来处理渲染结束后的逻辑
    [callback]
)

5.VDOM 和 DOM 的区别

  • 真实DOM存在重排和重绘,虚拟DOM不存在;
  • 虚拟 DOM 的总损耗是“虚拟 DOM 增删改+真实 DOM 差异增删改+排版与重绘(可能比直接操作真实DOM要少)”,真实 DOM 的总损耗是“真实 DOM 完全增删改+排版与重绘”

传统的原生 api 或 jQuery 去操作 DOM 时,浏览器会从构建 DOM 树开始从头到尾执行一遍流程。

当你在一次操作时,需要更新 10 个 DOM 节点,浏览器没这么智能,收到第一个更新 DOM 请求后,并不知道后续还有 9 次更新操作,因此会马上执行流程,最终执行 10 次流程。

而通过 VNode,同样更新 10 个 DOM 节点,虚拟 DOM 不会立即操作 DOM,而是将这 10 次更新的 diff 内容保存到本地的一个 js 对象中,最终将这个 js 对象一次性 attach 到 DOM 树上,避免大量的无谓计算。

6.VDOM 和 DOM 优缺点

真实 DOM 的优势:

  • 易用

真实 DOM 的缺点:

  • 效率低,解析速度慢,内存占用量过高
  • 性能差:频繁操作真实 DOM,易于导致重绘与回流

虚拟 DOM 的优势:

  • 简单方便:如果使用手动操作真实 DOM 来完成页面,繁琐又容易出错,在大规模应用下维护起来也很困难
  • 性能方面:使用 Virtual DOM,能够有效避免真实 DOM 数频繁更新,减少多次引起重绘与回流,提高性能
  • 跨平台:React 借助虚拟 DOM,带来了跨平台的能力,一套代码多端运行

虚拟 DOM 的缺点:

  • 在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化,首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,速度比正常稍慢

7.react 的生命周期

react生命周期图解

初始化阶段:

  • getDefaultProps:获取实例的默认属性
  • getInitialState:获取每个实例的初始化状态
  • componentWillMount:组件即将被装载、渲染到页面上
  • render:组件在这里生成虚拟的 DOM 节点
  • componentDidMount:组件真正在被装载之后

运行中状态:

  • componentWillReceiveProps:组件将要接收到属性的时候调用
  • shouldComponentUpdate:组件接受到新属性或者新状态的时候(可以返回 false,接收数据后不更新,阻止 render 调用,后面的函数不会被继续执行了)
  • componentWillUpdate:组件即将更新不能修改属性和状态
  • render:组件重新描绘
  • componentDidUpdate:组件已经更新

销毁阶段:

  • componentWillUnmount:组件即将销毁
挂载

当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:

  • constructor()
  • static getDerivedStateFromProps()
  • render()
  • componentDidMount()
getDerivedStateFromProps

该方法是新增的生命周期方法,是一个静态的方法,因此不能访问到组件的实例

执行时机:组件创建和更新阶段,不论是props变化还是state变化,都会调用。

在每次render方法前调用,第一个参数为即将更新的props,第二个参数为上一个状态的state,可以比较props 和 state来加一些限制条件,防止无用的state更新

该方法需要返回一个新的对象作为新的state或者返回null表示state状态不需要更新

更新

当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:

  • static getDerivedStateFromProps()
  • shouldComponentUpdate()
  • render()
  • getSnapshotBeforeUpdate()
  • componentDidUpdate()
getSnapshotBeforeUpdate

该周期函数在render后执行,执行之时DOM元素还没有被更新

该方法返回的一个Snapshot值(不返回报错),作为componentDidUpdate第三个参数传入

getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log('#enter getSnapshotBeforeUpdate');
    return 'foo';
}

componentDidUpdate(prevProps, prevState, snapshot) {
    console.log('#enter componentDidUpdate snapshot = ', snapshot);
}

此方法的目的在于获取组件更新前的一些信息,比如组件的滚动位置之类的,在组件更新后可以根据这些信息恢复一些UI视觉上的状态

卸载

当组件从 DOM 中移除时会调用如下方法:

  • componentWillUnmount()
错误处理

当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:

  • static getDerivedStateFromError():更改状态,从而显示降级组件
  • componentDidCatch():打印错误信息

8.state和props有什么区别

一个组件的数据可以来源于组件内部,也可以来源于组件外部(比如父组件)。

组件内部的状态就是state,一般在constructor中定义。通过setState修改,会调用render方法重新渲染组件。 setState 还可以接受第二个参数,它是一个函数,会在 setState 调用完成并且组件开始重新渲染时被调用,可以用来监听渲染是否完成。

组件外部定义的状态是props,组件中的props不可以修改,只能通过传入新的props。

相同点:

  • 两者都是 JavaScript 对象
  • 两者都是用于保存状态
  • props 和 state 都能触发渲染更新

区别:

  • props 是外部传递给组件的,而 state 是在组件内被组件自己管理的,一般在 constructor 中初始化
  • props 在组件内部是不可修改的,但 state 在组件内部可以进行修改 state 是多变的、可以修改

9.super和super(props)的区别

在ES6的class中:

class sup {
  constructor(name) {
    this.name = name;
  }

  printName() {
    console.log(this.name);
  }
}

class sub extends sup {
  constructor(name, age) {
    super(name); // super代表的是父类的构造函数
    this.age = age;
  }

  printAge() {
    console.log(this.age);
  }
}

let jack = new sub("jack", 20);
jack.printName(); //输出 : jack
jack.printAge(); //输出 : 20

在上面的例子中,可以看到通过 super 关键字实现调用父类,super 代替的是父类的构建函数,使用 super(name) 相当于调用sup.prototype.constructor.call(this,name)

如果在子类中不使用 super关键字,则会引发报错,报错的原因是子类是没有自己的 this 对象的,它只能继承父类的 this 对象,然后对其进行加工。

而 super() 就是将父类中的 this 对象继承给子类的,没有 super() 子类就得不到 this 对象。

如果先调用 this,再初始化 super(),同样是禁止的行为。所以在子类 constructor 中,必须先代用 super 才能引用 this。

在 React 中,类组件是基于 ES6 的规范实现的,继承 React.Component,因此如果用到 constructor 就必须写 super() 才初始化 this。

这时候,在调用 super() 的时候,我们一般都需要传入 props 作为参数,如果不传进去,React 内部也会将其定义在组件实例中。 所以无论有没有 constructor,在 render 中 this.props 都是可以使用的,这是 React 自动附带的,是可以不写的。

综上所述:

  • 在 React 中,类组件基于 ES6,所以在 constructor 中必须使用 super
  • 在调用 super 过程,无论是否传入 props,React 内部都会将 porps 赋值给组件实例 porps 属性中
  • 如果只调用了 super(),那么 this.props 在 super() 和构造函数结束之间仍是 undefined

10.react引入css的方式有哪些

组件式开发选择合适的css解决方案尤为重要

通常会遵循以下规则:

  • 可以编写局部css,不会随意污染其他组件内的原生;
  • 可以编写动态的css,可以获取当前组件的一些状态,根据状态的变化生成不同的css样式;
  • 支持所有的css特性:伪类、动画、媒体查询等;
  • 编写起来简洁方便、最好符合一贯的css风格特点

在这一方面,vue使用css起来更为简洁:

  • 通过 style 标签编写样式
  • scoped 属性决定编写的样式是否局部有效
  • lang 属性设置预处理器
  • 内联样式风格的方式来根据最新状态设置和改变css

而在react中,引入CSS就不如Vue方便简洁,其引入css的方式有很多种,各有利弊

常见的CSS引入方式有以下:

  • 行内样式:

    <div style={{
        width:'200px',
        height:'80px',     
    }}>测试数据</div>
    
  • 组件中引入 .css 文件

  • 组件中引入 .module.css 文件

  • CSS in JS

通过上面四种样式的引入,各自的优缺点:

  • 在组件内直接使用css该方式编写方便,容易能够根据状态修改样式属性,但是大量的演示编写容易导致代码混乱
  • 组件中引入 .css 文件符合我们日常的编写习惯,但是作用域是全局的,样式之间会层叠
  • 引入.module.css 文件能够解决局部作用域问题,但是不方便动态修改样式,需要使用内联的方式进行样式的编写
  • 通过css in js 这种方法,可以满足大部分场景的应用,可以类似于预处理器一样样式嵌套、定义、修改状态等

11.react事件绑定方式有哪些

绑定方式
  • render方法中使用bind
    • test
    • 这种方式在组件每次render渲染的时候,都会重新进行bind的操作,影响性能
  • render方法中使用箭头函数
    • this.handleClick(e)}>test
    • 每一次render的时候都会生成新的方法,影响性能
  • constructor中bind:this.handleClick = this.handleClick.bind(this);
  • 定义阶段使用箭头函数绑定
区别
  • 编写方面:方式一、方式二、方式四写法简单,方式三的编写过于冗杂
  • 性能方面:方式一和方式二在每次组件render的时候都会生成新的方法实例,性能问题欠缺。若该函数作为属性值传给子组件的时候,都会导致额外的渲染。而方式三、方式四只会生成一个方法实例

综合上述,方式四是最优的事件绑定方式。

12.react组件的创建方式以及区别

创建方式
  • 函数组件:通过一个函数,return 一个jsx语法声明的结构
  • React.createClass 方法创建:语法冗余,目前已经不太使用
  • 继承 React.Component 创建的类组件:最终会被编译成createClass
区别

由于React.createClass创建的方式过于冗杂,并不建议使用。

而像函数式创建和类组件创建的区别主要在于需要创建的组件是否需要为有状态组件:对于一些无状态的组件创建,建议使用函数式创建的方式。

在考虑组件的选择原则上,能用无状态组件则用无状态组件。

不过,由于react hooks的出现,函数式组件创建的组件通过使用hooks方法也能使之成为有状态组件,再加上目前推崇函数式编程,所以这里建议都使用函数式的方式来创建组件。

13.react 中组件之间如何通信

组件传递的方式有很多种,根据传送者和接收者可以分为如下:

  • 父组件向子组件传递:props
  • 子组件向父组件传递:父组件向子组件传一个函数,然后通过这个函数的回调,拿到子组件传过来的值
  • 兄弟组件之间的通信:状态提升,在公共的父组件中进行状态定义
  • 父组件向后代组件传递:React.createContext创建一个context进行组件传递
  • 非关系组件传递:redux

14.React中key的作用

官网中对于diff有如下规则:

  • 对比不同类型的元素:当元素类型变化时,会销毁重建
  • 对比同一类型的元素:当元素类型不变时,比对及更新有改变的属性并且“在处理完当前节点之后,React 继续对子节点进行递归。”
  • 对子节点进行递归:React 使用 key 来匹配原有树上的子元素以及最新树上的子元素。若key一致,则进行更新,若key不一致,就销毁重建

15.react函数组件和类组件的区别

针对两种React组件,其区别主要分成以下几大方向:

  • 编写形式:类组件的编写形式更加的冗余
  • 状态管理:在hooks之前函数组件没有状态,在hooks提出之后,函数组件也可以维护自身的状态
  • 生命周期:函数组件没有生命周期,这是因为生命周期钩子都来自于继承的React.Component,但是可以通过useEffect实现类似生命周期的效果
  • 调用方式:函数组件通过执行函数调用,类组件通过实例化然后调用实例的render方法
  • 获取渲染的值:函数组件存在闭包陷阱,类组件不存在(Props在 React中是不可变的所以它永远不会改变,但是 this 总是可变的,以便您可以在 render 和生命周期函数中读取新版本)

16.ReactRouter 组件的理解,常用的react router组件

react-router等前端路由的原理大致相同,可以实现无刷新的条件下切换显示不同的页面。

路由的本质就是页面的URL发生改变时,页面的显示结果可以根据URL的变化而变化,但是页面不会刷新。

因此,可以通过前端路由可以实现单页(SPA)应用

react-router主要分成了几个不同的包:

  • react-router: 实现了路由的核心功能
  • react-router-dom: 基于 react-router,加入了在浏览器运行环境下的一些功能
  • react-router-native:基于 react-router,加入了 react-native 运行环境下的一些功能
  • react-router-config: 用于配置静态路由的工具库
常用组件

react-router-dom的常用的一些组件:

  • BrowserRouter、HashRouter:使用两者作为最顶层组件包裹其他组件,分别匹配history模式和hash模式
  • Route:Route用于路径的匹配,然后进行组件的渲染,对应的属性如下:
    • path 属性:用于设置匹配到的路径
    • component 属性:设置匹配到路径后,渲染的组件
    • render 属性:设置匹配到路径后,渲染的内容
    • exact 属性:开启精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件
  • Link、NavLink:通常路径的跳转是使用Link组件,最终会被渲染成a元素,其中属性to代替a标题的href属性 NavLink是在Link基础之上增加了一些样式属性,例如组件被选中时,发生样式变化,则可以设置NavLink的一下属性:
    • activeStyle:活跃时(匹配时)的样式
    • activeClassName:活跃时添加的class
  • switch:swich组件的作用适用于当匹配到第一个组件的时候,后面的组件就不应该继续匹配
  • redirect:路由的重定向
hooks

除了一些路由相关的组件之外,react-router还提供一些hooks,如下:

  • useHistory:组件内部直接访问history,无须通过props获取
  • useParams:获取路由参数
  • useLocation:返回当前 URL的 location对象
传参

路由传递参数主要分成了三种形式:

动态路由的方式(params):

路由配置:

{ path: '/detail/:id/:name', component: Detail }

路由跳转:

import { useHistory,useParams } from 'react-router-dom';
const history = useHistory();
// 跳转路由   地址栏:/detail/2/zora
history.push('/detail/2/zora')

<!--或者-->
this.props.history.push( '/detail/2/zora' )

获取参数:

// 获取路由参数
const params = useParams()  
console.log(params) // {id: "2",name:"zora"}

<!-- 或者 -->
this.props.match.params 

优点:

  • 刷新页面,参数不丢失

缺点:

  • 只能传字符串,传值过多url会变得很长
  • 参数必须在路由上配置

search传递参数

路由不需要特别配置

路由跳转:

import { useHistory } from 'react-router-dom';
const history = useHistory();
// 路由跳转  地址栏:/detail?id=2
history.push('/detail?id=2')  
// 或者
history.push({pathname:'/detail',search:'?id=2'})

获取参数:所获取的是查询字符串,所以,还需要进一步的解析,自己自行解析,也可以使用第三方模块:qs,或者nodejs里的query-string

const params = useLocation() 
<!--或者-->
this.props.location.search

优点:

  • 刷新页面,参数不丢失

缺点:

  • 只能传字符串,传值过多url会变得很长,获取参数需要自定义hooks

state传参

路由不需要单独配置

路由跳转:

import { useHistory,useLocation } from 'react-router-dom';
const history = useHistory();
const item = {id:1,name:"zora"}
// 路由跳转
history.push(`/user/role/detail`, { id: item });

<!--或者-->
this.props.history.push({pathname:"/sort ",state : { name : 'sunny' }});

获取参数:

// 参数获取
const {state} = useLocation()
console.log(state)  // {id:1,name:"zora"}

<!--或者-->
this.props.location.state 

优点:

  • 可以传对象

缺点:

  • <HashRouter> 刷新页面,参数丢失

<HashRouter> 通过state传递参数,刷新页面后参数丢失,官方建议使用< BrowserRouter>, < BrowserRouter>页面刷新参数也不会丢失

query

路由不需要特别配置

路由跳转:

this.props.history.push({pathname:"/query",query: { name : 'sunny' }});

获取参数:

 this.props.location.query.name

优势:

  • 传参优雅,传递参数可传对象;

缺点:

  • 刷新地址栏,参数丢失

17.React Router有几种模式,实现原理是什么

react Router 有四个库:

  • react router:核心库,封装了Router,Route,Switch等核心组件,实现了从路由的改变到组件的更新的核心功能,
  • react router dom:dom环境下的router。在react-router的核心基础上,添加了用于跳转的Link组件,和histoy模式下的BrowserRouter和hash模式下的HashRouter组件等。所谓BrowserRouter和HashRouter,也只不过用了history库中createBrowserHistory和createHashHistory方法
  • react router native:RN环境下的router
  • react router config

在单页应用中,一个web项目只有一个html页面,一旦页面加载完成之后,就不用因为用户的操作而进行页面的重新加载或者跳转,其特性如下:

  • 改变 url 且不让浏览器像服务器发送请求
  • 在不刷新页面的前提下动态改变浏览器地址栏中的URL地址

react router dom其中主要分成了两种模式:

  • hash 模式:在url后面加上#,如http://127.0.0.1:5500/home/#/page1
  • history 模式:允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录

React Router对应的hash模式和history模式对应的组件为:

  • HashRouter
  • BrowserRouter

这两个组件的使用都十分的简单,作为最顶层组件包裹其他组件

原理

参考

单页面应用路由实现原理是,切换url,监听url变化,从而渲染不同的页面组件。

主要的方式有history模式和hash模式。

history模式
①改变路由
history.pushState
history.pushState(state,title,path)

1 state:一个与指定网址相关的状态对象, popstate 事件触发时,该对象会传入回调函数。如果不需要可填 null。

2 title:新页面的标题,但是所有浏览器目前都忽略这个值,可填 null。

3 path:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个地址。

history.replaceState
history.replaceState(state,title,path)

参数和pushState一样,这个方法会修改当前的history对象记录, history.length 的长度不会改变。

②监听路由
popstate事件
window.addEventListener('popstate',function(e){
    /* 监听改变 */
})

同一个文档的 history 对象出现变化时,就会触发 popstate 事件 history.pushState 可以使浏览器地址改变,但是无需刷新页面。注意⚠️的是:用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件popstate 事件只会在浏览器某些行为下触发, 比如点击后退、前进按钮或者调用 history.back()、history.forward()、history.go()方法。

hash模式
①改变路由
window.location.hash

通过window.location.hash 属性获取和设置 hash 值。

hash模式下 ,history.push 底层是调用了window.location.href来改变路由。history.replace底层是调用 window.location.replace改变路由。

②监听路由
onhashchange
window.addEventListener('hashchange',function(e){
    /* 监听改变 */
})

18.react render原理,在什么时候触发

render存在两种形式:

  • 类组件中的render方法
  • 函数组件的函数本身

触发时机:

  • 类组件setState
  • 函数组件通过useState hook修改状态

一旦执行了setState就会执行render方法(无论值是否发生变化),useState 会判断当前值有无发生改变确定是否执行render方法,一旦父组件发生渲染,子组件也会渲染

19.如何提高组件的渲染效率

在之前文章中,我们了解到render的触发时机,简单来讲就是类组件通过调用setState方法, 就会导致render,父组件一旦发生render渲染,子组件一定也会执行render渲染

父组件渲染导致子组件渲染,子组件并没有发生任何改变,这时候就可以从避免无谓的渲染,具体实现的方式有如下:

  • shouldComponentUpdate:
    • 通过shouldComponentUpdate生命周期函数来比对 state和 props,确定是否要重新渲染
    • 默认情况下返回true表示重新渲染,如果不希望组件重新渲染,返回 false 即可
  • PureComponent:
    • 跟shouldComponentUpdate原理基本一致,通过对 props 和 state的浅比较结果来实现 shouldComponentUpdate
  • React.memo
    • React.memo用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与 PureComponent 十分类似。但不同的是, React.memo 只能用于函数组件
    • 如果需要深层次比较,这时候可以给memo第二个参数传递比较函数

20.react diff

跟Vue一致,React通过引入Virtual DOM的概念,极大地避免无效的Dom操作,使我们的页面的构建效率提到了极大的提升

而diff算法就是更高效地通过对比新旧Virtual DOM来找出真正的Dom变化之处

传统diff算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3),react将算法进行一个优化,复杂度降为O(n)

react中diff算法主要遵循三个层级的策略:

  • tree层级
    • DOM节点跨层级的操作不做优化,只会对相同层级的节点进行比较
    • 只有删除、创建操作,没有移动操作
  • conponent 层级
    • 如果是同一个类的组件,则会继续往下diff运算,如果不是一个类的组件,那么直接删除这个组件下的所有子节点,创建新的
  • element 层级
    • 对于比较同一层级的节点们,每个节点在对应的层级用唯一的key作为标识
    • 提供了 3 种节点操作,分别为 INSERT_MARKUP(插入)、MOVE_EXISTING (移动)和 REMOVE_NODE (删除)
    • 通过key可以准确地发现新旧集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将旧集合中节点的位置进行移动,更新为新集合中节点的位置
    • 由于dom节点的移动操作开销是比较昂贵的,在只修改文本的情况下,没有key的情况下要比有key的性能更好

22.react 性能优化的手段

  • 避免不必要的render:通过shouldComponentUpdate、PureComponent、React.memo

  • 使用 Immutable:在做react性能优化的时候,为了避免重复渲染,我们会在shouldComponentUpdate()中做对比,当返回true执行render方法。Immutable通过is方法则可以完成对比,而无需像一样通过深度比较的方式比较

  • 避免使用内联函数:每次调用render函数时都会创建一个新的函数实例

  • 事件绑定方式:避免在render函数中声明函数,通过在constructor绑定this,或者在声明函数的时候使用箭头函数

  • 使用 React Fragments 避免额外标记:用户创建新组件时,每个组件应具有单个父标签。这个额外标签除了充当父标签之外,并没有其他作用,这时候则可以使用fragement

  • 懒加载组件:从工程方面考虑,webpack存在代码拆分能力,可以为应用创建多个包,并在运行时动态加载,减少初始包的大小。而在react中使用到了Suspense和 lazy组件实现代码拆分功能,基本使用如下:

        const johanComponent = React.lazy(() => import(/* webpackChunkName: "johanComponent" */ './myAwesome.component'));
     
        export const johanAsyncComponent = props => (
          <React.Suspense fallback={<Spinner />}>
            <johanComponent {...props} />
          </React.Suspense>
        );
    
    
  • 服务端渲染:采用服务端渲染端方式,可以使用户更快的看到渲染完成的页面

23.在React项目中如何捕获错误

错误在我们日常编写代码是非常常见的

举个例子,在react项目中去编写组件内JavaScript代码错误会导致 React 的内部状态被破坏,导致整个应用崩溃,这是不应该出现的现象

作为一个框架,react也有自身对于错误的处理的解决方案。

为了解决出现的错误导致整个应用崩溃的问题,react16引用了错误边界新的概念

错误边界是一种 React 组件,这种组件可以捕获发生在其子组件树任何位置的 JavaScript 错误,并打印这些错误,同时展示降级 UI,而并不会渲染那些发生崩溃的子组件树

错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误

形成错误边界组件的两个条件:

  • 使用了 static getDerivedStateFromError()
  • 使用了 componentDidCatch()

抛出错误后,请使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch() 打印错误信息,如下:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 你同样可以将错误日志上报给服务器
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 你可以自定义降级后的 UI 并渲染
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}

然后就可以把自身组件的作为错误边界的子组件,如下:

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

下面这些情况无法捕获到异常:

  • 事件处理
  • 异步代码
  • 服务端渲染
  • 自身抛出来的错误

对于错误边界无法捕获的异常,如事件处理过程中发生问题并不会捕获到,是因为其不会在渲染期间触发,并不会导致渲染时候问题

这种情况可以使用js的try…catch…语法,如下:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    try {
      // 执行操作,如有错误则会抛出
    } catch (error) {
      this.setState({ error });
    }
  }

  render() {
    if (this.state.error) {
      return <h1>Caught an error.</h1>
    }
    return <button onClick={this.handleClick}>Click Me</button>
  }
}

除此之外还可以通过监听onerror事件:

window.addEventListener('error', function(event) { ... })

24.Redux和Vuex的异同点,以及用到的相同的思想

相同点
  • state共享数据
  • 流程一致:定义全局state,触发修改方法,修改state
  • 全局注入store
不同点:
  • redux使用的是不可变数据,而Vuex是可变的。
  • redux每次都是用新的state替换旧的state,vuex是直接修改。
  • redux在检测数据变化时是通过diff算法比较差异的;vuex是通过getter/setter来比较的
  • vuex定义了state,getter,mutation,action;redux定义了state,reducer,action
  • vuex中state统一存放,方便理解;react中state依赖reducer初始值
  • vuex的mapGetters可以快捷得到state,redux中是mapStateToProps
  • vuex同步使用mutation,异步使用action;redux同步异步都使用reducer
相同思想
  • 单一数据源
  • 变化可预测
  • MVVM思想

25.什么是状态提升

使用 react 经常会遇到几个组件需要共用状态数据的情况。这种情况下,我们最好将这部分共享的状态提升至他们最近的父组件当中进行管理。我们来看一下具体如何操作吧。

import React from 'react'
class Child_1 extends React.Component{
    constructor(props){
        super(props)
    }
    render(){
        return (
            <div>
                <h1>{this.props.value+2}</h1>
            </div> 
        )
    }
}
class Child_2 extends React.Component{
    constructor(props){
        super(props)
    }
    render(){
        return (
            <div>
                <h1>{this.props.value+1}</h1>
            </div> 
        )
    }
}
class Three extends React.Component {
    constructor(props){
        super(props)
        this.state = {
            txt:"牛逼"
        }
        this.handleChange = this.handleChange.bind(this)
    }
    handleChange(e){
        this.setState({
            txt:e.target.value
        })
    }
    render(){
       return (
            <div>
                <input type="text" value={this.state.txt} onChange={this.handleChange}/>
                <p>{this.state.txt}</p>
                <Child_1 value={this.state.txt}/>
                <Child_2 value={this.state.txt}/>
            </div>
       )
    }
}
export default Three

26.对 React context 的理解

在React中,数据传递一般使用props传递数据,维持单向数据流,这样可以让组件之间的关系变得简单且可预测,但是单项数据流在某些场景中并不适用。单纯一对的父子组件传递并无问题,但要是组件之间层层依赖深入,props就需要层层传递显然,这样做太繁琐了。

Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

可以把context当做是特定一个组件树内共享的store,用来做数据传递。简单说就是,当你不想在组件树中通过逐层传递props或者state的方式来传递数据时,可以使用Context来实现跨层级的组件数据传递。

JS的代码块在执行期间,会创建一个相应的作用域链,这个作用域链记录着运行时JS代码块执行期间所能访问的活动对象,包括变量和函数,JS程序通过作用域链访问到代码块内部或者外部的变量和函数。

假如以JS的作用域链作为类比,React组件提供的Context对象其实就好比一个提供给子组件访问的作用域,而 Context对象的属性可以看成作用域上的活动对象。由于组件 的 Context 由其父节点链上所有组件通 过 getChildContext()返回的Context对象组合而成,所以,组件通过Context是可以访问到其父组件链上所有节点组件提供的Context的属性。

27.React声明组件有哪几种方法,有什么不同?

React 声明组件的三种方式:

  • 函数式定义的无状态组件
  • ES5原生方式React.createClass定义的组件
  • ES6形式的extends React.Component定义的组件

(1)无状态函数式组件 它是为了创建纯展示组件,这种组件只负责根据传入的props来展示,不涉及到state状态的操作 组件不会被实例化,整体渲染性能得到提升,不能访问this对象,不能访问生命周期的方法

(2)ES5 原生方式 React.createClass // RFC React.createClass会自绑定函数方法,导致不必要的性能开销,增加代码过时的可能性。

(3)E6继承形式 React.Component // RCC 目前极为推荐的创建有状态组件的方式,最终会取代React.createClass形式;相对于 React.createClass可以更好实现代码复用。

无状态组件相对于于后者的区别: 与无状态组件相比,React.createClass和React.Component都是创建有状态的组件,这些组件是要被实例化的,并且可以访问组件的生命周期方法。

React.createClass与React.Component区别:

① 函数this自绑定

  • React.createClass创建的组件,其每一个成员函数的this都有React自动绑定,函数中的this会被正确设置。
  • React.Component创建的组件,其成员函数不会自动绑定this,需要开发者手动绑定,否则this不能获取当前组件实例对象。

② 组件属性类型propTypes及其默认props属性defaultProps配置不同

  • React.createClass在创建组件时,有关组件props的属性类型及组件默认的属性会作为组件实例的属性来配置,其中defaultProps是使用getDefaultProps的方法来获取默认组件属性的
  • React.Component在创建组件时配置这两个对应信息时,他们是作为组件类的属性,不是组件实例的属性,也就是所谓的类的静态属性来配置的。

③ 组件初始状态state的配置不同

  • React.createClass创建的组件,其状态state是通过getInitialState方法来配置组件相关的状态;
  • React.Component创建的组件,其状态state是在constructor中像初始化组件属性一样声明的。

27.React中constructor和getInitialState的区别?

两者都是用来初始化state的。前者是ES6中的语法,后者是ES5中的语法,新版本的React中已经废弃了该方法。

getInitialState是ES5中的方法,如果使用createClass方法创建一个Component组件,可以自动调用它的getInitialState方法来获取初始化的State对象,

var APP = React.creatClass ({
  getInitialState() {
    return { 
        userName: 'hi',
        userId: 0
     };
 }
})

React在ES6的实现中去掉了getInitialState这个hook函数,规定state在constructor中实现,如下:

Class App extends React.Component{
    constructor(props){
      super(props);
      this.state={};
    }
  }

28.React 组件中怎么做事件代理?它的原理是什么?

React基于Virtual DOM实现了一个SyntheticEvent层(合成事件层),定义的事件处理器会接收到一个合成事件对象的实例,它符合W3C标准,且与原生的浏览器事件拥有同样的接口,支持冒泡机制,所有的事件都自动绑定在最外层上。

在React底层,主要对合成事件做了两件事:

  • 事件委派: React会把所有的事件绑定到结构的最外层,使用统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部事件监听和处理函数。
  • 自动绑定: React组件中,每个方法的上下文都会指向该组件的实例,即自动绑定this为当前组件。

29.React-Router怎么设置重定向?

使用``组件实现路由的重定向:

<Switch>
  <Redirect from='/users/:id' to='/users/profile/:id'/>
  <Route path='/users/profile/:id' component={Profile}/>
</Switch>

当请求 /users/:id 被重定向去 '/users/profile/:id'

  • 属性 from: string:需要匹配的将要被重定向路径。
  • 属性 to: string:重定向的 URL 字符串
  • 属性 to: object:重定向的 location 对象
  • 属性 push: bool:若为真,重定向操作将会把新地址加入到访问历史记录里面,并且无法回退到前面的页面。

30.setState 是同步异步?为什么?实现原理?

1. setState是同步执行的

setState是同步执行的,但是state并不一定会同步更新

2. setState在React生命周期和合成事件中批量覆盖执行

在React的生命周期钩子和合成事件中,多次执行setState,会批量执行

具体表现为,多次同步执行的setState,会进行合并,类似于Object.assign,相同的key,后面的会覆盖前面的

当遇到多个setState调用时候,会提取单次传递setState的对象,把他们合并在一起形成一个新的
单一对象,并用这个单一的对象去做setState的事情,就像Object.assign的对象合并,后一个
key值会覆盖前面的key值

经过React 处理的事件是不会同步更新 this.state的. 通过 addEventListener || setTimeout/setInterval 的方式处理的则会同步更新。
为了合并setState,我们需要一个队列来保存每次setState的数据,然后在一段时间后执行合并操作和更新state,并清空这个队列,然后渲染组件。

31.如何将两个或多个组件嵌入到一个组件中?

可以通过以下方式将组件嵌入到一个组件中:

class MyComponent extends React.Component{
    render(){
        return(          
            <div>
                <h1>Hello</h1>
                <Header/>
            </div>
        );
    }
}
class Header extends React.Component{
    render(){
        return
            <h1>Header Component</h1>   
   };
}
ReactDOM.render(
    <MyComponent/>, document.getElementById('content')
);

32.React Hooks 和生命周期的关系?

函数组件 的本质是函数,没有 state 的概念的,因此不存在生命周期一说,仅仅是一个 render 函数而已。 但是引入 Hooks 之后就变得不同了,它能让组件在不使用 class 的情况下拥有 state,所以就有了生命周期的概念,所谓的生命周期其实就是 useStateuseEffect()useLayoutEffect()

即:Hooks 组件(使用了Hooks的函数组件)有生命周期,而函数组件(未使用Hooks的函数组件)是没有生命周期的

下面是具体的 class 与 Hooks 的生命周期对应关系

  • constructor:函数组件不需要构造函数,可以通过调用 **useState 来初始化 state**。如果计算的代价比较昂贵,也可以传一个函数给 useState
const [num, UpdateNum] = useState(0)
  • getDerivedStateFromProps:一般情况下,我们不需要使用它,可以在渲染过程中更新 state,以达到实现 getDerivedStateFromProps 的目的。
function ScrollView({row}) {
  let [isScrollingDown, setIsScrollingDown] = useState(false);
  let [prevRow, setPrevRow] = useState(null);
  if (row !== prevRow) {
    // Row 自上次渲染以来发生过改变。更新 isScrollingDown。
    setIsScrollingDown(prevRow !== null && row > prevRow);
    setPrevRow(row);
  }
  return `Scrolling down: ${isScrollingDown}`;
}

React 会立即退出第一次渲染并用更新后的 state 重新运行组件以避免耗费太多性能。

  • shouldComponentUpdate:可以用 **React.memo** 包裹一个组件来对它的 props 进行浅比较
const Button = React.memo((props) => {  // 具体的组件});

注意:**React.memo 等效于 **``**PureComponent**,它只浅比较 props。这里也可以使用 useMemo 优化每一个节点。

  • render:这是函数组件体本身。
  • componentDidMount, componentDidUpdateuseLayoutEffect 与它们两的调用阶段是一样的。但是,我们推荐你一开始先用 useEffect,只有当它出问题的时候再尝试使用 useLayoutEffectuseEffect 可以表达所有这些的组合。
// componentDidMount
useEffect(()=>{
  // 需要在 componentDidMount 执行的内容
}, [])
useEffect(() => { 
  // 在 componentDidMount,以及 count 更改时 componentDidUpdate 执行的内容
  document.title = `You clicked ${count} times`; 
  return () => {
    // 需要在 count 更改时 componentDidUpdate(先于 document.title = ... 执行,遵守先清理后更新)
    // 以及 componentWillUnmount 执行的内容       
  } // 当函数中 Cleanup 函数会按照在代码中定义的顺序先后执行,与函数本身的特性无关
}, [count]); // 仅在 count 更改时更新

请记得 React 会等待浏览器完成画面渲染之后才会延迟调用 ,因此会使得额外操作很方便

  • componentWillUnmount:相当于 useEffect 里面返回的 cleanup 函数
// componentDidMount/componentWillUnmount
useEffect(()=>{
  // 需要在 componentDidMount 执行的内容
  return function cleanup() {
    // 需要在 componentWillUnmount 执行的内容      
  }
}, [])
  • componentDidCatch and getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会加上。
class 组件Hooks 组件
constructoruseState
getDerivedStateFromPropsuseState 里面 update 函数
shouldComponentUpdateuseMemo
render函数本身
componentDidMountuseEffect
componentDidUpdateuseEffect
componentWillUnmountuseEffect 里面返回的函数
componentDidCatch
getDerivedStateFromError

33.为什么 useState 要使用数组而不是对象

useState 的用法:

const [count, setCount] = useState(0)

可以看到 useState 返回的是一个数组,那么为什么是返回数组而不是返回对象呢?

这里用到了解构赋值,所以先来看一下ES6 的解构赋值:

数组的解构赋值
const foo = [1, 2, 3];
const [one, two, three] = foo;
console.log(one);    // 1
console.log(two);    // 2
console.log(three);    // 3
对象的解构赋值
const user = {
  id: 888,
  name: "xiaoxin"
};
const { id, name } = user;
console.log(id);    // 888
console.log(name);    // "xiaoxin"

看完这两个例子,答案应该就出来了:

  • 如果 useState 返回的是数组,那么使用者可以对数组中的元素命名,代码看起来也比较干净
  • 如果 useState 返回的是对象,在解构对象的时候必须要和 useState 内部实现返回的对象同名,想要使用多次的话,必须得设置别名才能使用返回值

下面来看看如果 useState 返回对象的情况:

// 第一次使用
const { state, setState } = useState(false);
// 第二次使用
const { state: counter, setState: setCounter } = useState(0) 

这里可以看到,返回对象的使用方式还是挺麻烦的,更何况实际项目中会使用的更频繁。 **总结:*useState 返回的是 array 而不是 object 的原因就是为了*降低使用的复杂度,返回数组的话可以直接根据顺序解构,而返回对象的话要想使用多次就需要定义别名了。

34.如何在React中使用innerHTML

增加dangerouslySetInnerHTML属性,并且传入对象的属性名叫_html

function Component(props){
    return <div dangerouslySetInnerHTML={{_html:'<span>你好</span>'}}>
    </div>
}

小程序

1.什么是微信小程序

  • 小程序是一种全新的连接用户与服务的方式,它可以在微信内被便捷地获取和传播,同时具有出色的使用体验。
  • 寄生于宿主应用,可以调用宿主应用的其他能力,也可以调用设备本身的能力。

2.微信小程序为什么会越来越受欢迎/优点是什么?

  • 不占内存,随用随走。 小程序不需要存储空间,不用下载、注册等一系列繁琐的操作,用户使用简介方便,随用随走。
  • 成本低、利润大。 相对于APP,小程序的开发成本更低,小程序还可以结合线上线下的营销活动,争取更大的利润。
  • 宣传应用范围广。 不管是餐饮、旅游、教培、电商,还是政务,几乎每个领域都可以通过小程序实现自己的业务。而且小程序、社群、直播等多种方式都可以进行宣传,相对于传统的实体门店宣传范围更广泛。
  • 日活量丰富,用户基数大。 那微信小程序来说,小程序是基于微信的,微信的十亿日活量,带给小程序巨大的流量池,用转化更容易。

3.小程序与H5 通信方式

3.1 :小程序->H5 通过 URL 拼接参数
http://127.0.0.1:8080/test?key=123
3.2:H5->小程序wx.miniProgram.postMessage api

实现方式:

  • 引入js SDK
<script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
  • vue 项目需要安装依赖
npm install weixin-webview-jssdk
  • 小程序绑定方法
<web-view  bindmessage="bindGetMsg"></web-view>

  bindGetMsg:function(res){
        console.log('从h5页面获取到的信息----->',res)
}
  • h5端 调用wx.miniProgram.postMessage
import wx from "weixin-webview-jssdk";
wx.miniProgram.postMessage({ data: { foo: {} } });

img 优点:接入成本低

缺点:向小程序发送消息,会在特定时机(小程序后退、组件销毁、分享)触发组件的 message 事件,只能这些特定时机,基本宣布postMessage没用!因为这些时机很苛刻,不符合我们要求。反人类设计!

3.3:小程序-> H5 url 携带信息navigateTo、reLaunch、redirectTo

实现方式:

wx.miniProgram.navigateTo({
  url: '../h5/loading-page',
})

wx.miniProgram.navigateTo({
  url: '../h5/loading-page?type=aaa',
})

缺点:url 数据量有限,且需要打开界面

3.4 :内存共享(无法实现)

无法实现,原因 wx.setStorage 与localStorage 隔离

localStorage.setItem('h5key','value')

wx.setStorageSync('wx-key', 'value')

img

3.5:长连-Websocket
  • Websocket 简介:WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议;
  • 建立在 TCP 协议之上,服务器端的实现比较容易。
  • 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
  • 数据格式比较轻量,性能开销小,通信高效。
  • 可以发送文本,也可以发送二进制数据。
  • 没有同源限制,客户端可以与任意服务器通信。
  • 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

优点:可以实现实时通信

缺点:成本高,服务器压力大等;放弃此方式。

总结
  • 微信并不鼓励在小程序中大范围嵌入 H5,为了避免开发者把小程序变成“浏览器”,微信对小程序与内嵌 H5 的通讯做了诸多限制
  • 尽量使用单一方式实现,比如纯小程序原生,将h5功能移至小程序原生
  • 原生页面与 H5 之间通过 URL 进行通信
  • 不要尝试越过wx 限制
  • 不得不用混合开发时,尽量做好优化,引入骨架屏等优化方式提高用户体验感
  • 以上三种方式均未很好实现web-view 与H5双向通信

4.请谈谈微信小程序主要目录和文件的作用?

  • project.config.json:项目配置文件,用的最多的就是配置是否开启https校验
  • App.js:设置一些全局的基础数据等
  • App.json:底部tab,标题栏和路由等设置
  • App.wxss:公共样式,引入iconfont等
  • pages:里面包含一个个具体的页面
  • index.json:配置当前页面标题和引入组件
  • index.wxml:页面结构
  • index.wxss:页面样式表
  • index.js:页面的逻辑,请求和数据处理
  1. wxml 模板文件,是框架设计的一套标签预言,结合基础组件,事件系统,可以构建出页面的结构 wxss 样式文件,是一套样式语言,用于描述WXML的组件样式 js脚本逻辑文件。逻辑处理网络请求 json配置文件,小程序设置,如页面注册,页面标题及tabBar
  2. app.json是整个小程序的全局配置,包括:
    • pages:所有页面路径
    • 网络设置(网络超时事件)
    • 页面表现(页面注册)
    • window:(背景色,导航样式,默认标题)
    • 底部tab等
  3. app.js 监听并处理小程序的生命周期函数,声明全局变量
  4. app.wxss 全局配置的样式文件

5.请谈谈wxml与标准的html的异同?

  • 都是用来描述页面的结构
  • 都由标签,属性等构成
  • 标签名字不一样,且小程序标签更少,单一标签更多
  • 多了一些 wx:if 这样的属性以及{{}} 这样的表达式
  • WXML仅能在微信小程序开发者工具中预览,而HTML可以在浏览器内预览
  • 组件封装不同,WXML对组件进行了重新封装
  • 小程序运行在JS Core内,没有DOM树和windiw对象,小程序中无法使用window对象和document对象。

6.请谈谈WXSS和CSS的异同?

都是用来描述页面的样子

  • WXSS具有CSS大部分的特性,也做了一些扩充和修改
  • WXSS新增了尺寸单位,WXSS在底层支持新的尺寸单位rpx
  • WXSS仅支持部分CSS选择器
  • WXSS提供全局样式与局部样式

7.你是怎么封装微信小程序的数据请求的?

  1. 在根目录下创建util目录及api.js文件和apiConfig.js文件
  2. 在apiConfig.js封装基础的get,post和put,upload等请求方法,设置请求体,带上token和异常处理等
  3. 在api中引入apiConfig.js封装好的请求方法.根据页面数据请求的urls,设置对应的方法并导出
  4. 在具体的页面中导入或将所有的接口放在统一的js文件中并导出
  5. 在app.js中创建封装请求数据的方法
  6. 在子页面中调用封装的请求数据

8.(重要)小程序页面之间有哪些(传值)传递数据的方法?

  1. 页面跳转或重定向时,使用url带参数传递数据
  2. 使用组件模板 template传递参数
  3. 使用缓存传递参数
  4. 使用数据库传递参数
  5. 给html元素添加data-*属性来传递值,然后通过e.currentTarget.dataset或onload的param参数获取(data- 名称不能有大写字母,不可以存放对象)
  6. 设置id 的方法标识来传值,通过e.currentTarget.id获取设置的id值,然后通过设置全局对象的方式来传递数据
  7. 在navigator中添加参数数值
  8. 使用全局遍历实现数据传递

9.请谈谈小程序的双向绑定和vue的异同?

大体相同,但小程序之间this.data的属性是不可以同步到视图的,必须调用this.setData()方法

10.请谈谈小程序的生命周期函数

  • onLoad():页面加载时触发,只会调用一次,可获取当前页面路径中的参数
  • onShow():页面显示/切入前台时候触发,一般用来发送数据请求
  • onReady():页面初次渲染完成时触发,只会调用一次,代表页面已可和视图层进行交互
  • onHide():页面隐藏/切入后台时触发,如底部tab切换到其他页面或小程序切入后台等

11.简述微信小程序原理

小程序本质就是一个单页面应用,所有的页面渲染和事件处理,都在一个页面内进行,但又可以通过微信客户端调用原生的各种接口;

它的架构,是数据驱动的架构模式,它的UI和数据是分离的,所有的页面更新,都需要通过对数据的更改来实现;

它从技术讲和现有的前端开发差不多,采用JavaScript、WXML、WXSS三种技术进行开发;

功能可分为webviewAPPService两个部分:

  • webview主要用来展示UI,appservice用来处理业务逻辑,数据及接口调用,它们在两个进程中进行,通过系统层JSBridge实现通信,实现UI的渲染,事件处理;
  • appService用来处理业务逻辑、数据及接口调用;

两个部分在两个进程中运行,通过系统层JSBridge实现通信,实现UI的渲染、事件的处理等。

javaScript的代码是运行在微信App中的,因此一些h5技术的应用需要微信APP提供对应的API支持 wxml 微信自己基于xml语法开发的,因此在开发时只能使用微信提供的现有标签,html的标签是无法使用的 wxss具有css的大部分特性,但并不是所有都支持,没有详细文档

wxss的图片引入需要使用外链地址,没有body,样式可以使用import导入。

12.那些方法来提高微信小程序的应用速度?

  • 提高页面的加载速度
  • 用户行为预测
  • 减少默认的data的大小
  • 组件化方案

13.分析微信小程序的优劣势?

优势:

  • 容易上手,基础组件库比较全,基本不需要考虑兼容问题
  • 开发文档比较完善,开发社区比较活跃,支持插件式开发
  • 良好的用户体验,无需下载,通过搜索和扫一扫就可以打开,打开速度快,安卓上可以添加到桌面,与原生APP差不多
  • 开发成本比APP要低
  • 为用户提供良好的保障(小程序发布,严格是审查流程)

劣势:

  • 限制较多,页面大小不能超过1M,不能打开超过5个层级的页面
  • 样式单一,部分组件已经是成型的,样式不可修改,例如:幻灯片,导航
  • 推广面窄,不能分享朋友圈,只能通过分享给朋友,附加小程序推广
  • 依托与微信,无法开发后台管理功能
  • 后台调试麻烦,因为api接口必须https请求且公网地址
  • 真机测试,个别安卓和苹果表现迥异,例如安卓的定位功能加载很慢

14.怎么解决微信小程序的异步请求问题?

在回调函数中调用下一个组件的函数

*/app.js*/
success:function(info){
  that.apirtnCallback(info)
}

*/index.js*/
onLoad:function(){
    app.apirtnCallback = res =>{
        console.log(res)
    }
}

14. 小程序关联微信公众号如何确定用户的唯一性?

使用wx.getUserlnfo方法 withCredentials为true时,可获取encryptedData,里面有union_id,后端需要进行对称解密

15. 使用webview直接加载要注意那些事项?

  • 必须要在小程序后台使用管理员添加业务域名
  • h5页面跳转至小程序的脚步必须是1.3.1以上
  • 微信分享只可以是小程序的主名称,如要自定义分享内容,需小程序版本在1.7.1以上
  • h5的支付不可以是微信公众号的appid,必须是小程序的appid,而且用户的openid也必须是用户和小程序的

16. 小程序调用后台接口遇到那些问题?

  • 数据的大小限制,超过范围会直接导致整个小程序崩溃,除非重启小程序
  • 小程序不可以直接渲染文章内容这类型的html文本,显示需要借助插件注:插件渲染会导致页面加载变慢,建议在后台对文章内容的html进行过滤,后台直接处理批量替换p标签div标签为view标签,然后其他的标签让插件来做

17. 微信小程序如何实现下拉刷新?

用view代替scroll-view,设置onPullDownRefresh函数实现

1、先在 app.json 或 page.json 中配置 enablePullDownRefresh:true

2、page 里用 onPullDownRefresh 函数,在下拉刷新时执行

3、在下拉函数执行时发起数据请求,请求返回后,调用 wx.stopPullDownRefresh

4、停止下拉刷新的状态

18.webview中的页面怎么跳转回小程序?

// 1.
wx.miniProgram.navigateTo({
    url:'pages/login/login'+'$params'
})

//跳转到小程序导航页面
wx.miniProgram.switchTab({
    url:'/pages/index/index'
})

  
// 2.
// 首先,需要在你的html页面中引用一个js文件
<script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.3.0.js"></script>

//然后为你的按钮标签注册一个点击事件
$(".kaiqi").click(function(){
        wx.miniProgram.redirectTo({url: '/pages/indexTwo/indexTwo'})
});

// 这里的redirectTo跟小程序的wx.redirectTo()跳转页面是一样的,会关闭当前跳转到页面,换成navigateTo,跳转页面就不会关闭当前页面

18. bindtap和catchtap的区别?

  • bind事件绑定不会阻止冒泡事件向上冒泡
  • catch事件绑定可以阻止冒泡事件向上冒泡

19. (重要)简述小程序路由的区别

  • wx.navigateTo():保留当前页面,跳转到应用内的某个页面。但是不能跳到 tabbar 页面
  • wx.redirectTo():关闭当前页面,跳转到应用内的某个页面。但是不能跳转 tabbar 页面
  • wx.switchTab():跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面
  • wx.navigateBack():关闭当前页面,返回上一页面或多级页面。可通过 getCurrentPages() 获取当前的页面栈,决定需要返回几层
  • wx.reLaunch():关闭所有页面,打开到应用内的某个页面

20.小程序和Vue写法的区别?

1. 遍历的时候:
小程序:wx:for="list" 

Vue:v-for="item in list"

2. 调用data模型(赋值)的时候:
小程序: this.data.item // 调用
			  this.setDate({item:1})//赋值

Vue:   this.item //调用
        this.item=1 //赋值

21. 小程序与原生App那个好?

各有各自的优点,都又有缺点

小程序的优点:

  • 基于微信平台开发,享受微信自带的流量,这个优点最大
  • 无需安装,只要打开微信就能用,不占手机内存,体验好
  • 开发周期段,一般最多一个月就可以上线完成
  • 开发所需的资金少,所需资金是开发原生APP的一半不到
  • 小程序名称是唯一的,在微信的搜索里权重很高
  • 容易上手,只要之前有HTML+CSS+JS基础知识,写小程序基本没有大问题
  • 基本不需要考虑兼容性问题,只要微信可以正常运行的机器,就可以运行小程序
  • 发布,审核高效,基本上午发布审核,下午就审核通过,升级简单,支持灰度发布
  • 开发文档完善,社区活跃
  • 支持插件式开发,一些基本功能可以开发成插件,供多个小程序使用

小程序缺点:

  • 局限性很强(比如页面大小不能超过1M,不能打开超过5个层级的页面,样式单一,小程序的部分组件已经是成型的了,样式不能修改,比如幻灯片,导航)只能依赖于微信依托与微信,无法开发后台管理功能
  • 不利于推广,推广面窄,不能分享朋友圈,只能分享给朋友,附近小程序推广,其中附加小程序也收到微信限制
  • 后台调试麻烦,因为API接口必须https请求,且公网地址,也就是说后台代码必须发布到远程服务器上;当然我们可以修改host进行dns映射把远程服务器转到本地,或者开启tomcat远程调试;不管怎么说终归调试比较麻烦
  • 前台测试有诸多坑,最头疼莫过于模拟器与真机显示不一致
  • js引用只能使用绝对路径,不能操作DOM

原生App优点:

  • 原生的相应速度快
  • 对于有无网络操作时,譬如离线操作基本选用原生开发
  • 需要调用系统硬件的功能(摄像头,拨号,短信蓝牙…)
  • 在无网络或者弱网情况下体验好

原生App缺点:

  • 开发周期长,开发成本高,需要下载

22.小程序的发布流程(开发流程)

  1. 注册微信小程序账号
  2. 获取微信小程序的AppID
  3. 下载微信小程序开发者工具
  4. 创建demo项目
  5. 去微信公众号配置域名
  6. 手机浏览
  7. 代码上传
  8. 提交审核
  9. 小程序发布

23. (重要)小程序授权登录 + 支付流程

授权,微信登录获取code,微信登录,获取 iv , encryptedData 传到服务器后台,如果没有注册就需要注册。

//1.小程序注册,要以公司的身份去注册一个小程序,才有微信支付权限

//2.绑定商户号

//3.在小程序填写合法域

//4.调用 wx.login() 获取 appid

//5.调用 wx.requestPayment
wx.requestPayment({
  'timeStamp': '',//时间戳从1970年1月1日00:00:00至今的秒数,即当前的时间
  'nonceStr': '',//随机字符串,长度为32个字符以下。
  'package': '',//统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=*
  'signType': 'MD5',//签名类型,默认为MD5,支持HMAC-SHA256和MD5。注意此处需与统一下单的签名类型一致
  'paySign': '',//签名,具体签名方案参见微信公众号支付帮助文档;
  'success':function(res){},//成功回调
  'fail':function(res){},//失败
  'complete':function(res){}//接口调用结束的回调函数(调用成功、失败都会执行)
})

24.小程序还有那些功能?

客服功能,录音,视频,音频,地图,定位,拍照,动画,canvas

25. 小程序的常见问题:

  • rpx:小程序的尺寸单位,规定屏幕为750rpx,可适配不同分辨率屏幕 本地资源无法通过wxss获取:background-image:可以使用网络图片,或者base64,或者使用标签
  • wx.navigateTo无法打开页面:一个应用同时只能打开5个页面,请避免多层级的交互方式,或使用wx.redirectTo
  • tabBar设置不显示:
    • 1.tabBar的数量少于2项或超过5项都不会显示。
    • 2.tabBar写法错误导致不会显示。
    • 3.tabBar没有写pagePath字段(程序启动后显示的第一个页面)

26.小程序的生命周期

首先他有两个生命周期,一个是小程序的生命周期,一个是页面的生命周期:

26.1 小程序的:

onLaunch:当小程序初始化完成时,会触发 onLaunch(全局只触发一次)。

onShow:当小程序启动,或从后台进入前台显示,会触发 onShow

onHide:当小程序从前台进入后台,会触发 onHide。

onError:当小程序发生脚本错误,或者 api 调用失败时,会触发 onError 并带上错误信息

26.2 页面的:

onLoad: 监听页面加载.

onReady:监听页面初次渲染完成.

onShow: 监听页面显示.

onHide: 监听页面隐藏.

onUnload: 监听页面卸载

27. 微信小程序用哪种响应式方案?

flex +rpx

rpx(responsive pixel): 可以根据屏幕宽度进行自适应。规定屏幕宽为750rpx。如在 iPhone6 上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素。

因为小程序是以微信为平台运行的,可以同时运行在android与ios的设备上,所以不可避免的会遇到布局适配问题,特别是在iphone5上,因为屏幕尺寸小的缘故,也是适配问题最多的机型,下面就简单介绍几种适配方法。

rpx适配 rpx是小程序中的尺寸单位,它有以下特征:

小程序的屏幕宽固定为750rpx(即750个物理像素),在所有设备上都是如此 1rpx=(screenWidth / 750)px,其中screenWidth为手机屏幕的实际的宽度(单位px),例如iphone6的screenWidth=375px,则在iphone6中1rpx=0.5px 由上可知,在不同设备上rpx与px的转换是不相同的,但是宽度的rpx却是固定的,所以可以使用rpx作为单位,来设置布局的宽高。

vw、vh适配 vw和vh是css3中的新单位,是一种视窗单位,在小程序中也同样适用。

小程序中,窗口宽度固定为100vw,将窗口宽度平均分成100份,1份是1vw 小程序中,窗口高度固定为100vh ,将窗口高度平均分成100份,1份是1vh 所以,我们在小程序中也可以使用vw、vh作为尺寸单位使用在布局中进行布局,但是一般情况下,百分比+rpx就已经足够使用了,所以它们的出场机会很少。

28.授权验证登录怎么做,用户退出后下次进入还需要再次授权吗

wx.login 获取到一个 code,拿这 code 去请求后台得到 openId, sessionKey, unionId。

调 wx.getUserInfo

一次性授权:

永久授权:调取授权登录接口并把获取到的用户公开信息存入数据库

29.小程序组件通信

1. 父传子
  1. 在父组件中的子组件标签绑定属性 传递要传输的变量
  2. 在子组件中用properties来接收数据 可以直接使用
  3. 改变组件的properties数据使用setData()
2. 子传父
  1. 在父组件的子组件标签上定义一个事件,绑定要执行的方法
  2. 在子组件中通过 this.triggerEvent来触发自定义事件,传递数据
  3. 在父组件中接收传递的数据,通过事件对象e来接收
3.获取其他组件数据

this.selectComponent(“.类名”):想获取哪个组件的数据

29.如何提高小程序的首屏加载时间

  • 提前请求:异步数据数据请求不需要等待页面渲染完成、
  • 利用缓存:利用 storage API 对异步请求数据进行缓存,二次启动时先利用缓存数据渲染页面,再进行后台更新
  • 避免白屏:先展示页面骨架和基础内容
  • 及时反馈:及时地对需要用户等待的交互操作给出反馈,避免用户以 为小程序没有响应
  • 性能优化:避免不当使用 setdata 和 onpagescroll
包体积优化
  • 分包加载(优先采用,大幅降低主包体积)。
  • 图片优化(1.使用tinypng压缩图片素材; 2.服务器端支持,可采用webp格式)。
  • 组件化开发(易维护)。
  • 减少文件个数及冗余数据。
请求优化
  • 关键数据尽早请求(onLoad()阶段请求,次要数据可以通过事件触发再请求);整合请求数据,降低请求次数。
  • 采用cdn缓存静态的接口数据(如判断用户登录状态,未登录则请求缓存接口数据),cdn稳定且就近访问速度快(针对加载总时长波动大)。
  • 缓存请求的接口数据。
首次渲染优化
  • 图片懒加载(节省带宽)。
  • setData优化(不要一次性设置过多的数据等)。
  • DOM渲染优化(减少DOM节点)

30.微信小程序实现跳转到另外一个小程序的方法

1,首先需要在当前小程序app.json中定义:需要跳转的小程序的app-id

app.josn

 "navigateToMiniProgramAppIdList": [
    "*******" //appid
  ],

第一种方法:wx.navigateToMiniProgram(打开另一个小程序)

wx.navigateToMiniProgram({
  appId: '**********',
  path: 'page/index/index?id=123', //路径和携带的参数
  extraData: {
    foo: 'bar'
  },
  envVersion: 'develop',
  success(res) {
    // 打开成功
  },
  fail(res){
    // 打开失败
  },
  complete(res){
    // 调用结束  不管成功还是失败都执行
  }
  })
  • appId:跳转到的小程序app-id
  • path:打开的页面路径,如果为空则打开首页,path 中 ? 后面的部分会成为 query,在小程序的 App.onLaunch、App.onShow 和 Page.onLoad的回调函数中获取query数据
  • extraData:需要传递给目标小程序的数据,目标小程序可在 App.onLaunch、App.onShow 中获取到这份数据
  • envVersion:要打开的小程序版本,有效值: develop(开发版),trial(体验版),release(正式版),仅在当前小程序为开发版或体验版时此参数有效,如果当前小程序是正式版,则打开的小程序必定是正式版

第二种方法:navigator(跳转)

<navigator
class="btn" 
target="miniProgram" 
open-type="navigate" 
app-id="wx213c5ba6740c814d"
version="trial">
点击跳转另一个小程序
</navigator>
  • target:在哪个目标上发生跳转,默认当前小程序,有效值: self(当前小程序),miniProgram(其它小程序)
  • open-type:跳转方式 “avigate 对应 wx.navigateTo 或 wx.navigateToMiniProgram 的功能”
  • app-id:跳转到的小程序app-id
  • version:要打开的小程序版本,有效值: develop(开发版),trial(体验版),release(正式版),仅在当前小程序为开发版或体验版时此参数有效,如果当前小程序是正式版,则打开的小程序必定是正式版

31.小程序页面之间有哪些(传值)传递数据的方法?

  1. 页面跳转或重定向时,使用url带参数传递数据
  2. 使用组件模板 template传递参数
  3. 使用缓存传递参数
  4. 使用数据库传递参数
  5. 给html元素添加data-*属性来传递值,然后通过e.currentTarget.dataset或onload的param参数获取(data- 名称不能有大写字母,不可以存放对象)
  6. 设置id 的方法标识来传值,通过e.currentTarget.id获取设置的id值,然后通过设置全局对象的方式来传递数据
  7. 在navigator中添加参数数值

32.微信小程序和H5的区别?

  • 运行环境不同(小程序在微信运行,h5在浏览器运行)
  • 开发成本不同(h5需要兼容不同的浏览器)
  • 获取系统权限不同(系统级权限可以和小程序无缝衔接)
  • 应用在生成环境的运行速度流程(h5需不断对项目优化来提高用户体验)

作者:觉晓
链接:https://juejin.cn/post/7211775653167071291
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

33.小程序调用后台接口遇到那些问题?

  • 数据的大小限制,超过范围会直接导致整个小程序崩溃,除非重启小程序

  • 小程序不可以直接渲染文章内容这类型的html文本,显示需要借助插件

    注:插件渲染会导致页面加载变慢,建议在后台对文章内容的html进行过滤,后台直接处理批量替换p标签div标签为view标签,然后其他的标签让插件来做

34.微信支付步骤

1.打开小程序,点击直接下单

2.用wx.login获取到用户临时登录凭证code,发送到后端服务器换取openid

3.在下单时,小程序需要将购买的商品id,数量,以及用户的openid传送到服务器

4.服务器在接收到商品id数量,openid后,生成服务器订单数据,同时经过一定的签名算法,向微信支付发起请求,获取预付单信息,同时将获取的数据再次进行相应规则的签名,向小程序端响应必要的信息

5.小程序端在获取到对应的参数之后,调用wx.requestPayment()发起微信支付,唤醒支付工作台,进行支付

6.接下来一系列操作都是由用户来操作的包括了微信支付密码,指纹等验证,确认支付后指向鉴权调起支付

7.鉴权调起支付:在微信后台进行鉴权,微信后台直接返回给前端支付结果,前端收到返回数据后对支付结果进行展示

8.推送支付结果:微信后台在给前端返回支付的结果后,也会向后台返回一个支付的结果,后台通过这个支付结果来更新订单的状态

35.小程序授权登录

1.当用户进入小程序的时候首先需要判断用户是否授权过小程序

2.如果没授权,需要通过一个按钮来实现授权登录

3.通过调用getuserInfo(),可以获取到个人信息

4.用户可以授权登录也可以取消登录

5.根据登录接口返回的code,判断用户是否为新用户

6.当用户注册成功之后,再调用登录接口,保存token

36.如何自定义tabbar

  1. 取消当前的 tabbar
  2. 插入自定义 tabbar 组件
  3. app.json 调用组件
  4. 页面显示 tabbar 组件

37.小程序怎样使用自定义组件

  1. 新建自定义组件目录,生成目录结构;
  2. 写好组件内容;
  3. 在要使用的目标页面的 json 文件中配置下 usingComponents,引入组件;
  4. 以标签的形式在页面中使用该组件即可;
  5. 传递数据和 vue 一样,通过自定义属性,然后在组件里通过 properties 接收就可以使用了

38.tabBar设置不显示

tabBar设置不显示有如下几个原因:

  1. tabBar 的数量少于2项或超过5项都不会显示;
  2. tabBar 写法错误导致不显示;
  3. tabBar 没有写 pagePath 字段(程序启动后显示的第一个页面)

39.登录流程?

登录流程是调wx.login获取code传给后台服务器获取微信用户唯一标识openid及本次登录的会话密钥(session_key)等)。拿到开发者服务器传回来的会话密钥(session_key)之后,前端要保存wx.setStorageSync(‘sessionKey’, ‘value’)

持久登录状态:session信息存放在cookie中以请求头的方式带回给服务端,放到request.js里的wx.request的header里

40.小程序简单介绍下三种事件对象的属性列表?

1. 基础事件(BaseEvent)

  1. type:事件类型

  2. timeStamp:事件生成时的时间戳

  3. target:触发事件的组件的属性值集合

  4. currentTarget:当前组件的一些属性集合

2. 自定义事件(CustomEvent)

  1. detail

3. 触摸事件(TouchEvent)

  1. touches

  2. changedTouches

41.app.json 是对当前小程序的全局配置,讲述三个配置各个项的含义?

  1. pages字段 —— 用于描述当前小程序所有页面路径,这是为了让微信客户端知道当前你的小程序页面定义在哪个目录。
  2. window字段 —— 小程序所有页面的顶部背景颜色,文字颜色定义在这里的
  3. tab字段—小程序全局顶部或底部tab

42.简述下 wx.navigateTo(), wx.redirectTo(), wx.switchTab(), wx.navigateBack(), wx.reLaunch()的区别

  1. wx.navigateTo():保留当前页面,跳转到应用内的某个页面。但是不能跳到 tabbar` 页面
  2. wx.redirectTo():关闭当前页面,跳转到应用内的某个页面。但是不允许跳转到 tabbar` 页面
  3. wx.switchTab():跳转到 abBar页面,并关闭其他所有非tabBar` 页面
  4. wx.navigateBack():关闭当前页面,返回上一页面或多级页面。可通过getCurrentPages()` 获取当前的页面栈,决定需要返回几层
  5. **wx.reLaunch():**关闭所有页面,打开到应用内的某个页面

uniapp

1.uniapp进行条件编译的两种方法?小程序端和H5的代表值是什么?

通过 #ifdef、#ifndef 的方式 H5 : H5 MP-WEIXIN : 微信小程序

2.uniapp的配置文件、入口文件、主组件、页面管理部分

pages.json 配置文件 main.js 入口文件 App.vue 主组件 pages 页面管理部分

3.uniapp上传文件时用到api是什么 格式是什么?

uni.uploadFile({
    url: '要上传的地址',
    fileType:'image',
            filePath:'图片路径',
            name:'文件对应的key',
            success: function(res){
            console.log(res)
    },
})

4.uniapp获取地理位置的API是什么?

uni.getLocation

5.rpx、px、em、rem、%、vh、vw的区别是什么?

  • rpx 相当于把屏幕宽度分为750份,1份就是1rpx
  • px 绝对单位,页面按精确像素展示
  • em 相对单位,相对于它的父节点字体进行计算
  • rem 相对单位,相对根节点html的字体大小来计算
  • % 一般来说就是相对于父元素
  • vh 视窗高度,1vh等于视窗高度的1%
  • vw 视窗宽度,1vw等于视窗宽度的1%

6.uniapp如何监听页面滚动?

使用 onPageScroll 监听

7.如何让图片宽度不变,高度自动变化,保持原图宽高比不变?

给image标签添加 mode='widthFix'

8.uni-app的优缺点

优点:

  1. 一套代码可以生成多端
  2. 学习成本低,语法是vue的,组件是小程序的
  3. 拓展能力强
  4. 使用HBuilderX开发,支持vue语法
  5. 突破了系统对H5条用原生能力的限制

缺点:

  1. 问世时间短,很多地方不完善
  2. 社区不大
  3. 官方对问题的反馈不及时
  4. 在Android平台上比微信小程序和iOS差
  5. 文件命名受限

9.分别写出jQuery、vue、小程序、uni-app中的本地存储数据和接受数据是什么?

jQuery

存:$.cookie('key','value')
取:$.cookie('key')

vue

存储:localstorage.setItem(‘key’,‘value’)
接收:localstorage.getItem(‘key’)

微信小程序

存储:通过wx.setStorage/wx.setStorageSync写数据到缓存
接收:通过wx.getStorage/wx.getStorageSync读取本地缓存,

uni-app

存储:uni.setStorage({key:“属性名”,data:“值”})
接收:uni.getStorage({key:“属性名”,success(e){e.data//这就是你想要取的token}})

10.jq、vue、uni-app、小程序的页面传参方式

jq传参

通过url拼接参数进行传参。

vue传参

vue可以通过标签router-link跳转传参,通过path+路径,query+参数
也可以通过事件里的this.$router.push({})跳转传参

小程序传参

通过跳转路径后面拼接参数来进行跳转传参

11.vue , 微信小程序 , uni-app绑定变量属性

vue和uni-app动态绑定一个变量的值为元素的某个属性的时候,会在属性前面加上冒号":";
小程序绑定某个变量的值为元素属性时,会用两个大括号{{}}括起来,如果不加括号,为被认为是字符串。

12.vue,小程序,uni-app的生命周期

vue

1. beforeCreate(创建前)
2. created(创建后)
3. beforeMount(载入前),(挂载)
4. mounted(载入后)
5. beforeUpdate(更新前)
6. updated(更新后)
7. beforeDestroy(销毁前)
8. destroyed(销毁后)

小程序/uni-app

1. onLoad:首次进入页面加载时触发,可以在 onLoad 的参数中获取打开当前页面路径中的参数。
2. onShow:加载完成后、后台切到前台或重新进入页面时触发
3. onReady:页面首次渲染完成时触发
4. onHide:从前台切到后台或进入其他页面触发
5. onUnload:页面卸载时触发
6. onPullDownRefresh:监听用户下拉动作
7. onReachBottom:页面上拉触底事件的处理函数
8. onShareAppMessage:用户点击右上角转发

webpack&git

1.对webpack的理解?

webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。我们可以使用webpack管理模块。因为在webpack看来,项目中的所有资源皆为模块,通过分析模块间的依赖关系,在其内部构建出一个依赖图,最终编绎输出模块为 HTML、JavaScript、CSS 以及各种静态文件(图片、字体等),让我们的开发过程更加高效。

image.png webpack的主要作用如下:

  • 模块打包。可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。利用打包我们就可以在开发的时候根据我们自己的业务自由划分文件模块,保证项目结构的清晰和可读性。
  • 编译兼容。在前端的“上古时期”,手写一堆浏览器兼容代码一直是令前端工程师头皮发麻的事情,而在今天这个问题被大大的弱化了,通过webpackLoader机制,不仅仅可以帮助我们对代码做polyfill,还可以编译转换诸如.less.vue.jsx这类在浏览器无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提高开发效率。
  • 能力扩展。通过webpackPlugin机制,我们在实现模块化打包编译兼容的基础上,可以进一步实现诸如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。

2.webpack的构建流程?

webpack的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  • 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  • 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  • 确定入口:根据配置中的 entry 找出所有的入口文件
  • 编译模块:从入口文件出发,调用所有配置的 loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  • 完成模块编译:在经过上一步使用 loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

在以上过程中,webpack会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用webpack提供的 API 改变webpack的运行结果。

简单说:

  • 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler
  • 编译:从 entry 出发,针对每个 Module 串行调用对应的 loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译处理
  • 输出:将编译后的 Module 组合成 Chunk,将 Chunk 转换成文件,输出到文件系统中

3.常见的loader有哪些?

默认情况下,webpack只支持对jsjson文件进行打包,但是像csshtmlpng等其他类型的文件,webpack则无能为力。因此,就需要配置相应的loader进行文件内容的解析转换。

常用的loader如下:

  • image-loader:加载并且压缩图片文件。
  • less-loader: 加载并编译 LESS 文件。
  • sass-loader:加载并编译 SASS/SCSS 文件。
  • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性,使用css-loader必须要配合使用style-loader
  • style-loader:用于将 CSS 编译完成的样式,挂载到页面的 style 标签上。需要注意 loader 执行顺序,style-loader 要放在第一位,loader 都是从后往前执行。
  • babel-loader:把 ES6 转换成 ES5
  • postcss-loader:扩展 CSS 语法,使用下一代 CSS,可以配合 autoprefixer 插件自动补齐 CSS3 前缀。
  • eslint-loader:通过 ESLint 检查 JavaScript 代码。
  • vue-loader:加载并编译 Vue 组件。
  • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 (处理图片和字体)
  • url-loader:与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值会交给 file-loader 处理,小于阈值时返回文件 base64 形式编码 (处理图片和字体)

4.常见的plugin有哪些?

webpack中的plugin赋予其各种灵活的功能,例如打包优化、资源管理、环境变量注入等,它们会运行在webpack的不同阶段(钩子 / 生命周期),贯穿了webpack整个编译周期。目的在于解决 loader 无法实现的其他事

常用的plugin如下:

  • HtmlWebpackPlugin:简化 HTML 文件创建 (依赖于 html-loader)
  • mini-css-extract-plugin: 分离样式文件,CSS 提取为独立文件,支持按需加载 (替代extract-text-webpack-plugin)
  • clean-webpack-plugin: 目录清理

5.loader和plugin的区别?

loader是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中;plugin赋予了webpack各种灵活的功能,例如打包优化、资源管理、环境变量注入等,目的是解决 loader无法实现的其他事。

在运行时机上,loader 运行在打包文件之前;plugin则是在整个编译周期都起作用。

在配置上,loadermodule.rules中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)loaderoptions (参数)等属性;pluginplugins中单独配置,类型为数组,每一项是一个 plugin 的实例,参数都通过构造函数传入。

6.webpack的热更新原理是?

模块热替换(HMR - hot module replacement),又叫做热更新,在不需要刷新整个页面的同时更新模块,能够提升开发的效率和体验。热更新时只会局部刷新页面上发生了变化的模块,同时可以保留当前页面的状态,比如复选框的选中状态等。

热更新的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上webpack-dev-server与浏览器之间维护了一个websocket,当本地资源发生变化时,webpack-dev-server会向浏览器推送更新,并带上构建时的hash,让客户端与上一次资源进行对比。客户端对比出差异后会向webpack-dev-server发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向webpack-dev-server发起 jsonp 请求获取该chunk的增量更新。

后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loadervue-loader都是借助这些 API 实现热更新。

7.如何提高webpack的构建速度?

  1. 代码压缩

    • JS压缩
      webpack 4.0默认在生产环境的时候是支持代码压缩的,即mode=production模式下。 实际上webpack 4.0默认是使用terser-webpack-plugin这个压缩插件,在此之前是使用 uglifyjs-webpack-plugin,两者的区别是后者对 ES6 的压缩不是很好,同时我们可以开启 parallel参数,使用多进程压缩,加快压缩。

    • CSS压缩
      CSS 压缩通常是去除无用的空格等,因为很难去修改选择器、属性的名称、值等。可以使用另外一个插件:css-minimizer-webpack-plugin

    • HTML压缩

      使用HtmlWebpackPlugin插件来生成 HTML 的模板时候,通过配置属性minify进行 html 优化。

      module.exports = {
          plugin:[
              new HtmlwebpackPlugin({
                  minify:{
                      minifyCSS: false, // 是否压缩css
                      collapseWhitespace: false, // 是否折叠空格
                      removeComments: true // 是否移除注释
                  }
              })
          ]
      }
      
  2. 图片压缩
    配置image-webpack-loader

  3. Tree Shaking
    Tree Shaking是一个术语,在计算机中表示消除死代码,依赖于ES Module的静态语法分析(不执行任何的代码,可以明确知道模块的依赖关系)。 在webpack实现Tree shaking有两种方案:

    • usedExports:通过标记某些函数是否被使用,之后通过 Terser 来进行优化的

      module.exports = {
          ...
          optimization:{
              usedExports
          }
      }
      

      使用之后,没被用上的代码在webpack打包中会加入unused harmony export mul注释,用来告知Terser在优化时,可以删除掉这段代码。

    • sideEffects:跳过整个模块/文件,直接查看该文件是否有副作用sideEffects:跳过整个模块/文件,直接查看该文件是否有副作用
      sideEffects用于告知webpack compiler哪些模块时有副作用,配置方法是在package.json中设置sideEffects属性。如果sideEffects设置为false,就是告知webpack可以安全的删除未用到的exports。如果有些文件需要保留,可以设置为数组的形式,如:

       "sideEffecis":[    
           "./src/util/format.js",    
           "*.css" *// 所有的css文件* 
       ] 
      

4.缩小打包域
排除webpack不需要解析的模块,即在使用loader的时候,在尽量少的模块中去使用。可以借助 includeexclude这两个参数,规定loader只在那些模块应用和在哪些模块不应用。

5.减少ES6转为ES5的冗余代码
使用bable-plugin-transform-runtime插件

6.提取公共代码
通过配置CommonsChunkPlugin插件,将多个页面的公共代码抽离成单独的文件

8.git是什么?git的五个命令,git和svn的区别

git是什么

1.  git是目前世界上最先进的分布式管理系统。

git的常用命令

1. git init 把这个目录变成git可以管理的仓库
2. git add README.md 文件添加到仓库
3. git add 不但可以跟单一文件,也可以跟通配符,更可以跟目录。一个点就把当前目录下所有未追踪的文件全部add4. git commit -m ‘first commit’把文件提交到仓库
5. git remote add origin git@github.com:wangjiax9/practice.git //关联远程仓库 
6. git push -u origin master //把本地库的所有内容推送到远程库上

Git和SVN的区别

1. Git是分布式版本控制工具 , SVN是集中式版本控制工具
2. Git没有一个全局的版本号,而SVN有。
3. Git和SVN的分支不同
4. git吧内容按元数据方式存储,而SVN是按文件
5. Git内容的完整性要优于SVN
6. Git无需联网就可使用(无需下载服务端),而SVN必须要联网(须下载服务端)因为git的版本区就在自己电脑上,而svn在远程服务器上。

9. Git项目如何配置,如何上传至GitHub。描述其详细步骤

1. 注册登录github
2. 创建github仓库
3. 安装git客户端
4. 绑定用户信息
5. 设置ssh key
6. 创建本地项目以及仓库
7. 关联github仓库
8. 推送项目到github仓库

10.常用命令

  • 初始化一个仓库:git init
  • 查看分支:git branch
  • 将已修改或未跟踪的文件添加到暂存区:git add [file] 或 git add .
  • 提交至本地仓库:git commit -m “提及记录xxxx”
  • 本地分支推送至远程分支:git push
  • 查看当前工作目录和暂存区的状态: git status
  • 查看提交的日志记录: git log
  • 从远程分支拉取代码:git pull
  • 合并某分支(xxx)到当前分支: git merge xxx
  • 切换到分支xxx:git checkout xxx
  • 创建分支xxx并切换到该分支:git checkout -b xxx
  • 删除分支xxx:git branch -d xxx
  • 将当前分支到改动保存到堆栈中:git stash
  • 恢复堆栈中缓存的改动内容:git stash pop

11.git merge 和git rebase的区别?

相同点:

git mergegit rebase两个命令都⽤于从⼀个分⽀获取内容并合并到当前分⽀。

不同点:

  1. git merge会⾃动创建⼀个新的commit,如果合并时遇到冲突的话,只需要修改后重新commit
  • 优点:能记录真实的commit情况,包括每个分⽀的详情
  • 缺点:由于每次merge会⾃动产⽣⼀个commit,因此在使用⼀些可视化的git工具时会看到这些自动产生的commit,这些commit对于程序员来说没有什么特别的意义,多了反而会影响阅读。
  1. git rebase会合并之前的commit历史。
  • 优点:可以得到更简洁的提交历史,去掉了merge 产生的commit
  • 缺点:因为合并而产生的代码问题,就不容易定位,因为会重写提交历史信息

场景:

  • 当需要保留详细的合并信息,建议使⽤git merge, 尤其是要合并到master
  • 当发现⾃⼰修改某个功能时提交比较频繁,并觉得过多的合并记录信息对自己来说没有必要,那么可尝试使用git rebase

12.对GitFlow的理解?

GitFlow重点解决的是由于源代码在开发过程中的各种冲突导致开发活动混乱的问题。重点是对各个分支的理解。

  • master:主分支。
  • develop:主开发分支,平行于master分支。
  • feature:功能分支,必须从develop分支建立,开发完成后合并到develop分支。
  • release:发布分支,发布的时候用,一般测试时候发现的 bug 在该分支进行修复。从develop分支建立,完成后合并回developmaster分支。
  • hotfix:紧急修复线上bug使用,必须从master分支建立,完成后合并回developmaster分支。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值