面试整理

337 阅读22分钟

1 CSS 相关问题

1.1 盒模型

盒模型分为IE盒模型和W3C标准盒模型。

  1. 标准盒模型 box-sizing: content-box 属性 width,height 只包含内容 content,不包含border和padding
  2. IE和模型 box-sizing: border-box 属性 width, height 包含 borderpadding ,指的是content+padding+border

为了在项目中方便开发我们都会使用基础样式。 统一使用 border-box 方便处理宽高。

1.2 垂直水平居中

1.2.1 绝对布局 margin:auto

有固定宽高 自适应 偏移。 div使用绝对布局,设置margin:auto;并设置top、left、right、bottom的值相等即可,不一定要都是0。

1.2.2 绝对布局 margin 偏移

有固定宽高 自适应 偏移。top 和 left 50% margin-top 和 margin-left 负宽高的一半

1.2.3 绝对布局 transform

position transform 元素未知宽度 transform:translate(-50%,-50%)

1.2.4 flex 弹性盒模型

display: flex; 
justify-content: center; /*使子项目水平居中*/
align-items: center; /*使子项目垂直居中*/

1.3 CSS三栏布局

所谓三栏布局就是指页面分为左中右三部分然后对中间一部分做自适应的一种布局方式。不管是三栏布局还是两栏布局思想是一样的。

1.3.1 绝对定位

这种方式的核心是使用定位将左右两边的元素进行绝对定位(absolute),然后中间元素宽度100% 设置左右 margin 值

该方法有个明显的缺点,就是如果中间栏含有最小宽度限制,或是含有宽度的内部元素,当浏览器宽度小到一定程度,会发生层重叠的情况。

1.3.2 浮动

原理基本个绝对定位差不多,都是将两边元素脱离正常文档流。给左右元素分别添加左右浮动,设置左右 margin 值

1.3.3 圣杯布局

html 代码如下注意元素的顺序:

<div class="main">Main</div> 
<div class="left">Left</div> 
<div class="right">Right</div>

CSS 代码如下:

body,
    html {
      height: 200px;
      width: 500px;
      padding: 0;
      margin: 0
    }

    body {
      padding-left: 100px;
      padding-right: 200px;
    }

    .left {
      background: red;
      width: 100px;
      float: left;
      margin-left: -100%;
      position: relative;
      left: -100px;
      
      height: 100%;
    }

    .main {
      background: blue;
      width: 100%;
      height: 100%;
      float: left;
    }

    .right {
      background: red;
      width: 200px;
      height: 100%;
      float: left;
      margin-left: -200px;
      position: relative;
      right: -200px;
    }

解释:

  • HTML元素的顺序将中间元素放在前面。
  • 中间部分需要根据浏览器宽度的变化而变化,所以要用100%,这里设左中右向左浮动,因为中间100%,左层和右层根本没有位置上去,固需要给中间元素设浮动。
  • 把左层margin-left负100%,发现left上去了,因为负到出窗口没位置了,只能往上挪。
  • 把右层margin-left负本身的宽度将右层移动到上边。
  • 使用定位将左右栏固定到左右留白位置。

1.3.4 双飞翼布局

html 代码如下注意元素的顺序:

<div class="main">
    <div class="inner">
      Main
    </div>
</div>
<div class="left">Left</div>
<div class="right">Right</div>

CSS 代码如下:

body,
    html {
      height: 200px;
      width: 500px;
      padding: 0;
      margin: 0
    }


    .left {
      background: red;
      width: 100px;
      float: left;
      margin-left: -100%;
      height: 100%;
    }

    .main {
      background: blue;
      width: 100%;
      height: 100%;
      float: left;
    }

    .inner {
      margin-left: 100px;
      margin-right: 200px;
    }

    .right {
      background: red;
      width: 200px;
      height: 100%;
      float: left;
      margin-left: -200px;
    }

通过增加多一个div就可以不用相对布局了,只用到了浮动和负边距。

最后我们来总结一下,双飞翼布局其实和圣杯布局的精髓是一样的,都是通过设置负margin来实现元素的排布,

  • 不同的就是html结构,双飞翼是在main元素内部又设置了一个inner并设置它的左右margin,而非圣杯布局的padding,来排除两边元素的覆盖。
  • 双飞翼布局可以多了一个html结构,但是可以不用设置left,right元素的定位。

1.4、清除浮动的方法

为什么要清除浮动?原因是,浮动的元素会脱离文档流,,一个元素一旦浮动,就会脱离文档流,那么他的父元素也管不了他了,布局也会往前推进,出现了父元素高度坍塌的现象。

1.4.1 给父级设置对应的高度

缺点:如果高度一直变化将无法显示全部内容

1.4.2 给父级设置overflow:hidden

缺点:当文本过长,且包含过长英文时,会出现英文文本被隐藏的情况

1.4.3 末尾增加空元素进行clear

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

.bottomDiv { 
   clear: both; 
}

缺点:很明显,增加了一个div标签

1.4.4 给父级添加伪元素进行clear

.box::after { 
    content: '.';
    height: 0; 
    display: block; 
    clear: both;
}

相比较算是最优解了。

1.5 BFC

BFC 全称:Block Formatting Context, 名为 "块级格式化上下文"。

W3C官方解释为:BFC它决定了元素如何对其内容进行定位,以及与其它元素的关系和相互作用,当涉及到可视化布局时,Block Formatting Context提供了一个环境,HTML在这个环境中按照一定的规则进行布局。

简单来说就是,BFC是一个完全独立的空间(布局环境),让空间里的子元素不会影响到外面的布局。那么怎么使用BFC呢,BFC可以看做是一个CSS元素属性

1.5.1 触发条件

  • 根元素,即HTML标签
  • 浮动元素:float值为left、right
  • overflow值不为 visible,为 auto、scroll、hidden
  • display值为 inline-block、table-cell、table-caption、table、inline-table、flex、inline-flex、grid、inline-grid
  • 定位元素:position值为 absolute、fixed

1.5.2 BFC的规则

  1. 内部的盒会在垂直方向一个接一个排列(可以看作BFC中有一个的常规流);
  2. 处于同一个BFC中的元素相互影响,可能会发生margin 合并;完整的说法是:属于同一个BFC的两个相邻Box的margin会发生折叠,不同BFC不会发生折叠。)
  3. 每个元素的margin box的左边,与容器块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此;(这说明BFC中子元素不会超出他的包含块,而position为absolute的元素可以超出他的包含块边界)
  4. BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素,反之亦然;
  5. 计算BFC的高度时,考虑BFC所包含的所有元素,连浮动元素也参与计算;
  6. 浮动盒区域不叠加到BFC上;

1.5.3 BFC解决了什么问题

解决margin重叠问题

描述:同一BFC下,垂直方向的 marin会发生合并。

解决:由BFC规则2可知 两个不同BFC环境的盒子,他们两的margin才不会重叠,那么我们只需触发最后一个子元素的BFC就行。

浮动元素与BFC盒子不重叠(浮动元素高度塌陷问题)

根据第5点可知:BFC盒子会把内部的float盒子算进高度中,这也是为什么前面可以通过给父级盒子设置float: left position: absolute overflow: hidden来解决浮动的高度塌陷问题,因为这些做法都使父级盒子变成一个BFC盒子,而BFC盒子会把内部的float盒子算进高度,顺势解决了高度塌陷问题

浮动元素与BFC盒子不重叠

描述:两个字元素一个有浮动一个没有 会发生覆盖。这时候给没有浮动的加 overflow:hidden 触发BFC。

1.6 如何用css实现一个三角形

纯CSS实现三角形有以下几种方案:

1.6.1 border

通过 border 属性。给定一个宽度和高度都为 0 的元素,其 border 的任何值都会直接相交,我们可以利用这个交点来创建三角形。也就是说,border属性是三角形组成的,下面给每一边都提供不同的边框颜色:

.triangle { 
    width: 0; 
    height: 0; 
    border: 100px solid; border-color: orangered skyblue gold yellowgreen; 
}

效果: image.png

可以看到,我们已经基本上实现了4个三角形形状。所以可以根据border这个特性来绘制三角形。 如果想要一个指向下面的三角形,可以让 border 的上边可见,其他边都设置为透明:

.triangle { 
    width: 0; height: 0; 
    border-top: 50px solid skyblue; 
    border-right: 50px solid transparent; 
    border-left: 50px solid transparent; 
}

1.6.2 clip-path剪裁

最后一种方法——clip-path,它是最精简和最可具扩展性的。 不过目前其在浏览器兼容性不是很好,使用时要考虑浏览器是否支持。

lip-path属性使用裁剪方式创建元素的可显示区域。区域内的部分显示,区域外的隐藏。

clip-path的语法有四种:

  • inset(定义矩形)
  • circle(定义圆)
  • ellipse(定义椭圆)
  • polygon(定义多边形)

polygon的值为多个坐标点组成,坐标第一个值是x方向,第二个值是y方向。左上角是原点,右下角是(100%,100%)的点

.triangle { 
    width: 100px; 
    height: 100px; 
    background: cyan; clip-path: polygon(0 100%, 50% 0, 100% 100%); // 对应三角形三个顶点 
}

1.7 CSS Modules

css modules指的是所有的类名和动画名称默认都有各自作用域的CSS文件,是在构建步骤中对CSS类名和选择器限定作用域的一种方式(类似于命名空间)。通过CSS Modules可以保证单个组件的所有样式集中在同一个地方、只应用于该组件。

1.7.1 解决的问题(优点)

  • 提高代码重用率
  • 提高开发效率、减少沟通成本
  • 提高页面容错
  • 降低耦合
  • 降低发布风险
  • 减少Bug定位时间和Fix成本
  • 更好的实现快速迭代
  • 便于代码维护

1.7.2 实现

在 React 中默认开启了 CSS Module,样式表文件需要以 xxx.module.sass/less/css 命名

我们也可以通过配置 webpack 来开启 CSS Module

module.exports = {
  module: {
    rules: [
      {
        test: /.css$/i,
        loader: "css-loader",
        options: {
          modules: true,
          localIdentName: "[name]_[local]__[hash:base64:5]"
        }
      }
    ]
  }
};

localIdentName 可以定义生产的哈希类名,默认是 [hash:base64]

localIdentName选项的占位符有:

 [name] 源文件名称 (样式文件的文件名)
 [folder] 文件夹相对于 compiler.context 或者 modules.localIdentContext 配置项的相对路径。
 [path] 源文件相对于 compiler.context 或者 modules.localIdentContext 配置项的相对路径。
 [file] - 文件名和路径。
 [ext] - 文件拓展名。
 [hash] - 字符串的哈希值。基于 localIdentHashSalt、localIdentHashFunction、localIdentHashDigest、localIdentHashDigestLength、localIdentContext、resourcePath 和 exportName 生成。
 [<hashFunction>:hash:<hashDigest>:<hashDigestLength>] - 带有哈希设置的哈希。
 [local] - 原始类名。

2 手写题

2.1 防抖与节流

2.1.1 函数防抖(debounce)

所谓防抖,就是指触发事件后 n 秒后才执行函数,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

 function debounce(func, wait) {
      let timeout;
      return function () {
        const context = this;
        const args = [...arguments];
        if (timeout) clearTimeout(timeout);
        timeout = setTimeout(() => {
          func.apply(context, args)
        }, wait);
      }
    }
    
    // 业务函数
    function ajax(content) {
      console.log('ajax request ' + content)
    }

    let inputb = document.getElementById('debounce')

    let debounceAjax = debounce(ajax, 500)

    inputb.addEventListener('keyup', function (e) {
      debounceAjax(e.target.value)
    })

2.1.2 函数节流(throttle)

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

对于节流,一般有两种方式可以实现,分别是时间戳版和定时器版。

时间戳版:

function throttle(func, wait) {
      var previous = 0;
      return function () {
        let now = Date.now();
        let context = this;
        let args = arguments;
        if (now - previous > wait) {
          func.apply(context, args);
          previous = now;
        }
      }
    }
// 调用方式
content.onmousemove = throttle(fun,1000);

当当前时间减去上次时间大于间隔时间将会执行

定时器版:


function throttle(func, wait) {
      let timeout;
      return function () {
        let context = this;
        let args = arguments;
        if (!timeout) {
          timeout = setTimeout(() => {
            timeout = null;
            func.apply(context, args)
          }, wait)
        }

      }
    }

定时器存在将不会执行直到定时器执行结束。

2.1.3 总结

  • 函数防抖和函数节流都是防止某一时间频繁触发,但是这两兄弟之间的原理却不一样。
  • 函数防抖是某一段时间内只执行一次,而函数节流是间隔时间执行。

结合应用场景:

  • debounce

    • search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
    • window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
  • throttle

    • 鼠标不断点击触发,mousedown(单位时间内只触发一次)

    • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断

2.2 数组去重、数组乱序

2.2.1数组去重

去重是开发中经常会碰到的一个热点问题,不过目前项目中碰到的情况都是后台接口使用SQL去重,简单高效,基本不会让前端处理去重。

2.2.1.1 双循环去重

双重for(或while)循环是比较笨拙的方法,它实现的原理很简单:先定义一个包含原始数组第一个元素的数组,然后遍历原始数组,将原始数组中的每个元素与新数组中的每个元素进行比对,如果不重复则添加到新数组中,最后返回新数组;因为它的时间复杂度是O(n^2),如果数组长度很大,那么将会非常耗费内存

function unique(arr) {
      if (!Array.isArray(arr)) {
        console.log('type error!')
        return
      }
      let res = [arr[0]]
      for (let i = 1; i < arr.length; i++) {
        let flag = true
        for (let j = 0; j < res.length; j++) {
          if (arr[i] === res[j]) {
            flag = false;
            break
          }
        }
        if (flag) {
          res.push(arr[i])
        }
      }
      return res
    }

2.2.1.2 indexOf方法

数组的indexOf()方法可返回某个指定的元素在数组中首次出现的位置。该方法首先定义一个空数组res,然后调用indexOf方法对原来的数组进行遍历判断,如果元素不在res中,则将其push进res中,最后将res返回即可获得去重的数组

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    let res = []
    for (let i = 0; i < arr.length; i++) {
        if (res.indexOf(arr[i]) === -1) {
            res.push(arr[i])
        }
    }
    return res
}

利用indexOf检测元素在数组中第一次出现的位置是否和元素现在的位置相等,如果不等则说明该元素是重复元素

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    return Array.prototype.filter.call(arr, function(item, index){
        return arr.indexOf(item) === index;
    });
}


2.2.1.3 利用对象属性去重

创建空对象,遍历数组,将数组中的值设为对象的属性,并给该属性赋初始值1,并添加到去重数组中,每出现一次,对应的属性值增加1,这样,属性值对应的就是该元素出现的次数了

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    let res = [],
        obj = {}
    for (let i = 0; i < arr.length; i++) {
        if (!obj[arr[i]]) {
            res.push(arr[i])
            obj[arr[i]] = 1
        } else {
            obj[arr[i]]++
        }
    }
    return res
}


2.2.1.4 set与解构赋值去重

ES6中新增了数据类型set,set的一个最大的特点就是数据不重复。

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    return [...new Set(arr)]  // 也可以通过Array.from 转换为真实的数组
}

2.2.2 对象数组去重

就是这种的格式的数据

/ 原数据是这样的                   // 去重后数据是这样的
[{                                [{
  "goodsId": "1",                    "goodsId": "1",
  "quota": 12,                       "quota": 12,
  "skuId": "1"                       "skuId": "1"
},                                 },
{                                  {
  "goodsId": "2",                    "goodsId": "2",
  "quota": 12,                       "quota": 12,
  "skuId": "2"                       "skuId": "2"
},                                 }]
{
  "goodsId": "1",
  "quota": 12,
  "skuId": "1"
}]

对于这种的就不能使用 ES6的 Array.from或者new Set

2.2.2.1 使用reduce

利用reduce方法遍历数组,reduce第一个参数是遍历需要执行的函数,第二个参数是item的初始值。判断对象hash中是否存在,不存才将添加到你对象和上次返回的数组中。

function uniqueFunc2(arr, uniId){
  let hash = {}
  return arr.reduce((accum,item) => {
    hash[item[uniId]] ? '' : hash[item[uniId]] = true && accum.push(item)
    return accum
  },[])
}


2.2.2.2 利用对象访问属性的方法

利用对象访问属性的方法,判断对象中是否存在key

function uniqueFunc3(arr, uniId){
  let obj = {}
  let tempArr = []
  for(var i = 0; i<arr.length; i++){
    if(!obj[arr[i][uniId]]){
      tempArr.push(arr[i])
      obj[arr[i][uniId]] = true
    }
  }
  return tempArr
}

2.2.2.3 使用filter和Map

通过Map储存不重复的值的特性,结合filter过滤得到去重后的数组。

function uniqueFunc(arr, uniId){
  const res = new Map();
  return arr.filter((item) => !res.has(item[uniId]) && res.set(item[uniId], 1));
}

2.2.3 数组乱序

2.2.3.1 sort与Math.random

利用数组的sort方法,判断随机出来的0~1的值与0.5的大小,实现伪排序。就是不够乱

function randArr (arr) {
    return arr.sort(() => {
        return (Math.random() - 0.5);
    });
}

2.2.3.1 洗牌算法
function shuffle(arr) {
    let length = arr.length,
        r      = length,
        rand   = 0;

    while (r) {
        rand = Math.floor(Math.random() * r--);
        if(r != rand){
          [arr[r], arr[rand]] = [arr[rand], arr[r]];
        }
    }
    return arr;
}

算法的大致描述:

  1. 找到数组的最后一个元素
  2. 在头和尾中间随机一个位置(不包含尾)
  3. 交换元素
  4. 尾部和随机位换位并且尾部迁移一位。
  5. 重复1~4的步骤 直到找到头部结束。

2.3 call、apply、bind实现

2.3.1 call实现

//传递参数从一个数组变成逐个传参了,不用...扩展运算符的也可以用arguments代替
Function.prototype.myCall = function (context, ...args) {
    //这里默认不传就是给window,也可以用es6给参数设置默认参数
    context = context || window
    args = args ? args : []
    //给context新增一个独一无二的属性以免覆盖原有属性
    const key = Symbol()
    context[key] = this
    //通过隐式绑定的方式调用函数
    const result = context[key](...args)
    //删除添加的属性
    delete context[key]
    //返回函数调用的返回值
    return result
}

2.3.1 apply实现

Function.prototype.myApply = function (context, args) {
    //这里默认不传就是给window,也可以用es6给参数设置默认参数
    context = context || window
    args = args ? args : []
    //给context新增一个独一无二的属性以免覆盖原有属性
    const key = Symbol()
    context[key] = this
    //通过隐式绑定的方式调用函数
    const result = context[key](...args)
    //删除添加的属性
    delete context[key]
    //返回函数调用的返回值
    return result
}

2.3.1 bind实现

Function.prototype.myBind = function (context, ...args) {
    const fn = this
    args = args ? args : []
    return function newFn(...newFnArgs) {
        if (this instanceof newFn) {
            return new fn(...args, ...newFnArgs)
        }
        return fn.apply(context, [...args,...newFnArgs])
    }
}

2.3.4 call、apply 和 bind 的区别

  • 三者都是用来改变函数的this指向
  • 三者的第一个参数都是this指向的对象
  • bind是返回一个绑定函数可稍后执行,call、apply是立即调用
  • 三者都可以给定参数传递
  • call给定参数需要将参数全部列出,apply给定参数数组

2.4 sleep函数

sleep函数顾名思义就是需要实现一个能等待执行的函数;

首先想到的是肯定需要使用setTimeout,然后想到Generator函数是可以用yield暂停代码的,所以这应该也能实现;那么作为Generator函数的语法糖async/await应该也能实现; async/await可以实现,那么Promise应该也没问题,所以有四种方式。

2.4.1 回调函数方式

function sleep(callback, time) {
    if (typeof callback === 'function') {
    setTimeout(callback, time)
  }
}

sleep(function(){console.log("1")}, 1000)

而且当异步的操作越来越多时,回调的嵌套会越来越深,代码的执行顺序会非常不直观,也不利于维护。

2.4.2 Promise方式

Promise 是 ES6 提供的一种异步编程解决方案,它将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。

const sleep = time => {
    return new Promise(resolve => setTimeout(resolve, time));
}

sleep(1000).then(() => console.log("1"))

2.4.3 async await 方式

使用async await就可以避免Promise的一连串.then.then.then。 正常同步代码就行。

function sleep(duration) {
  return new Promise(resolve => {
      setTimeout(resolve, duration);
  })
}
async function changeColor(color, duration) {
	console.log('traffic-light ', color);
	await sleep(duration);
}

2.5 控制最大并发数

JavaScript 实现一个带并发限制的异步调度器 Scheduler,保证同时运行的任务最多有两个。完善代码中 Scheduler类,使得以下程序能正确输出。

实现步骤:

  1. 因为add函数后面有个 .then ,所以在add函数里面我们需要return Promise
  2. 我们需要创建两个list,tasks 和 usingTask,usingTask是正在执行的任务队列,最多两个。taks是等待执行的任务队列
  3. 我们在add函数内return Promise之后,我们需要给当前函数 promiseCreator添加 resolve,用于在执行完之后,就是在then函数内跳出Promise
  4. 前面的步骤是先判断 usingTask 的长度是否小于2,小于2就添加到usingTask任务队列里,并直接执行。如果大于2,就先添加到tasks任务队列里,等待添加
  5. 我们在添加到usingTask 里之后,可以直接执行,执行好之后,也就是在then方法里,我们需要做三个事情
  6. 我们先使用 promiseCreator.resolve() 因为我们return 的是Promise对象,所以需要resolve
  7. 既然执行完了,就不需要在占usingTask 的位置,将其删除
  8. 最后将 tasks内的任务添加到 usingTask 内,并执行。因为是队列,所以我们需要调用 shift方法 
class Scheduler {
    constructor() {
        this.tasks = [], // 待运行的任务
        this.usingTask = [] // 正在运行的任务
    }
    // promiseCreator 是一个异步函数,return Promise
    add(promiseCreator) {
        return new Promise((resolve, reject) => {
            promiseCreator.resolve = resolve
            if (this.usingTask.length < 2) {
                this.usingRun(promiseCreator)
            } else {
                this.tasks.push(promiseCreator)
            }
        })
    }
    usingRun(promiseCreator) {
        this.usingTask.push(promiseCreator)
        promiseCreator().then(() => {
            promiseCreator.resolve()
            let index = this.usingTask.findIndex(promiseCreator)
            this.usingTask.splice(index, 1)
            if (this.tasks.length > 0) {
                this.usingRun(this.tasks.shift())
            }
        })
    }
}
const timeout = (time) => new Promise(resolve => {
    setTimeout(resolve, time)
})
const scheduler = new Scheduler()
const addTask = (time, order) => {
    scheduler.add(() => timeout(time)).then(() => console.log(order))
}
addTask(400, 4)
addTask(200, 2)
addTask(300, 3)
addTask(100, 1)
// 2, 4, 3, 1

2.6 instanceof

其实 instanceof 主要的实现原理就是只要右边变量的 prototype 在左边变量的原型链上即可。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。

function new_instance_of(leftVaule, rightVaule) { 
    let rightProto = rightVaule.prototype; // 取右表达式的 prototype 值
    leftVaule = leftVaule.__proto__; // 取左表达式的__proto__值
    while (true) {
    	if (leftVaule === null) {
            return false;	
        }
        if (leftVaule === rightProto) {
            return true;	
        } 
        leftVaule = leftVaule.__proto__ 
    }
}

2.7 实现new

new关键字内部执行

  1. 创建一个空对象,并使该空对象继承constructor.prototype;
  2. 执行构造函数,并将this指向刚刚创建的新对象;
  3. 返回新对象;
function objectFactory() {
    const obj = new Object(); 
    const con = [].shift.call(arguments);// arguments 一个用于被 constructor 调用的参数列表。
    obj.__proto__ = con.prototype;
    var ret = con.apply(obj, arguments);
    
    // ret || obj 这里这么写考虑了构造函数显示返回 null 的情况
    return typeof ret === 'object' ? ret || obj : obj;
};

  1. new产生一个新对象;
  2. 拿到传入的参数中的第一个参数,即构造函数 con;
  3. 执行构造函数,并将this指向创建的空对象obj;
  4. 将传入构造函数的参数,在obj上下文中执行一遍;
  5. 如果构造函数返回一个对象,则直接返回这个对象;

2.8 Promise 实现

面试官:“你能手写一个 Promise 吗”

2.9 数组的扁平(实现flat)

扁平化,顾名思义就是减少复杂性装饰,使其事物本身更简洁、简单,突出主题。就是将一个复杂的嵌套多层的数组,一层一层的转化为层级较少或者只有一层的数组。

2.9.1 flat

  • 用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。
  • 不传参数时,默认“拉平”一层,可以传入一个整数,表示想要“拉平”的层数。
  • 传入 <=0 的整数将返回原数组,不“拉平”
  • Infinity 关键字作为参数时,无论多少层嵌套,都会转为一维数组
  • 如果原数组有空位,flat() 会跳过空位。
const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, "string", { name: "风信子" }, [{a: 1},{b:2},,{c:3}]];

const ddd = arr.flat(Infinity);  // [1,2,3,4,1,2,3,1,2,3,1,2,3,5,"string",{"name":"风信子"},{"a":1},{"b":2},{"c":3}]  空位已跳过。

2.9.2 reduce函数进行递归

const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, "string", { name: "风信子" }, [{ a: 1 }, { b: 2 }, , { c: 3 }]];
function flatten(array, number = 1) {
  return array.reduce((pre, curr) => {
    return pre.concat(Array.isArray(curr) && number > 0 ? flatten(curr, number - 1) : [curr]); // 需要注意curr项应变为数组,否则concat会展开一层
  }, []);
}
const ddd = flatten(arr, Infinity);  
console.log(ddd) // [1,2,3,4,1,2,3,1,2,3,1,2,3,5,"string",{"name":"风信子"},{"a":1},{"b":2},{"c":3}]

2.9.3 递归遍历

const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, "string", { name: "风信子" }, [{ a: 1 }, { b: 2 }, , { c: 3 }]];
function flatten(array, number = 1) {
  let result = [];
  for (const item of array) {
    if (Array.isArray(item) && number > 0) {
      result = result.concat(flatten(item, number - 1));
    } else {
      result.push(item);
    }
  }
  return result;
}

const ddd = flatten(arr, Infinity);
console.log(ddd)  // [1,2,3,4,1,2,3,1,2,3,1,2,3,5,"string",{"name":"风信子"},{"a":1},{"b":2},undefined,{"c":3}]

2.9.4 使用栈和展开运算符

const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, "string", { name: "风信子" }, [{ a: 1 }, { b: 2 }, , { c: 3 }]];
function flatten(array, number = 1) {
  let result = [];
  const stack = [].concat(array);
  while (stack.length > 0) {
    const item = stack.pop();
    if (Array.isArray(item) && number > 0) {
      stack.push(...item);
      number--;
    } else {
      item !== undefined && result.unshift(item);
    }
  }
  return result;
}
const ddd = flatten(arr, Infinity);
console.log(ddd)  // [1,2,3,4,1,2,3,1,2,3,1,2,3,5,"string",{"name":"风信子"},{"a":1},{"b":2},{"c":3}]

2.10 设计模式之发布订阅与观察者

设计模式之发布订阅与观察者

2.11 数组扁平转树形结构数据(Tree)

2.11.1 递归方式

        const flatArr = [
        { id: '01', parentId: 0, name: '节点1' },
        { id: '011', parentId: '01', name: '节点1-1' },
        { id: '0111', parentId: '011', name: '节点1-1-1' },
        { id: '02', parentId: 0, name: '节点2' },
        { id: '022', parentId: '02', name: '节点2-2' },
        { id: '023', parentId: '02', name: '节点2-3' },
        { id: '0222', parentId: '022', name: '节点2-2-2' },
        { id: '03', parentId: 0, name: '节点3' },
      ]
      
    function getTreeData (arr, parentId) {
        function loop (parentId) {
          return arr.reduce((pre, cur) => {
            if (cur.parentId === parentId) {
              cur.children = loop(cur.id)
              pre.push(cur)
            }
            
            return pre
          }, [])
        }
        return loop(parentId)
      }

      const result = getTreeData(flatArr, 0)
      console.log('result', result)


2.11.2 非递归

<script>

  /**
  * 把平铺的数组结构转成树形结构
  */
  const arr = [
    { 'id': '29', 'pid': '', 'name': '总裁办' },
    { 'id': '2c', 'pid': '', 'name': '财务部' },
    { 'id': '2d', 'pid': '2c', 'name': '财务核算部' },
    { 'id': '2f', 'pid': '2c', 'name': '薪资管理部' },
    { 'id': 'd2', 'pid': '', 'name': '技术部' },
    { 'id': 'd3', 'pid': 'd2', 'name': 'Java研发部' }
  ]

  function tranListToTreeData(list) {
    // 1. 定义两个中间变量
    const treeList = [],  // 最终要产出的树状数据的数组
      map = {}        // 存储映射关系


    // 2. 建立一个映射关系,并给每个元素补充children属性.
    // 映射关系: 目的是让我们能通过id快速找到对应的元素
    // 补充children:让后边的计算更方便
    list.forEach(item => {
      if (!item.children) {
        item.children = []
      }
      map[item.id] = item
    })
    //  {
    //    "29": { 'id': '29', 'pid': '',     'name': '总裁办', children:[] },
    //    '2c': { 'id': '2c', 'pid': '',     'name': '财务部', children:[] },
    //    '2d': { 'id': '2d', 'pid': '2c', 'name': '财务核算部', children:[]},
    //    '2f': { 'id': '2f', 'pid': '2c', 'name': '薪资管理部', children:[]},
    //    'd2': { 'id': 'd2', 'pid': '',     'name': '技术部', children:[]},
    //    'd3': { 'id': 'd3', 'pid': 'd2', 'name': 'Java研发部', children:[]}
    //  }

    // 3. 循环
    list.forEach(item => {
      // 对于每一个元素来说,先找它的上级
      //    如果能找到,说明它有上级,则要把它添加到上级的children中去
      //    如果找不到,说明它没有上级,直接添加到 tree3List
      const parent = map[item.pid]
      if (parent) {
        parent.children.push(item)
      } else {
        treeList.push(item)
      }
    })
    // 4. 返回出去
    return treeList
  }

  const treeList = tranListToTreeData(arr)
  console.log(treeList);
</script>

3 JS相关问题

3.1 浅拷贝和深拷贝

浅拷贝和深拷贝都是对于JS中的引用类型而言的,浅拷贝就只是复制对象的引用,如果拷贝后的对象发生变化,原对象也会发生变化。只有深拷贝才是真正地对对象的拷贝。

  1. 赋值运算符 = 实现的是浅拷贝,只拷贝对象的引用值;
  2. JavaScript 中数组和对象自带的拷贝方法都是“首层浅拷贝”,concat、slice、Object.assign()、... 展开运算符;
  3. JSON.stringify 实现的是深拷贝,但是对目标对象有要求,undefined、function、symbol 会在转换过程中被忽略。。。;
  4. 若想真正意义上的深拷贝,请递归。

3.2 let、const、var区别

  • 声明变量的三种方式:varletconst
  • var 在全局作用域下声明变量,会挂载到全局的 window 对象下,全局可用,而 letconst 声明的变量不会
  • var 声明的变量,不是全局作用域就是函数作用域,没有块级作用域,会忽略代码块 {}(除函数外),其因在于 JS 早期块没有词法环境
  • var 声明的变量允许重新声明,而 letconst 不允许重新声明
  • var 声明的变量存在变量提升,但是赋值不会。letconst 不存在变量提升
  • letconst 声明形成块作用域,声明的变量只能在他声明的封闭块内使用
  • 同一作用域下 letconst 不能声明同名变量,而 var 可以
  • letconst 存在暂存性死区(TDZ)
  • const 变量一旦声明必须赋值,不能使用 null 占位,声明后不能再修改,如果声明的是复合类型数据,我们可以更改其属性

3.3 变量提升

原因就是js在创建执行上下文时,会检查代码,找出变量声明和函数声明,并将函数声明完全存储在环境中,而将通过var声明的变量设定为undefined,这就是所谓的变量提升

  • 所有的声明都会提升到作用域的最顶上去。
  • 同一个变量只会声明一次,其他的会被忽略掉或者覆盖掉。
  • 函数声明的优先级高于变量申明的优先级,并且函数声明和函数定义的部分一起被提升。
  • let和var同样存在变量提升,而且let声明还具有暂时性死区的概念。

3.4 promise、async await、Generator的区别

  • async/await 是建立在 Promises上的,不能被使用在普通回调以及节点回调
  • async/await相对于promise来讲,写法更加优雅
  • async/await 和 Promises 很像,不阻塞
  • async/await 代码看起来像同步代码
  • async/await就是generator语法糖,可以用generator来模拟async/await。
  • async/await 让 try/catch 可以同时处理同步和异步错误

3.5 JS继承 原型链继承、构造函数继承、组合继承、原型继承、寄生式继承、寄生组合继承

JS继承 原型链继承、构造函数继承、组合继承、原型继承、寄生式继承、寄生组合继承

3.6 es5的继承和es6的继承有什么区别

  1. ES5 prototype 继承通过原型链(构造函数 + [[prototype]])指向实现继承。 (备注:后续__proto__我都会写成[[prototype]]这种形式)子类的 prototype 为父类对象的一个实例。因此子类的原型对象包含指向父类的原型对象的指针,父类的实例属性为子类原型的属性。

  2. ES6 class 继承 通过class的extends + super实现继承。

    子类没有自己的this对象,因此必须在 constructor 中通过 super 继承父类的 this 对象,而后对此this对象进行添加方法和属性。
    super关键字在构造函数中表示父类的构造函数,用来新建父类的 this 对象。
    内部实现机制上,ES6 的继承机制完全不同,实质是先创造父类的实例对象this---需要提前调用super方法,然后再用子类的构造函数修改this指针。

    super用法

    super 可以作为函数和对象使用的。
    当作为函数使用的时候,只能在子类的构造函数中使用----表示父类的构造函数,但是 super 中的 this 指向的是子类的实例,因此在子类中super()表示的是 Father.prototype.constructor.call(this)。
    当作为对象使用的时候,super表示父类的原型对象,即表示 Father.prototype

区别:

  1. 写法不一样。class的继承通过extends关键字和super函数、super方法继承。(关于super实现继承的使用方式,具体我就不展开了)
  2. 类内部定义的方法都是不可枚举的,这个 ES5 不一样
  3. 类不存在变量提升,这一点与 ES5 完全不同
  4. 类相当于实例的原型,所有在类中定义的方法都会被实例继承。如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就成为静态方法
  5. 内部实现机制不一样。

3.7 数组和字符串常用方法

3.7.1 字符串方法

  1. length 长度
  2. charAr(index) 获取对应位置的字符串
  3. concat(string) 字符串合并
  4. includes(string) 检查字符串是否包含子字符串
  5. replace(stringToBeReplaced,stringToAdd) 字符串替换
  6. split(string) 字符串拆分成数组
  7. substring(index, index) 某个索引或索引范围处拆分字符串
  8. substr(start,lenght) 方法可在字符串中抽取从 start 下标开始的指定数目的字符
  9. slice(start, end) 方法可提取字符串的某个部分,并以新的字符串返回被提取的部分。

3.7.2 数组的方法

  1. push(),向数组的末尾添加一个或多个元素,并返回新的数组长度。原数组改变。
  2. pop(),删除并返回数组的最后一个元素,若该数组为空,则返回undefined。原数组改变。
  3. unshift(),向数组的开头添加一个或多个元素,并返回新的数组长度。原数组改变。
  4. shift(),删除数组的第一项,并返回第一个元素的值。若该数组为空,则返回undefined。原数组改变。
  5. concat(arr1,arr2…),合并两个或多个数组,生成一个新的数组。原数组不变。
  6. join(),将数组的每一项用指定字符连接形成一个字符串。默认连接字符为 “,” 逗号。
  7. reverse(),将数组倒序。原数组改变。
  8. sort(),对数组元素进行排序。按照字符串UniCode码排序,原数组改变。
  9. map(function),原数组的每一项执行函数后,返回一个新的数组。原数组不变。(注意该方法和forEach的区别)。
  10. slice() 按照条件查找出其中的部分内容 2个参数 查找的位置 不改变原数组
  11. splice(index,howmany,arr1,arr2…) ,用于添加或删除数组中的元素。从index位置开始删除howmany个元素,并将arr1、arr2…数据从index位置依次插入。howmany为0时,则不删除元素。原数组改变。

原数组改变的方法有:push pop shift unshift reverse sort splice
不改变原数组的方法有:concat map filter join every some indexOf slice forEach

3.8 模块化(CommonJS, AMD, UMD, CMD, ES6)的区别

3.8.1 模块化

模块化开发是一种管理方式,一种生产方式,也是一种解决问题的方案。一个模块就是实现某个特定功能的文件,我们可以很方便的使用别人的代码,想要什么模块,就引入那个模块。但是模块开发要遵循一定的规范,后面就出现了我们所熟悉的一系列规范。

到目前为止,大概分为以下几个里程碑式节点。

原始的开发方式 ---> CommonJS ---> AMD ---> CMD ---> UMD ---> ES6Module

3.8.2 CommonJS

CommonJS规范,主要运行于服务器端,同步加载模块,而加载的文件资源大多数在本地服务器,所以执行速度或时间没问题。一个单独的文件就是一个模块。

模块功能主要的几个命令:requiremodule.exportsrequire命令用于输入其他模块提供的功能,module.exports命令用于规范模块的对外接口,输出的是一个值的拷贝,输出之后就不能改变了,会缓存起来。

优点:

  1. 简单并且容易使用
  2. 服务器端模块便于重用

缺点:

  1. 同步的模块加载方式不适合在浏览器环境中
  2. 不能非阻塞的并行加载多个模块

3.8.3 AMD

AMD(Asynchronous Module Definition - 异步加载模块定义)规范,制定了定义模块的规则,一个单独的文件就是一个模块,模块和模块的依赖可以被异步加载。主要运行于浏览器端,这和浏览器的异步加载模块的环境刚好适应,它不会影响后面语句的运行。该规范是在RequireJs的推广过程中逐渐完善的。

模块功能主要的几个命令:definerequirereturndefine.amddefine是全局函数,用来定义模块,define(id?, dependencies?, factory)require命令用于输入其他模块提供的功能,return命令用于规范模块的对外接口,define.amd属性是一个对象,此属性的存在来表明函数遵循AMD规范。

// moduleA.js 
define(['jQuery','lodash'], function($, _) { 
    var name = 'weiqinl', 
    function foo() {

    } 
    return { name, foo } 
}) 

// index.js 
require(['moduleA'], function(a) {
    a.name === 'weiqinl' // true 
    a.foo() // 执行A模块中的foo函数 
    // do sth... 
}) 

// index.html 
<script src="js/require.js" data-main="js/index"></script>

优点:

  1. 适合在浏览器环境中异步加载模块
  2. 可以并行加载多个模块

缺点:

  1. 提高了开发成本
  2. 不符合通用的模块化思维方式

3.8.4 CMD

CMD(Common Module Definition - 通用模块定义)规范主要是Sea.js推广中形成的,一个文件就是一个模块,可以像Node.js一般书写模块代码。主要在浏览器中运行,当然也可以在Node.js中运行。

// moduleA.js
// 定义模块
define(function(require, exports, module) {
var func = function() {
var a = require('./a') // 到此才会加载a模块
a.func()
if(false) {
var b = require('./b') // 到此才会加载b模块
b.func()
}
}
// do sth...
exports.func = func;
})

// index.js
// 加载使用模块
seajs.use('moduleA.js', function(ma) {
var ma = math.func()
})

// HTML,需要在页面中引入sea.js文件。
<script src="./js/sea.js"></script>
<script src="./js/index.js"></script>

这里define是一个全局函数,用来定义模块,并通过exports向外提供接口。之后,如果要使用某模块,可以通过require来获取该模块提供的接口。最后使用某个组件的时候,通过seajs.use()来调用。

优点:可以很容易在 node 中运行

缺点:依赖 SPM 打包,模块的加载逻辑偏重

3.8.5 UMD

UMD(Universal Module Definition - 通用模块定义)模式,该模式主要用来解决CommonJS模式和AMD模式代码不能通用的问题,并同时还支持老式的全局变量规范。

既然要通用,怎么办呢?那就先判断是否支持 node 的模块,支持就使用 node;再判断是否支持 AMD,支持则使用 AMD 的方式加载。这就是所谓的 UMD

(function (window, factory) {
  if (typeof exports === "object") {
    // CommonJS
    module.exports = factory();
  } else if (typeof define === "function" && define.amd) {
    // AMD
    define(factory);
  } else {
    // 浏览器全局定义
    window.eventUtil = factory();
  }
})(this, function () {
  // do something
});

3.8.6 ES6 Module

ES6的模块化已经不是规范了,而是JS语言的特性。随着ES6的推出,AMD和CMD也随之成为了历史。

模块化规范输出的是一个值的拷贝,ES6 模块输出的是值的引用。 模块化规范是运行时加载,ES6 模块是编译时输出接口。 模块化规范输出的是一个对象,该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,ES6 module 是一个多对象输出,多对象加载的模型。从原理上来说,模块化规范是匿名函数自调用的封装,而ES6 module则是用匿名函数自调用去调用输出的成员。

它和前几种方式有区别和相同点。

  1. 它因为是标准,所以未来很多浏览器会支持,可以很方便的在浏览器中使用。
  2. 它同时兼容在node环境下运行。
  3. 模块的导入导出,通过importexport来确定。
  4. 可以和Commonjs模块混合使用。
  5. CommonJS输出的是一个值的拷贝。ES6模块输出的是值的引用,加载的时候会做静态优化。
  6. CommonJS模块是运行时加载确定输出接口,ES6模块是编译时确定输出接口。

3.9 ajax axios fetch 区别

ajaxaxiosfetch 是在前端开发中用于进行网络请求的三种常见方法。它们都可以用于向服务器发送请求并获取响应数据,但在使用方式和功能方面有一些区别。以下是它们之间的主要区别:

  1. ajaxajax 是一种基于原生 JavaScript 的异步请求技术。它使用 XMLHttpRequest 对象来发送请求和接收响应。ajax 具有相对较低的层级封装,需要开发人员手动处理请求和响应的细节。使用 ajax 时,你可以直接操作请求头、设置请求方法和处理响应。
  2. axiosaxios 是一个基于 Promise 的 HTTP 客户端,可以在浏览器和 Node.js 中使用。它提供了更高级别的封装,使发送请求和处理响应更加简单和灵活。axios 支持以简洁的方式设置请求参数、处理请求和响应拦截器,并提供了更好的错误处理和取消请求的支持。
  3. fetchfetch 是浏览器内置的 API,用于发送网络请求。它提供了一种现代化、基于 Promise 的方式来进行网络通信。与 axios 类似,fetch 也提供了一种较低级别的封装,但相比于 axios,它的功能和语法更为简单。fetch 通过链式调用的方式设置请求参数,返回的是一个 Promise 对象。

三者做个对比:

网络请求特点
Ajax一种技术统称,主要利用XHR实现网络请求
Fetch具体API,基于promise,实现网络请求
Axios一个封装库,基于XHR封装,较为推荐使用

3.10 Set、WeakSet、Map、WeakMap的区别

3.10.1 Set和WeakSet的区别

  1. 里面的元素都唯一,Set的元素可以是任何值,而WeakSet的元素只能是对象。
  2. 对于基本类型的值,只要“===”为真,则这两个值相等(无法加入Set中)。对于引用类型(Set和WeakSet中),只有地址相同,它们才不唯一,而值相同的两个不同对象,它们是不同的。
  3. NaN被视为同一个值。
  4. WeakSet中的元素是弱引用,即随时会消失(被回收)。

3.10.2 Map和WeakMap的区别

  1. Map的键和值都可以是任意类型,而WeakMap的键只能是对象。
  2. Map和WeakMap的键必须唯一,判断方式是“===”和是否地址相同。
  3. Map视NaN为同一个键。
  4. WeakMap中的键值对的键是弱引用,WeakMap中的成员随时会消失。

3.10.3 Set和Map区别

Set、WeakSet都是元素的集合,而Map和WeakMap是键值对的集合。

4 浏览器相关知识

4.1 从输入URL到呈现页面过程

  1. 浏览器地址栏输入 URL 并回车
  2. 浏览器查找当前 URL 是否存在缓存,并比较缓存是否过期
    • 通过Cache-ControlExpires来检查是否命中强缓存,命中则直接取本地磁盘的html(状态码为200 from disk(or memory) cache,内存or磁盘);
    • 如果没有命中强缓存,则会向服务器发起请求,服务器通过EtagLast-Modify来与服务器确认返回的响应是否被更改(协商缓存),若无更改则返回状态码(304 Not Modified),浏览器取本地缓存;
    • 若强缓存和协商缓存都没有命中则返回请求结果。
  3. DNS 解析 URL 对应的 IP
    • DNS 解析首先会从你的浏览器的缓存中去寻找是否有这个网址对应的 IP 地址,如果没有就向OS系统的 DNS 缓存中寻找,如果没有就是路由器的 DNS 缓存, 如果没有就是 ISP 的DNS 缓存中寻找。
    • DNS缓存的寻找过程就是: 浏览器 -> 系统 -> 路由器 -> ISP。
    • 如果在某一个缓存中找到的话,就直接跳到下一步。 如果都没有找到的话,就会向 ISP 或者公共的域名解析服务发起 DNS 查找请求。这个查找的过程还是一个递归查询的过程。
  4. 根据 IP 建立 TCP 连接(三次握手)
  5. 发送 http 请求
  6. 服务器处理请求,浏览器接受 HTTP 响应
  7. 浏览器解析并渲染页面
  8. 关闭 TCP 连接(四次握手)

4.1.1 浏览器地址栏输入 URL 并回车

用户在地址栏输入内容之后,浏览器会首先判断用户输入的是合法的URL还是搜索内容,如果是搜索内容就合成URL,如果是合法的URL就开始进行加载。

4.1.2 http缓存

当我们第一次访问网站的时候,比如 juejin.cn,电脑会把网站上的图片和数据下载到电脑上,当我们再次访问该网站的时候,网站就会从电脑中直接加载出来,这就是缓存。

缓存的优点

  1. 减少了不必要的数据传输,节省带宽。
  2. 减少服务器的负担,提升网站性能。
  3. 加快了客户端加载网页的速度。
  4. 用户体验友好。

强缓存

强制缓存不需要发送请求到服务端,根据请求头expires和cache-control判断是否命中强缓存

控制强制缓存的字段分别是 Expires 和 Cache-Control,其中Cache-Control优先级比Expires高。

Expires 设置过期时间

4.2 浏览器的兼容性

4.2.1 了解浏览器

造成浏览器兼容性问题的根本原因就是:各浏览器使用了不同的内核,并且它们处理同一件事情的时候思路不同。

主流浏览器 有五个:IE(Trident内核)、Firefox(火狐:Gecko内核)、Safari(苹果:webkit内核)、Google Chrome(谷歌:Blink内核)、Opera(欧朋:Blink内核)

四大内核:Trident(IE内核)、Gecko(Firefox内核)、webkit内核、Blink(Chrome内核)

从浏览器内核的角度来看,浏览器兼容性问题可分为以下三类:

  • 渲染相关:和 样式 相关的问题,即体现在布局效果上的问题。
  • 脚本相关:和 脚本 相关的问题,包括JavaScript和DOM、BOM方面的问题。对于某些浏览器的功能方面的特性,也属于这一类。
  • 其他类别:除以上两类问题外的功能性问题,一般是浏览器自身提供的功能,在内核层之上的。

4.2.2 如何处理

对于大多数据的兼容性问题我们可以通过项目的管理工具(webpack)进行处理,

4.2.2.1 JavaScript代码兼容性

browserlist

browserslist是什么呢?它是一个浏览器兼容性配置,并且它被多个我们常用的JS工具使用,比如Babel,Eslint。具体怎么用呢?Browserlist有一些方便理解的查询语法,如:

> 5% // 全球用户量大于5%的浏览器
last 2 versions // 各浏览器的最近2个版本
not dead // "dead"指超过24个月没有官方支持或更新的浏览器

通过这些查询语句就能找到符合要求的厂商与版本的浏览器,从而Babel等工具可以利用它来做针对性的语法转换或其他工作。

browserslist可以在项目的package.json或.browserlistrc文件中配置

语法转换: 引入合适的loader。使用专门用来处理JS语法转换的一个工具:Babel。Babel是一个JavaScript编译器,它主要用来将ES6+的高级语法转换为兼容性更好的语法,从而支持一些老版本的浏览器。

在Webpack中,我们可以添加对应的babel-loader。安装npm install -D babel-loader @babel/core @babel/preset-env。配置如下:

module: {
  rules: [
    {
      test: /.m?js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    }
  ]
}

  • babel-loader:babel的Webpack loader。
  • @babel/core:babel编译器的核心库。包含源码转AST,AST转源码等核心工具函数。
  • @babel/preset-env:一个babel preset。它可以根据浏览器兼容性配置来做针对性的语法转换。

Polyfill:

polyfill的目的是注入一些API,比如Object.assign,以在浏览器本身不支持的情况下调用。

语法转换与Polyfill是各司其职的。像constvar这种语法层面的就只能通过转译来做,像String.prototype.includes这种API方法就需要polyfill。另外,polyfill的一个好处是,如果浏览器中存在原生方法时将优先使用,这样性能与安全性更好。

4.2.2.2 css 相关

清除默认样式

通过添加base样式清除浏览器默认样式,第三方库:Reset-css 、 normalize.css

目标浏览器来添加对应的CSS前缀

在Webpack中使用postcss-loader

postcss与Babel相似,它本身只包含一些核心API,但不处理具体的转换工作,所以需要添加一些postcss的plugin,如autoprefixerpostcss-preset-env。这些插件可以在项目下的postcss.config.js文件中配置:


const postcssPresetEnv = require("postcss-preset-env");

module.exports = {
  plugins: [require("autoprefixer"), postcssPresetEnv(/* pluginOptions */)],
};

CSS Hack

CSS属性Hack、CSS选择符Hack以及IE条件注释Hack, Hack主要针对IE浏览器。

1、属性级Hack:比如IE6能识别下划线””和星号” * “,IE7能识别星号” * “,但不能识别下划线””,而firefox两个都不能认识。

2、选择符级Hack:比如IE6能识别html .class{},IE7能识别+html .class{}或者*:first-child+html .class{}。

3、IE条件注释Hack:IE条件注释是微软从IE5开始就提供的一种非标准逻辑语句。比如针对所有IE:<!–[if IE]><!–您的代码–><![endif]–>,针对IE6及以下版本:<!–[if lt IE 7]><!–您的代码–><![endif]–>,这类Hack不仅对CSS生效,对写在判断语句里面的所有代码都 会生效。

PS:条件注释只有在IE浏览器下才能执行,这个代码在非IE浏览下被当做注释视而不见。可以通过IE条件注释载入不同的CSS、JS、HTML和服务器代码等。

js
复制代码
常用的CSS Hack
/* CSS属性级Hack */
color:red; /* 所有浏览器可识别*/
_color:red; /* 仅IE6 识别 */
*color:red; /* IE6、IE7 识别 */
+color:red; /* IE6、IE7 识别 */
*+color:red; /* IE6、IE7 识别 */
[color:red; /* IE6、IE7 识别 */
color:red9; /* IE6、IE7、IE8、IE9 识别 */
color:red; /* IE8、IE9 识别*/
color:red9; /* 仅IE9识别 */
color:red ; /* 仅IE9识别 */
color:red!important; /* IE6 不识别!important*/
-------------------------------------------------------------
/* CSS选择符级Hack */
*html #demo { color:red;} /* 仅IE6 识别 */
*+html #demo { color:red;} /* 仅IE7 识别 */
body:nth-of-type(1) #demo { color:red;} /* IE9+、FF3.5+、Chrome、Safari、Opera 可以识别 */
head:first-child+body #demo { color:red; } /* IE7+、FF、Chrome、Safari、Opera 可以识别 */
:root #demo { color:red9; } : /* 仅IE9识别 */
--------------------------------------------------------------
/* IE条件注释Hack */
<!--[if IE]>此处内容只有IE可见<![endif]-->
<!--[if IE 6]>此处内容只有IE6.0可见<![endif]-->
<!--[if IE 7]>此处内容只有IE7.0可见<![endif]-->
<!--[if !IE 7]>此处内容只有IE7不能识别,其他版本都能识别,当然要在IE5以上。<![endif]-->
<!--[if gt IE 6]> IE6以上版本可识别,IE6无法识别 <![endif]-->
<!--[if gte IE 7]> IE7以及IE7以上版本可识别 <![endif]-->
<!--[if lt IE 7]> 低于IE7的版本才能识别,IE7无法识别。 <![endif]-->
<!--[if lte IE 7]> IE7以及IE7以下版本可识别<![endif]-->
<!--[if !IE]>此处内容只有非IE可见<![endif]-->

常见问题

  1. ie url传递中文参数乱码

    解决方案:对get的参数使用encodeURIComponent进行编码

  2. NodeList在ie中不支持forEach方法

    解决方案:NodeList本质为一个集合,是一个类数组结构,所以可以转为数组结构再使用相关数组api。

juejin.cn/post/697293…

5 排序相关

5.1 冒泡排序

排序描述:

  1. 比较相邻的两个元素,如果第一个比第二个大,就进行交换
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素就是最大的数;
  3. 重复1~2步骤,直至排序完成

2019040407112968.gif

代码实现:

function bubbleSort(target) {
  let temp = null
  const len = target.length;
  console.time('冒泡排序耗时');
  // 外层循环i控制比较的轮数
  for (let i = 0; i < len; i++) {
    // 里层循环控制每一轮比较的次数j,target[i] 只用跟其余的len - i个元素比较
    for (let j = 0; j < len - i; j++) {
      if (target[j] > target[j + 1]) {
        temp = target[j]
        target[j] = target[j + 1]
        target[j + 1] = temp
      }
    }
  }
  console.timeEnd('冒泡排序耗时');
  return target
}

缺点: 当目标对象是已经排序好的数据,也需要进行多次遍历。

改进的冒泡排序法:

定义一个布尔值,在遍历每一次的时候,如果发生位置交换,就改变布尔值 当这一次循环结束之后,判断该布尔值是否变化 变化了则继续下一次,没变则退出。

 function bubbleSort(target) {
  let temp = null
  const len = target.length;
  for (let i = 0; i < len - 1; i++) {
    let flag = false
    for (let j = 0; j < len - i; j++) {
      if (target[j] > target[j + 1]) {
        temp = target[j]
        target[j] = target[j + 1]
        target[j + 1] = temp
        flag = true
      }
    }
    console.log(`第 ${i + 1} 次排序`)
    if (!flag) break
  }

  return target
}

console.log(bubbleSort([1, 2, 3, 4, 5, 6]))

5.2 插入排序

排序描述:

  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  5. 将新元素插入到该位置后;
  6. 重复步骤2~5。

2019040407121894.gif

代码实现:

function insertionSort(arr) {
  console.time('插入排序耗时:');
  for (var i = 1; i < arr.length; i++) {
    var current = arr[i]; // 当前元素
    var preIndex = i - 1; // 当前元素的前一个
    // 负责往前查找对比 
    while (preIndex >= 0 && arr[preIndex] > current) {
      arr[preIndex + 1] = arr[preIndex];
      preIndex--;
    }
    arr[preIndex + 1] = current;
  }
  console.timeEnd('插入排序耗时:');
  return arr;
}

var arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
console.log(insertionSort(arr))

5.3 快速排序

排序描述:

快速排序使用分治的思想,通过一趟排序将待排序列分割成两部分,其中一部分记录的关键字均比另一部分记录的关键字小。之后分别对这两部分记录继续进行排序,以达到整个序列有序的目的。

  1. 选择基准:在待排序列中,按照某种方式挑出一个元素,作为 "基准"(pivot)
  2. 分割操作:以该基准在序列中的实际位置,把序列分成两个子序列。此时,在基准左边的元素都比该基准小,在基准右边的元素都比基准大
  3. 递归地对两个序列进行快速排序,直到序列为空或者只有一个元素。

2019040407132111.gif

function quickSort(array, left, right) {
  if (Object.prototype.toString.call(array).slice(8, -1) === 'Array' && typeof left === 'number' && typeof right === 'number') {
    console.time('1.快速排序耗时');
    if (left < right) {
      var x = array[right], i = left - 1, temp;
      for (var j = left; j <= right; j++) {
        if (array[j] <= x) {
          i++;
          temp = array[i];
          array[i] = array[j];
          array[j] = temp;
        }
      }
      quickSort(array, left, i - 1);
      quickSort(array, i + 1, right);
    }
    console.timeEnd('1.快速排序耗时');
    return array;
  } else {
    return 'array is not an Array or left or right is not a number!';
  }
}

5.4 选择排序

从左到右开始找,每遍历一次将最小值跟当前遍历的第一个元素交换 时间复杂度 O(n^2^)

2019040407115547.gif

function selctionSort(target) {
  for (let i = 0; i < target.length - 1; i++) {
    let min = target[i]
    let minIndex = i

    for (let j = i + 1; j < target.length; j++) {
      if (target[j] < min) {
        min = target[j]
        minIndex = j
      }
    }

    console.log(`第${i + 1}次循环`, target)
    if (minIndex === i) continue
    target[minIndex] = target[i]
    target[i] = min
  }

  return target
}

6 webpack

6.1 webpack的干什么的。

主要功能:它提供了友好的前端模块化开发支持,以及代码压缩混淆、处理浏览器端JavaScript的兼容性、性能优化等强大的功能。

好处:让程序员把工作的中心放到具体功能的实现上,提高了前端开发效率和项目的可维护性。

1、减少请求,来达到减少性能消耗以及用户体验。从上面定义我们就可以知道,Webpack 可以将很多静态资源打包整合到一起,以前请求接口会有很多链接地址,每次请求都会向服务器询问,然后服务器返回需要js等,要是很多静态资源,将会请求很多,影响性能以及用户的体验,Webpack可以将打包的资源整合

2、将es6语法转变成es5语法,兼容老浏览器。一些老的终端机中的浏览器,兼容不到es6语法,可以通过Webpack来打包,转换成兼容的es5写法,例如es6的箭头函数等。

3、增强项目生命力,增强代码隐蔽性。不单单请求少了,性能提高了,还可以将静态资源图片、页面以及css等打包压缩,早期项目都是直接暴露出来,所以安全性隐蔽性不高,Webpack还可以提供丰富的插件。

6.2 webpack性能优化

6.2.1 开发环境性能优化

HMR

概念:hot module replacement 热模块替换 / 模块热替换

作用:一个模块发生变化,只会重新打包这一个模块,而不是打包所有模块,极大的提升了构建速度

  1. 样式文件:可以使用 HMR 功能:因为 style-loader 内部已经实现
  2. js 文件:默认不能使用 HMR 功能:开启需要添加支持 HMR 功能的 js 代码,且只能处理 「非入口 js 文件」(入口文件将其它文件全部引入,若添加,会导致全部重新加载)
  3. html 文件:默认不能使用 HMR 功能,同时会导致 「html 文件不能热更新」
    解决:修改 「entry」 入口,将 html 文件引入
module.exports = {  
// 引入html,解决热更新的问题  
entry: [
    './src/js/index.js', 
    './src/index.html'
    ],  
devServer: {    
    // 开启 HMR 功能    // 当修改了 webpack 配置,新配置要想生效,必须重启服务    
    hot: true  
  }
}

source-map

概念:一种提供源代码构建后代码映射技术(如果构建后代码出错了,可以通过映射追踪到源代码错误)

  • 参数:[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map

    • source-map:外部(错误代码的准确信息 和 位置)
    • inline-source-map:内联(只生成一个内联 source-map)(错误代码的准确信息 和 位置)
    • hidden-source-map:外部(直接生成 .map 文件)(不能追踪源代码错误,只能提示到构建后代码的错误位置)
    • eval-source-map:内联(每一个文件都生成对应的 source-map,都在 eval)(错误代码的准确信息 和 位置)
    • nosources-source-map:外部(错误代码的准确信息,没有源代码信息)
    • cheap-source-map:外部(错误代码的准确信息 和 位置,但只能精确到行)
    • cheap-module-source-map:外部(错误代码的准确信息 和 位置,会将 loader 的 source-map 加入)
  • 开发环境:速度快,调试更友好。eval-source-map / eval-cheap-module-source-map
    (vue 和 react 脚手架中默认使用:eval-source-map

    • 速度快:(eval > inline > cheap > ...)
      eval-cheap-source-map
      eval-source-map
    • 调试友好:
      source-map
      cheap-module-source-map
      cheap-source-map
  • 生产环境:源代码要不要隐藏?调试要不要更友好?source-map / cheap-module-source-map
    内联会让代码体积变大,所以在生产环境中只会只用 「外部 source-map」
    nosources-source-map
    hidden-source-map

module.exports = {  
    mode: 'development', // 'production'  
    devtool: 'eval-source-map' // 'source-map'
}

6.2.2 生产环境性能优化

webpack-bundle-analyzer

配置webpack-bundle-analyzer包可以帮助了解捆绑包中的真实内容,找出哪些模块构成了其最大尺寸,查找错误到达那里的模块。

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}

多线程构建

由于 Javascript 是单线程的,webpack 在构建上默认也是单线程的。

多线程可以提高程序的效率,在 webpack 中,就可以使用 thread-loader 来启用多线程的加载器。

{
    test: /.js$/,
    use: [
        'thread-loader',
        'babel-loader'
    ]
}

缓存加载器

在我们的项目开发过程中,Webpack 需要多次构建项目。为了加快后续构建,我们可以使用缓存,与缓存相关的加载器是缓存加载器。 cache-loader

{
        test: /.js$/,
        use: [
          'cache-loader',
          'thread-loader',
          'babel-loader'
        ],
}

缩小文件范围loader

  • 优化loader配置
  • test、include、exclude三个配置项来缩⼩loader的处理范围
  • 推荐include
{
    test: /.js$/,

    include: path.resolve(__dirname, '../src'),

    exclude: /node_modules/,
    use: [
        'babel-loader'
    ]
}

使⽤externals优化cdn静态资源

公用代码提取,使用 CDN 加载,对于vue,vuex,vue-router,axios,echarts,swiper等我们可以利用webpack的externals参数来配置,这里我们设定只需要在生产环境中才需要使用。

抽离CSS

mini-css-extract-plugin

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
 {
 test: /\.less$/,
 use: [
 // "style-loader", // 不再需要style-loader,⽤MiniCssExtractPlugin.loader代替
  MiniCssExtractPlugin.loader,
  "css-loader", // 编译css
  "postcss-loader",
  "less-loader" // 编译less
 ]
 },
plugins: [
  new MiniCssExtractPlugin({
   filename: "css/[name]_[contenthash:6].css",
   chunkFilename: "[id].css"
  })
 ]

压缩CSS

css-minimizer-webpack-plugin 可以压缩和去重 CSS 代码。

optimization: {
    minimizer: [
      new CssMinimizerPlugin(),
    ],
  }

压缩JS

terser-webpack-plugin 可以压缩和去重 JavaScript 代码。

const TerserPlugin = require('terser-webpack-plugin') 
optimization: { 
    minimizer: [ 
        new CssMinimizerPlugin(), 
        new TerserPlugin({ 
            terserOptions: {
                compress: { 
                    drop_console: true, // remove console statement 
                }, 
            }, 
        }), 
    ], 
}

tree-shaking

tree-shaking 就是:只编译实际用到的代码,不编译项目中没有用到的代码。

code split

code split 直接从字面上理解即可,就是代码分割技术,因为 html 里面的静态资源文件是并行加载的,即发送 http 请求并且把文件放到内存里这个过程是并行的。所以适当的代码分割技术可以让我们的项目运行性能更好。另外,代码分割也可以帮助我们在多路由的场景进行性能优化。

多入口配置

entry: {
  main: './src/js/index.js',
  test: './src/js/test.js'
},
output: {
  filename: 'js/[name].[contenthash:10].js'
  path: resolve(__dirname, 'build')
}

多个入口会打包生成多个 bundle.js 文件,减少多个体积

optimization配置

  1. node_modules 中代码单独打包成一个 chunk
  2. 自动分析多入口 chunk 中,有没有公共文件,如果有会打包成一个单独 chunk
optimization: {
splitChunks: {
      chunks: "async", //对同步 initial,异步 async,所有的模块有效 all
      minSize: 30000, //最⼩尺⼨,当模块⼤于30kb
      maxSize: 0, //对模块进⾏⼆次分割时使⽤,不推荐使⽤
      minChunks: 1, //打包⽣成的chunk⽂件最少有⼏个chunk引⽤了这个模块
      maxAsyncRequests: 5, //最⼤异步请求数,默认5
      maxInitialRequests: 3, //最⼤初始化请求书,⼊⼝⽂件同步请求,默认3
      automaticNameDelimiter: "-", //打包分割符号
      name: true, //打包后的名称,除了布尔值,还可以接收⼀个函数function
      cacheGroups: {
        //缓存组
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          name: "vendor", // 要缓存的 分隔出来的 chunk 名称
          priority: -10, //缓存组优先级 数字越⼤,优先级越⾼
        },
        other: {
          chunks: "initial", // 必须三选⼀: "initial" | "all" | "async"(默认就async)
          test: /react|lodash/, // 正则规则验证,如果符合就提取 chunk,
          name: "other",
          minSize: 30000,
          minChunks: 1,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true, //可设置是否重⽤该chunk
        },
      },
    },
    }

import 函数

import('./test').then(res=>{
  console.log(res)
})

在上面这个例子中 test.js 文件会单独打包成一个 chunk。

dlls

有的时候公司并没有提供 cdn 上的一些库资源,但是你也不希望生产环境中反复给一些常用库打包,这个时候可以考虑 dlls。dlls 技术的原理是它可以帮你在本地提前打包好指定库,然后在项目再次打包的时候直接从本地引入而不需要再次打包。

// webpack.config.js
{
   plugins:[
    // 告诉 webpack 哪些库不参与打包,同时使用时改变名称
    new webpack.DllReferencePlugin({
      manifest: resolve(__dirname, 'dll/manifest.json') // 从 manifest 里面找映射关系
    }),
    new AddAssetHtmlWebpackPlugin({
      filepath: resolve(__dirname, 'dll/iquery') // 
    })
  ]
}

// webpack.dll.js
{
  entry:{
    jquery: ['jquery']  
  }
  output:{
    filename: '[name].js',
    path: resolve(__dirname, 'dll'),
    library: '[name]_[hash]' // 你打包好的库叫什么名字
  },
  plugin:{
     new webpack.DllPlugin({
       name: '[name]_[hash]',
       // 库和名称的映射关系,(存放库的路径,库包含哈希的名称)
       path: resolve(__dirname, 'dll/manifest.json')
     }),
  }
}

总结:dll 和 external 的区别是,dll 是打包好放到本地服务上;external 是不打包直接从 cdn 上引入。两者为同一个问题提供了两种略有差异的解决方案

7 vue 和 react

7.1 Vuex 和 Redux 的区别

从表面上来说,store 注入和使用方式有一些区别。

在 Vuex 中,$store 被直接注入到了组件实例中,因此可以比较灵活的使用:

  • 使用 dispatch 和 commit 提交更新
  • 通过 mapState 或者直接通过 this.$store 来读取数据

在 Redux 中,我们每一个组件都需要显示的用 connect 把需要的 props 和 dispatch 连接起来。

另外 Vuex 更加灵活一些,组件中既可以 dispatch action 也可以 commit updates,而 Redux 中只能进行 dispatch,并不能直接调用 reducer 进行修改。

从实现原理上来说,最大的区别是两点:

  • Redux 使用的是不可变数据,而Vuex的数据是可变的。Redux每次都是用新的state替换旧的state,而Vuex是直接修改
  • Redux 在检测数据变化的时候,是通过 diff 的方式比较差异的,而Vuex其实和Vue的原理一样,是通过 getter/setter来比较的(如果看Vuex源码会知道,其实他内部直接创建一个Vue实例用来跟踪数据变化)

7.1.1 Redux和 Vuex 设计思想

vuex和Redux本质上都是建立一套让开发者对全局变量的修改有意识的机制,这套机制为了让开发者意识到自己在做一些重要的、影响较大的操作,所以加入了一些繁琐的操作,这是状态管理库的基本设计思想。我认为状态管理库必须有以下特点:

  • 必须能够且易于共享数据,它的最基础功能就是提供共享的状态。
  • 提供一套机制用于修改状态,这套机制必须能让开发者意识到自己在修改状态。
  • 相同状态对应相同视图,不同状态对应不同的视图。
7.1.1.1 Redux
  1. Redux则是一个纯粹的状态管理系统,React利用React-Redux将它与React框架结合起来;
  2. 只有一个用createStore方法创建一个 store;
  3. action接收 view 发出的通知,告诉 Store State 要改变,有一个 type 属性;
  4. reducer:纯函数来处理事件,纯函数指一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,得到一个新的 state;
7.1.1.2 Vuex
  1. Vuex是吸收了Redux的经验,放弃了一些特性并做了一些优化,代价就是VUEX只能和VUE配合;
  2. store:通过 new Vuex.store创建 store,辅助函数mapState;
  3. getters:获取state,有辅助函数 mapGetters;
  4. action:异步改变 state,像ajax,辅助函数mapActions;
  5. mutation:同步改变 state,辅助函数mapMutations;
7.1.1.3 对比
1.Redux: view——>actions——>reducer——>state变化——>view变化(同步异步一样)
2.Vuex: view——>commit——>mutations——>state变化——>view变化(同步操作) 
view——>dispatch——>actions——>mutations——>state变化——>view变化(异步操作)

7.2 vue和react的区别

Vue的优势包括:

  • 模板和渲染函数的弹性选择
  • 简单的语法及项目创建
  • 更快的渲染速度和更小的体积

React的优势包括:

  • 更适用于大型应用和更好的可测试性
  • 同时适用于Web端和原生App
  • 更大的生态圈带来的更多支持和工具

而实际上,React和Vue都是非常优秀的框架,它们之间的相似之处多过不同之处,并且它们大部分最棒的功能是相通的:

  • 利用虚拟DOM实现快速渲染
  • 轻量级
  • 响应式组件
  • 服务器端渲染
  • 易于集成路由工具,打包工具以及状态管理工具
  • 优秀的支持和社区

juejin.cn/post/684490…

juejin.cn/post/684700…

7.3 diff 对比

7.3.1 react

1.Virtual DOM 中的首个节点不执行移动操作(除非它要被移除),以该节点为原点,其它节点都去寻找自己的新位置; 一句话就是首位是老大,不移动;

2.在 Virtual DOM 的顺序中,每一个节点与前一个节点的先后顺序与在 Real DOM 中的顺序进行比较,如果顺序相同,则不必移动,否则就移动到前一个节点的前面或后面;

3.tree diff:只会同级比较,如果是跨级的移动,会先删除节点 A,再创建对应的 A;将 O(n3) 复杂度的问题转换成 O(n) 复杂度;

4.component diff:

  • 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。
  • 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。
  • 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。

5.element differ:
tree differ 下面有三种节点操作:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)

  • INSERT_MARKUP,新的 component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作。
  • MOVE_EXISTING,在老集合有新 component 类型,且 element 是可更新的类型,generateComponentChildren 已调用 receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。
  • REMOVE_NODE,老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。

7.3.2 vue

  1. 自主研发了一套Virtual DOM,是借鉴开源库snabbdom.
  2. 也是同级比较,因为在 compile 阶段的optimize标记了static 点,可以减少 differ 次数;
  3. Vue 的这个 DOM Diff 过程就是一个查找排序的过程,遍历 Virtual DOM 的节点,在 Real DOM 中找到对应的节点,并移动到新的位置上。不过这套算法使用了双向遍历的方式,加速了遍历的速度,

7.3.3 对比

相同点:
都是同层 differ,复杂度都为 O(n);

不同点:
1.React 首位是除删除外是固定不动的,然后依次遍历对比;
2.Vue 的compile 阶段的optimize标记了static 点,可以减少 differ 次数,而且是采用双向遍历方法;