2022面试总结

81 阅读23分钟

HTML

HTML

DOCTYPE 及其作用 类型

xml html5 html4

HTML5 新特性、语义化

css

position 区别

盒模型

标准盒模型:width 指content
怪异盒模型:width = content + padding + border

总宽度:
标准:总宽度=width+margin(左右)+padding(左右)+ border(左右);
怪异:总宽度=width+margin(左右)(就是说width已经包含了padding和border值)

css属性控制模型

box-sizing: content-box; //设置标准模型

box-sizing: border-box;//设置IE模型 

BFC

是一个独立的渲染区域,隔离BFC内的元素和外部元素

触发条件:

    1. overflow 不等于 visible
    1. position 等于 absolute/ fixed
    1. float 不等于 none
    1. display的值为inline-block、table-cell、table-caption

作用:

1.防止margin发生重叠

2.实现2栏布局,消除文字环绕

3.清除浮动,防止 元素塌陷

【解释1:正常情况下,2个相邻元素都设置margin,则只保留其中较大的margin值】

特性:

【对外部元素来讲】

1.不与浮动元素重合

2.BFC内部布局和外部无关

【对内部元素来讲】

1.BFC内部元素垂直排列

2.BFC内部元素左侧紧贴外部元素的左侧

3.BFC元素的高度包括浮动元素的高度

css选择器

  • id选择器(#myid)

  • 类选择器(.myclass)

  • 属性选择器(a[rel="external"])

  • 伪类选择器(a:hover, li:nth-child)

  • 标签选择器(div, h1,p)

  • 相邻选择器(h1 + p)

  • 子选择器(ul > li)

  • 后代选择器(li a)

  • 通配符选择器(*)

选择器优先级

样式的优先级:

!important > 内联样式表 > id > class > 元素选择器

原理:专用性越高优先级大,因为id唯一,所以id选择器只能选择一个元素,类选择器可以选择多个选择器

到具体的计算层面来说 是由分数决定的

千位: 存在内联样式,则千位上加1

百位: ID选择器出现的次数

十位: 类选择器 属性选择器 出现的总次数

个位: 元素选择器 伪类出现的总次数

注意: 通用选择器 (*), 复合选择器 (+, >, ~, ' ') 和否定伪类 (:not) 在专用性中无影响。

#aa .bb p---分数:0111 
.cc .dd p---分数:0021

伪类选择器和伪元素区别

伪类是选择器,选择 处于特定状态下 的元素, 一个冒号(:)

伪元素是添加了一个全新的 HTML 元素,而不是将类应用于现有元素。伪元素以双冒号开头::

常用伪类:

:active :当用户激活(例如点击)一个元素时匹配。

:focus : 当元素具有焦点时匹配。

:hover : 当用户将鼠标悬停在元素上时匹配。

:checked: 匹配处于选中状态的单选按钮或复选框。

:visited :匹配访问过的链接。

:link : 匹配未访问的链接。

伪元素:

84b749290d704601ac3359c187f09ec.png

水平居中

文字/行内元素居中:

text-align:center

块级元素居中

margin:0 auto---使盒模型居中

position:absolute + margin-left:-0.5*宽度

.son{ position:absolute; width:100px; left:50%; margin-left:-50px; }

flex中的:justify-content:center

grid中:justify-items:center

垂直居中

单行文本

line-height等于父元素高度

块级元素

table-cell + vertical-align

position:absolute +transform/margin-top

flex:align-item

grid中的:align-items 属性

水平垂直居中

需要知道子元素宽高

父relative + 子absolute + 负margin

<style type="text/css">
  .out{
    position: relative;
    width: 300px;
    height: 300px;
    background: red;
  }

  .inner{
    position: absolute;
    width: 100px;
    height: 100px;
    background: yellow;
    left: 50%;
    top: 50%;
    margin-left: -50px;
    margin-top: -50px;
  }
</style>

父relative + 子absolute + auto margin

/*原理:auto 平分剩余空间,当left top right bottom都为0  则填充整个父元素的所有可用空间,那么auto,会平均分配剩余的空间*/
<style type="text/css">
  .out{
    position: relative;
    width: 300px;
    height: 300px;
    background: red;
  }

  .inner{
    position: absolute;
    width: 100px;
    height: 100px;
    background: yellow;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    margin: auto;
  }
</style>

参考:www.cnblogs.com/sunhang32/p…

子relative + 父absolute + calc

<style type="text/css">
  .out{
    position: relative;
    width: 300px;
    height: 300px;
    background: red;
  }

  .inner{
    position: absolute;
    width: 100px;
    height: 100px;
    background: yellow;
    left: calc(50% - 50px);
    top: calc(50% - 50px);
  }
</style>

不需要知道子元素宽高

absolute+transform

/*transform:对元素进行旋转、缩放、移动或倾斜 translate对元素进行移动*/
<style type="text/css">
  .out{
    position: relative;
    width: 300px;
    height: 300px;
    background: red;
  }

  .inner{
    position: absolute;
    background: yellow;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
  }
</style>

table-cell+vertical-align +text-align

//table-cell 将父元素当做表格中单元格 子元素当做单元格中内容
// vertical-align : 设置单元格框中的单元格内容的对齐方式
 <style type="text/css">
        .out{
          display: table-cell;
          width: 300px;
          height: 300px;
          text-align: center;
          vertical-align: middle;
          background: red;
        }
      
        .inner{
          display: inline-block;
          background: yellow;
          width: 100px;
          height: 100px;
        }
      </style>

flex

<style type="text/css">
  .out{
    display: flex;
    justify-content: center;
    align-items: center;
    width: 300px;
    height: 300px;
    background: red;
  }

  .inner{
    background: yellow;
    width: 100px;
    height: 100px;
  }
</style>

grid

//方法一:父元素指定子元素的对齐方式
<style type="text/css">
  .out{
    display: grid;
    align-content: center;
    justify-content: center;
    width: 300px;
    height: 300px;
    background: red;
  }

  .inner{
    background: yellow;
    width: 100px;
    height: 100px;
  }
</style>

//方法二:子元素自己指定自己的对齐方式
<style type="text/css">
  .out{
    display: grid;
    width: 300px;
    height: 300px;
    background: red;
  }

  .inner{
    background: yellow;
    width: 100px;
    height: 100px;
    align-self: center;
    justify-self: center;
  }
</style>

flex 布局属性

flex(Flexible Box)弹性布局 容器的属性:

  • flex-direction:决定主轴的方向(即项目的排列方向)flex-direction: row | row-reverse | column | column-reverse;
  • flex-wrap:决定换行规则 flex-wrap: nowrap | wrap | wrap-reverse;
  • flex-flow: flex-direction属性和flex-wrap属性的简写形式
  • justify-content:决定 项目在主轴上的对齐方式
  • align-items:决定 项目在交叉轴上 的对齐方式
  • align-content:多根轴线的对齐方式(当多行时,将所有flex子项当做整体)。如果项目只有一根轴线,该属性不起作用。

项目的属性(元素的属性):

  • order 属性:定义项目的排列顺序,顺序越小,排列越靠前,默认为 0
  • flex-grow 属性:定义项目的放大比例,如果有剩余空间,决定项目的放大的比例。默认为0,即如果存在剩余空间,也不放大。
  • flex-shrink 属性:定义了项目的缩小比例,决定当空间不足时的缩小比例,默认为1,即如果空间不足,该项目将缩小
  • flex-basis 属性:定义项目的占用空间。浏览器根据这个属性,计算主轴是否有多余空间,这个属性在flex-grow 、flex-shrink之前。它的默认值为auto,即项目的本来大小。
  • flex:是 flex-grow 和 flex-shrink、flex-basis 的简写,默认值为 0 1 auto。
  • align-self:允许单个项目与其他项目不一样的对齐方式,可覆盖align-items属性。

移动端适配(rem、em、vw)

rem

rem是什么

rem: 相对于根元素(html)的 font-size 大小来计算,主要作用是在不同尺寸的设备上,通过修改根节点的font-size大小,实现等比例缩放

常用: 默认情况下,html元素的font-size为16px,所以:1rem = 16px 为了计算方便(rem:px = 1:10),将html的font-size设为10px,也就是16* 62.5% : html{ font-size: 62.5% }

不直接设置成 html{ font-size: 10px }的原因:兼容性和未来发展趋势的综合考虑,px这个单位的含义已经越来越混乱,几乎无法评估以后的设备是会一直像现在这样对网页上的px做兼容处理,还是让px回归“像素”的本意,但62.5%代表默认字体尺寸的62.5%这个含义基本不会有混乱。

rem使用

1、在窗口变化的时候 设置根元素的font-size大小

根据设计稿的总宽度 和 默认根元素的font-size大小 和窗口变化时的宽度 等比计算此时根元素的font-size大小

/* 这里我们利用了一个自执行函数 */
(function(){ 
    change(); 
    function change(){ 
        /* 这里的html字体大小利用了一个简单书序公式(十字相乘),
        * 当我们默认设置以屏幕320px位基准此时的字体大小为20px(320 20px 是设计稿的总宽度 和 默认字体大小),
        * 那么浏览器窗口大小改变的时候新的html的fontSize(clientWidth 新的值)就是clientWidth*20/320 */    
        document.documentElement.style.fontSize =  document.documentElement.clientWidth*20/320 + 'px';
        }
        /* 监听窗口大小发生改变时 */ 
        window.addEventListener('resize',change,false);})
();
2、自动转换成rem
CSS处理器

写个函数将px转成rem:给定font-size 基准值,然后根据这个值 计算对应px的rem
1、sass预处理器

// 定义一个变量和一个mixin
$baseFontSize: 16; //默认基准font-size
@mixin px2rem($name, $px){
  #{$name}: $px / $baseFontSize * 1rem;
}

// 使用示例:
.container {
  @include px2rem(height, 240);
}

// scss翻译结果:
.container {
  height: 3.2rem;
}

2、less

// 定义一个变量, 注:移动端基于视觉稿横屏尺寸/100得出基准font-size
@baseFontSize: 16;
// rem转换函数
.px2rem(@name, @px) {
    @{name}: @px / @baseFontSize * 1rem;
}

// 使用示例
.container {
    .px2rem(font-size, 14);
}

// less翻译结果
.container {
    font-size: 0.85rem;
}
   

常用的适配方案:

lib-flexible + px2rem-loader

lib-flexible的作用:将设计稿中的设备独立像素来体现到逻辑像素的开发过程中,也就是自动设置根元素font-size。

  1,会自动在html的head中添加一个meta name="viewport"的标签,

  2,同时会自动设置html的font-size为屏幕宽度除以10,也就是1rem等于html根节点的font-size。

amfe-flexible + postcss-pxtorem

amfe-flexible:通过js检测屏幕宽度改变html里面的font-szie的,从而来达到自适应的视觉效果

postcss-loader

参考:
juejin.cn/post/684490… juejin.cn/post/684490… juejin.cn/post/684490…

rem缺点

在响应式布局中,必须通过js来动态控制根元素font-size的大小。 也就是说css样式和js代码有一定的耦合性。且必须将改变font-size的代码放在css样式之前。

vw vh 原理 使用

vw vh 是什么

1vw = 视图宽度 * 1%

  • vh:视窗高度的百分比
  • vmin:当前 vw 和 vh 中较小的一个值
  • vmax:当前 vw 和 vh 中较大的一个值

vw 使用 将px转换成 vw

// iPhone 6尺寸作为设计稿基准 sass $vw_base: 375;//屏幕宽度 单位:px @function vw($px) {
    @return ($px / $vm_base) * 100vw;
 }
 1vw = 屏幕宽度 * 1% 所以 屏幕宽度 = 100vw 将px 转为 vw $px 等于多少vw? 
 $px / 屏幕宽度(占比) * 100vw

vw 缺点

将屏幕等比缩放,缩放过大时会失真,它也随着视窗过大或者过小,失去了最大最小宽度的限制

vm + rem(???不确定 不要参考这个)

使用:

  1. 给根元素的字体大小设置随着视窗变化而变化的 vw 单位,这样就可以实现动态改变其大小
  2. 其他元素的文本字号大小、布局高宽、间距、留白都使用 rem 单位
  3. 限制根元素字体大小的最大最小值,配合 body 加上最大宽度和最小宽度,实现布局宽度的最大最小限制
// rem 单位换算: 
//定为 75px 只是方便运算,750px-75px、640-64px、1080px-108px,如此类推 $vw_fontsize: 75; 
// iPhone 6尺寸的根元素大小基准值
@function rem($px) { 
@return ($px / $vw_fontsize ) * 1rem; 
} 
// 根元素大小使用 vw 单位 
$vw_design: 750; 
html { font-size: ($vw_fontsize / ($vw_design / 2)) * 100vw; 
// 同时,通过Media Queries 限制根元素最大最小值 
@media screen and (max-width: 320px) { font-size: 64px; } 
@media screen and (min-width: 540px) { font-size: 108px; } }
// body 也增加最大最小宽度限制,避免默认100%宽度的 block 元素跟随 body 而过大过小 
body { max-width: 540px; min-width: 320px; }

JS

数据类型

基本数据类型:str number Boolean null undefined symol

复杂 / 引用数据类型:object

不同

基本数据类型存储在栈中

引用数据类型的值存储在堆中,地址存储在栈中

当复制引用类型时复制的是地址 指向的是同一块内存空间

数据类型判断

判断是不是array的方式

作用域 作用域链

作用域:确定当前执行代码对变量变量,函数和对象的访问权限。
作用域就是一个独立的地盘,让变量不会外泄、暴露出去,这样就可以隔离变量,不同作用域下同名变量不会有冲突。

作用域链:就是从当前作用域开始一层一层向上寻找某个变量,直到找到全局作用域还是没找到,就放弃。这种一层一层的关系,就是作用域链。

作用域分类

全局作用域:

  1. 没在函数内部定义的变量具有全局性
  2. 未定义就直接赋值的变量

块级作用域: 在{}使用let const 声明的变量是块级作用域的变量;if for while 和函数的{}

函数作用域: 在哪个函数内被声明,那么该函数{} 就是 变量、函数的作用域

静态作用域

js采用的是静态作用域 所以函数的作用域在定义时确定 github.com/mqyqingfeng…

执行上下文

当执行一段可执行代码时,会创建执行上下文(是评估和执行 JavaScript 代码的环境的抽象概念)

执行上下文包含: 变量对象 作用域链 this

变量对象:

变量对象会包括在这个上下文中定义的所有变量和函数

  1. 函数的所有形参 (如果是函数上下文)
  • 由名称和对应值组成的一个变量对象的属性被创建
  • 没有实参,属性值设为 undefined
  1. 函数声明
  • 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
  • 如果变量对象已经存在相同名称的属性,则完全替换这个属性
  1. 变量声明
  • 由名称和对应值(undefined)组成一个变量对象的属性被创建;
  • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

4、arguments

作用域链形成过程:

函数有一个内部属性 [[scope]],当函数创建的时候,[[scope]] 就会保存所有父变量对象 当函数激活时形成作用域链: 第一步:复制函数[[scope]]属性创建作用域链 第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明 第三步:将活动对象压入 作用域链顶端

this

this 指代函数当前的运行环境
(this是执行上下文中的一个属性,所以是在函数调用的时候才能确定的)

JavaScript 的 this 原理

根据函数调用不同 this指向不同:

作为函数调用-----默认绑定

概念:不使用其他3种方式绑定this的方式,成为默认绑定

this指向:

默认绑定的this,在非严格模式下指向全局对象,在严格模式下指向undefined

例:在浏览器上运行 默认绑定的this指向全局对象,在node环境下运行,this执行undefined

作为方法调用----隐式绑定

概念:函数的调用是在某个对象上触发的。通常形式:obj.fn()

this指向:以隐式绑定的方式调用函数,函数的上下文就是对象的上下文,this指向对象

//对象属性链中只有最后一层会影响到调用位置。
function sayHi(){ 
    console.log('Hello,', this.name);
}
var person2 = { name: 'Christina', sayHi: sayHi } 
var person1 = { name: 'YvetteLau', friend: person2 } 
person1.friend.sayHi(); //Christina 
//分析执行流程: 调用对象person1中friend属性,此属性对应对象person2 
//然后调用对象person2中sayHi()属性,此时sayHi函数绑定在对象person2上

隐式绑定 丢失this的情况

function sayHi(){ console.log('Hello,', this.name); } 
var person = { name: 'YvetteLau', sayHi: sayHi } 
var name = 'Wiliam'; 
var Hi = person.sayHi; 
Hi();

分析执行情况:

person.sayHi---调用person的sayHi属性,此属性对应函数sayHi。 
则person.sayHi = function sayHi(){ console.log('Hello,', this.name); }。 
也就是变量Hi对应sayHi函数,当执行到Hi()时,执行的其实是sayHi()函数, 
此时是默认绑定,thiswindow

总结:判断this的指向,只需看最后调用时,是以哪种方式调用的即可,看是obj.fn还是obj.fn()是否被直接调用

作为函数调用其实是 widow.fn() 此时对象是window;所以作为函数调用 和 作为方法调用 其实本质是一样的

作为构造函数调用------new 绑定 this指向:this指向实例

显示绑定/硬绑定--call()、 apply()

概念:通过call apply bind硬性更改函数的this指向

绑定优先级

new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

参考文章 按顺序读

闭包

什么是闭包

有权访问 另 一个函数作用域中变量的 函数,通常是在嵌套函数中实现的 JavaScript 深入之闭包

这么回答更接近底层: 在某个内部函数的执行上下文创建时,会将父级函数的活动对象加到内部函数的 [[scope]] 中,形成作用域链,所以即使父级函数的执行上下文销毁(即执行上下文栈弹出父级函数的执行上下文),但是因为其活动对象还是实际存储在内存中可被内部函数访问到的,从而实现了闭包。


作者:vortesnail
链接:juejin.cn/post/706158… 来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

闭包常见应用场景

在定时器、事件监听器、Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中;只要使用了 回调函数 就是使用闭包

//比如在定时器中,引擎会调用这个函数,在例子中就是 内部的 timer 函数,而词法作用域在这个过程中保持完整;timer函数的调用 不是在 setTimeout中而是在 setTimeou外层的 作用域中。
setTimeout( function timer() { console.log( i ); }, i*1000 );

闭包的用途

  1. 私有化变量
  2. 模仿块级作用域(ES5中没有块级作用域)
  3. 实现JS的模块
// 1. 私有化变量
//name是私有变量 
function Person(){ 
    var name = 'cxk'; 
    this.getName = function(){ 
        return name;
    } 
    this.setName = function(value){ 
       name = value; 
   } 
} 
const cxk = new Person() 
cxk.getName() //cxk 
cxk.setName('jntm') 
cxk.getName() //jntm 
console.log(name) //name is not defined
//2. 模仿块级作用域(ES5中没有块级作用域)
var arr = ["a", "b", "c", "d", "e"]; 
for (var i = 0; i < arr.length; i++) {
    (function(j) {
        var item = arr[j];
        setTimeout(function() { 
            console.log(item); 
        }, 1000 * (i + 1)); 
    })(i); 
}
// 3. 实现JS的模块
// a.js
var another = [1, 2, 3];
function doSomething() { 
    console.log( something ); 
} 
function doAnother() { 
    console.log( another.join( " ! " ) ); 
} 
return { 
    doSomething: doSomething, 
    doAnother: doAnother }; 
} 
//b.js
import CoolModule from a.js
var foo = CoolModule(); 
foo.doSomething(); // cool 闭包 
foo.doAnother(); // 1 ! 2 ! 3

闭包缺点:

1、闭包函数占据的内存比一般函数大 外层函数每次运行的时候,就会产生一个新的闭包,这个闭包中携带着外层函数中的变量。所以不能滥用闭包,否则会造成网页的性能问题

2、内存泄漏 内部变量不释放,并且可以被外部访问的情况,其实就是内存泄漏的表现

箭头函数和普通函数区别

1、this指向 2、箭头函数没有 arguments reset 3、箭头函数不new一个对象 3.1 箭头函数没有prototype属性 3.2 箭头函数不能使用apply call

function newFunc(father, ...rest) {
  var result = {};
  result.__proto__ = father.prototype;
  var result2 = father.apply(result, rest);
  if (
    (typeof result2 === 'object' || typeof result2 === 'function') &&
    result2 !== null
  ) {
    return result2;
  }
  return result;
}

箭头函数优点: 1、写法简单 2、隐式返回

原型与原型链

原型:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性。
对象proto属性和构造函数的prototype的属性 都指向原型对象\

原型链:每个对象通过 __proto__ (读音:proto) 属性指向原型对象,并从中继承方法和属性,同时原型对象也有自己的原型,这样一层层的,最终指向object.prototype._proto_属性也就是null,这种关系称为原型链。
(还可以提下原型对象的constructor属性指向构造函数)

image.png

github.com/mqyqingfeng…

对象继承常用写法

借用构造函数

子类借用父类构造函数,在子类的构造函数中调用父类的构造函数

**缺点:**子类只能继承父构造函数中定义的实例和方法 不能继承父类原型对象中的内容

(只是在子类中执行了父类中代码 但是子类原型对象还是指向son.prototype 无法继承父类原型上属性和方法)

function SuperType() {  
    this.colors = ["red", "blue", "green"]; 
} 
function SubType() { 
    // 继承SuperType  SuperType.call(this); 
} 
let instance1 = new SubType(); 
instance1.colors.push("black"); 
console.log(instance1.colors); // "red,blue,green,bla 
let instance2 = new SubType(); 
console.log(instance2.colors); // "red,blue,green"

寄生式组合继承(常用)

通过借用构造函数继承属性,但使用 混合式原型链 继承方法

function inheritPrototype(subType, superType) {

    let prototype = object(superType.prototype); // 创建对象

    prototype.constructor = subType; // 增强对象

    subType.prototype = prototype; // 赋值对象

}
function SuperType(name) {

    this.name = name;

    this.colors = ["red", "blue", "green"];

}

SuperType.prototype.sayName = function() {

    console.log(this.name);

};

function SubType(name, age) {

    SuperType.call(this, name);

    this.age = age;

}

inheritPrototype(SubType, SuperType);

    SubType.prototype.sayAge = function() {

    console.log(this.age);

};

es6 class继承

class继承使用extends 和super关键字。

解释:因为子类没有自己的this对象,所以必须在子类constructor构造函数中使用super()生成父类的this对象,否则会报错。this对象中有父类的方法和属性。

注:在super()中()写上要继承父类的属性。

image.png

es6继承和es5的区别

es5:实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面 es6:先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this

new

1、首先创建了一个新的空对象
2、设置原型,将空对象的原型设置为函数的prototype对象。
3、让函数的this指向这个对象,执行构造函数的代码(为这个新对象添加属性)
4、判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

//fn--构造函数,...args参数 
function new(fn,...args){ 
    let obj = {} 
    obj.__proto__ = fn.prototype 
    let res = fn.call(obj,...args) 
    //将属性和方法挂载到obj中 并执行了fn函数 
    return res instanceof Object?res:obj 
}

事件循环

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

1.同步任务放入栈中,进入主线程,形成执行栈。

2.当异步任务的触发条件达成时,将异步任务放到任务队列中((微任务放到微任务队列,宏任务放到宏任务队列))

3.当执行栈中为空时,会检查微任务队列,如果有任务,就全部执行,如果没有就执行宏任务;

4.上述过程不断重复,也就是常说的事件循环

什么事宏任务、微任务

异步队列又分为宏任务队列和微任务队列

  • 宏任务(macrotask)
    ajax、setTimeout、setInterval、setTmmediate(只兼容ie)、script、

  • 微任务(microtask)
    语言本身提供的,比如promise.then

宏任务和渲染线程关系

宏任务执行完后--渲染--宏任务--渲染
宏任务--微任务--渲染--宏任务--微任务--渲染
参考:juejin.im/post/59e85e…

js垃圾回收机制

浏览器的Javascript具有 自动垃圾回收机制(GC:Garbage Collecation),垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。

主要的标记策略

  1. 标记清除:当变量进入执行环境时,被标记为“进入环境”,当变量离开执行环境时,会被标记为“离开环境”。 垃圾回收器会销毁那些 带标记的值 并回收它们所占用的内存空间

  2. 引用计数:记录每个资源被引用的次数。当引用数为0时,就表明这个值不再用到了,因此可以收回其内存

优劣: 引用计数会产生循环引用的问题
解决方法:把变量设置为null 切断变量 与其 之前引用值之间的关系
参考:《JavaScript高级程序设计(第4版)》

内存泄漏

内存泄露是指你用不到(访问不到)的变量,依然占居着内存空间,不能被再次利用起来。
在 JS 中,常见的内存泄露主要有 4 种:全局变量(没有使用var let const声明的变量)、闭包、DOM 元素的引用、定时器

全局变量:

function setName() {

    name = 'Jake';

}
//解释器会把变量name 当作window 的属性来创建(相当于window.name = 'Jake' )。
//可想而知,在window 对象上创建的属性,只要window 本身不被清理就不会消失

只要在变量声明前头加上var 、let 或const 关键字即可,这样变量就会在函数执行完毕后离开作用域。

闭包

//调用outer() 会导致分配给name 的内存被泄漏。以上代码执行后创

建了一个内部闭包,只要返回的函数存在就不能清理name ,因为闭

包一直在引用着它。
let outer = function() {

    let name = 'Jake';

    return function() {

        return name;

    };

};

定时器

//只要定时器一直运行,回调函数中引用的name 就会一直占用内存。

//垃圾回收程序当然知道这一点,因而就不会清理外部变量。
let name = 'Jake';

setInterval(() => {

    console.log(name);

}, 100);

promise

promise 状态有几种?

Promise有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)

promise 方法?

promise.finally:方法不管Promise状态如何都会执行,该方法的回调函数不接受任何参数

promise.then:方法返回一个新的Promise实例,并接收两个参onResolved(fulfilled状态的回调);onRejected(rejected状态的回调,该参数可选) promise.catch:方法返回一个新的Promise实例

Promise.all():方法用于将多个 Promise 实例,包装成一个新的 Promise 实例, 参数是 一个由Promise对象组成的数组,也可以不是数组,但必须具有Iterator接口,且返回的每个成员都是Promise实例

结果有2种状态:

1、只有参数中实例状态都是 fulfilled,整体的状态才会变成fulfilled,此时所有实例的返回值组成一个数组,传递给 Promise.all()方法返回的新的实例 的回调函数。

2、参数中有一个实例被rejected,就会触发Promise.all()方法返回的新的实例 的 catch方法

注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()catch方法。因为rejected时会触发自己的catch方法,catch方法返回一个新的promise实例,此时指向的实际上是这个实例。该实例执行完catch方法后,也会变成resolved

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。参数和promise.all一样

只要参数中实例之中有一个改变状态,整体的状态就跟着改变。将率先改变状态的 Promise 实例的返回值,传递给整体的回调函数。

Promise.any()方法,只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。

Promise.allSettled()方法,接受一个数组作为参数,数组的每个成员都是一个 Promise 对象,并返回一个新的 Promise 对象,只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled还是rejected),返回的 Promise 对象才会发生状态变更。一旦发生状态变更,状态总是fulfilled。 它的回调函数接收到的参数是数组。该数组的每个成员对象,对应异步操作的结果

Promise.resolve()将现有对象转为Promise对象。根据参数不同 分为以下情况:

  1. 参数是一个 Promise 实例,Promise.resolve()将不做任何处理
  2. 参数是thenable对象(即具有then方法),Promise.resolve()将该对象转为Promise对象并立即执行then方法
  3. 参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的Promise对象,状态为fulfilled,其参数将会作为then方法中onResolved回调函数的参数
  4. 不带参数,会直接返回一个fulfilled状态的 Promise 对象

Promise.reject()同样返回一个新的Promise对象,状态为rejected,无论传入任何参数都将作为reject()的参数

promise 优劣?

优: 解决了回调地狱的问题,将异步操作以同步操作的流程表达出来
Promise 带来的额外好处是包含了更好的错误处理方式(包含了异常处理)

劣:

  1. 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  2. 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  3. 当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

实现promise

function myPromise(constructor){ let self=this;
  self.status="pending" //定义状态改变前的初始状态 
  self.value=undefined;//定义状态为resolved的时候的状态 
  self.reason=undefined;//定义状态为rejected的时候的状态 
  function resolve(value){
    //两个==="pending",保证了了状态的改变是不不可逆的 
    if(self.status==="pending"){
      self.value=value;
      self.status="resolved"; 
    }
  }
  function reject(reason){
     //两个==="pending",保证了了状态的改变是不不可逆的
     if(self.status==="pending"){
        self.reason=reason;
        self.status="rejected"; 
      }
  }
  //捕获构造异常 
  try{
      constructor(resolve,reject);
  }catch(e){
    reject(e);
    } 
}
myPromise.prototype.then=function(onFullfilled,onRejected){ 
  let self=this;
  switch(self.status){
    case "resolved": onFullfilled(self.value); break;
    case "rejected": onRejected(self.reason); break;
    default: 
  }
}

// 测试
var p=new myPromise(function(resolve,reject){resolve(1)}); 
p.then(function(x){console.log(x)})
//输出1

深拷贝、浅拷贝

浅拷贝:只复制第一层对象,当对象的属性是引用类型时,复制的是其引用 深拷贝:完全复制值,深拷贝后的对象与原来的对象是完全隔离的,互不影响

实现浅拷贝:

  1. 扩展运算符.... let b = {...a}
  2. assign let b = Object.assign({}, a)
  3. 数组的concat

实现深拷贝:
递归实现,对于 非基本类型的变量,递归至基本类型变量后 进行复制
具体参考: github.com/yygmind/blo…

es6

let、const 以及 var 的区别是什么

let,const,在块级作用域中用

let,const会变量提升 var不会

let,const不能重复声明,和遗漏声明变量,var可以

let,const有暂时性死区

const声明的常量,一旦声明,常量的值就不能改变(如果是对象,则对象的引用地址不变)

数组相关

数组方法.png

pop shift push unshift 返回什么

pop、shift:删除数组最后一个/第一个值,并返回删除的值
push、unshift:将一个多个值添加到数组末尾/开头,并返回改数组新长度

fill 、reverse、sort 在原数组上修改,并返回原数组;其它大部分都是返回新的数组

es6数组新增的方法有哪些

Array.from:将类数组对象、具有Iterator(遍历器)接口的对象转换为数组

Array.of:将一组值,转换为数组,如果没有参数,就返回一个空数组

Array.copyWithin:将指定位置的成员复制到其他位置(会覆盖原有成员),并返回当前数组

Array.fill:用给定值,填充一个数组。['a', 'b', 'c'].fill(7, 1, 2) // ['a', 7, 'c']

注意,如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。

Array.flat(),Array.flatMap(): 拉平数组,并返回新数组。

flat默认拉平一层,想要拉平多层 可以在flat(num) num 决定拉平的层数 不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数flat(Infinity)

flatMap: 对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。

Array.at: 返回对应位置的成员,支持负索引

Array.sort():排序

Array.find:返回第一个符合条件的数组成员,没有符合条件的返回undefined

Array.findIndex:返回第一个符合条件的数组成员的索引,没有符合条件的返回-1

Array.includes: 判断数组是否包含给定的值,返回布尔值

entries()keys()values() :用于遍历数组,keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。 都返回一个遍历器对象

Array.from和扩展运算符转换数组的区别:

扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,就无法转换。
Array.from方法还支持类似数组的对象。(所谓类似数组的对象,本质特征只有一点,即必须有length属性。)因此,任何有length属性的对象,都可以通过Array.from方法转为数组,而此时扩展运算符就无法转换

find、findeIndex 和indexOf区别:

find、findeIndex 都可以发现NaN,弥补了数组的indexOf方法的不足

[NaN].indexOf(NaN)
// -1

[NaN].findIndex(y => Object.is(NaN, y))
// 0

Array.includes 和 indexOf 区别

indexOf 一不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。 二是,它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判

用例代码:

// Array.from:
let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
};

// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']

// ES6的写法
用法:
还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组
let arr2 = Array.from(arrayLike,s => s+1); // ['a1', 'b1', 'c1']

//Array.of 
Array.of(1) // [1]
Array.of() // []

//Array.copyWithin
// 将3号位复制到0号位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4)

//flatMap
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]



map filter 区别

相同点:

filter 和 map 都是对数组的操作,均返回一个新的数组

不同点:

filter 是对原数组的过滤 满足条件的留下;map则是对原数组的加工,映射成新数组

filter 如果没有任何数组元素通过测试,则返回空数组。

map filter 和 for循环区别

  1. map filter 是对原数组中数据 修改,for循环什么的 是要根据数组元素做一些别的操作
  2. map filter 有返回值 for循环没有

for...of、 for...in 和 forEach、map 的区别?

for of:遍历具有Iterator(遍历器) 接口的数据,可以中断循环(breakcontinuereturn

for in:遍历对象自身的和继承的可枚举的属性, 不能直接获取属性值。可以中断循环(breakcontinue

forEach:只能遍历数组,不能中断,没有返回值(或认为返回值是undefined)

map: 只能遍历数组,不能中断,返回值是修改后的数组。

引申问题:

具有Iterator(遍历器)数据结构主要包括:

数组、类数组(argums、domList)、字符串、MapSet、Generator 对象

对象是否可以用for of

注意对象是不能使用for...of遍历的,必须部署了 Iterator 接口后才能使用

如何使用 for of 遍历对象

1、使用Object.keys方法将对象的键名生成一个数组,然后遍历这个数组。

for (var key of Object.keys(someObject)) {
  console.log(key + ': ' + someObject[key]);
}

2、给对象增加Symbol.iterator属性

obj[Symbol.iterator] = arr[Symbol.iterator].bind(arr);

示例代码:

//遍历对象时 i是key
var obj2 = {name:'ann',age:18}
for (let i in obj2){
    console.log(i)//name age
}
//遍历 数组时 i是下标
var arr = ['a', 'b', 'c', 'd'];    
for (let i in arr){
    console.log(i)//0123
}

iterator 接口是什么

一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。

判断否是数组

  1. Array.isArray
  2. instanceof Array 判断,如果返回true, 说明是数组
  3. Object.prototype.toString.call(arr)--[object Array]
  4. 通过 constructor 来判断,如果是数组,那么 arr.constructor === Array. (不准确,因为我们可以指定 obj.constructor = Array)

Iterator

Iterator 的遍历过程是这样的。

(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。

(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。

(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

vue

v-for v-if 优先级

1、v-for优先于v-if被解析 2、如果同时出现,每次渲染都会先执行循环再判断条件,无论如何循环都不可避免,浪费了性能 解决方法法: 1、外层嵌套template,在这一层进行v-if判断,然后在内部进行v-for循环 2、如果条件出现在循环内部,可通过计算属性提前过滤掉那些不需要显示的项

keep-alive

computed 和 watch 的区别和运用的场景?

computed: 计算属性。依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;

watch: 监听数据的变化。更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;

运用场景:

1)当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;

2)当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

链接:www.jianshu.com/p/3507b078f…

computed源码实现

为什么data是个函数

每次使用组件时都会对组件进行实例化操作,并且调用data函数返回一个对象作为组件的数据源。这样可以保证多个组件间数据互不影响。

如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数。

链接:www.jianshu.com/p/3507b078f…

组件间通信

父子组件通信:props emit;ref;emit; ref; parent / children;provide/inject;children; provide / inject ; attrs / $listeners

vue3新增:expose / ref

兄弟组件通信:vuex

跨级通信:Vuex;provide / inject 、attrs/attrs / listeners

ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例,可以通过实例直接调用组件的方法或访问数据

$children / $parent(双向都可以该值,父改子,子改父)

provide/ inject (不只父子通信 还可以 祖父和孙子 更深的也行) 默认情况下,provide/inject 绑定并不是响应式的,但是可以传入响应式 或者让computed监听数据

$attrs/$listeners

$attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。

$listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件

结合 inheritAttrs: false,开发者可以将这些 attribute 和监听器应用到根元素之外的其它元素:

inheritAttrs:

默认情况下vue会把父作用域的不被认作 props 的特性绑定 且作为 普通的 HTML 特性 应用在子组件的根元素上。

参考:www.jianshu.com/p/ce8ca875c…

v-for中 key的作用

key的作用主要是为了高效的更新虚拟DOM; 在新旧虚拟DOM对比的过程中,通过key可以精准判断两个节点是否是同一个,从而避免频繁更新不同元素,如果没有key vue采用就地复用。

key为什么不能是随机数、索引

使用 v-for 渲染的元素列表时,它默认使用“就地更新”的策略,如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。 如果不使用key的话 对比新旧虚拟DOM节点的时候 采用双端比较法,这样效率不高,浪费性能
如果使用索引的话,key 的绑定关系有变化,所以被重新渲染,这会影响性能 还会产生一些隐蔽bug
如果是随机数的话,新旧节点的key不同,旧节点会被全部删掉,新节点重新创建 影响性能

juejin.cn/post/684490… www.cnblogs.com/wuqun/p/132…

mvvm

MVVM

M: 数据层,保存每个页面中的数据【通过 Ajax/fetch 等 API 完成客户端和服务端业务 Model 的同步】

V: 视图层,HTML结构

VM:数据层和视图层之间的调度者,实现数据的双向绑定(视图层修改数据,得通过vm层让m层修改数据,视图层想要获取数据,也得通过VM层从数据层获取数据)

MVC和MVVM的区别:

mvvm实现了数据的双向的绑定

具体解释:

View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。

ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理

数据双向绑定原理

通过监听器Observer、解析器Compile、和Watcher实现双向绑定。

1、数据监听器Observer:通过object.defineProperty对数据对象的所有属性进行监听,在getter中收集依赖,setter中派发依赖,通知watcher

2、指令解析器Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图

3、Watcher:作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,当收到通知后会触发watcher 中更新函数(会对新、旧Vnode 通过Diff算法进行比较,然后找到新旧VNode 不同的节点,最后将这些不同的节点更新到真实的DOM上 并在页面中渲染)

clipboard.png

响应式数据原理

根据数据类型做不同的处理。
1.对象: 通过Object.defineProperty 监听数据属性的 get 来进行数据依赖收集,再通过 set 来完成数据更新的派发;
2. 数组则通过重写数组方法来实现的。扩展它的 7 个变更⽅法,通过监听这些方法可以做到依赖收集和派发更新;( push/pop/shift/unshift/splice/reverse/sort ) 在Vue中修改数组的索引和长度是无法监控到的。需要通过以上7种变异方法修改数组才会触发数组对应的wacther进行更新。数组中如果是对象数据类型也会进行递归劫持。

Object.defineProperty 缺点 如何补救

1、不能监听属性的新增和删除

2、 不能监听数组
补救: 对象新增:

this.obj = Object.assign({},this.obj, { b: 1, e: 2 }) 
this.$set(this.obj,'f',0) 
this.obj = {...this.obj,...{ b: 3, e: 2 }}

对象删除:vue.$delete(obj, propertyName/index)

数组:使用set 或 splice

vue3为什么抛弃Object.defineProperty

1、Object.defineProperty 只能劫持对象的属性,而 Proxy 是直接代理对象。

Object.defineProperty只能劫持对象的属性,需要遍历属性,如果属性值也是对象的话,需要深度遍历 proxy则直接代理 不需要遍历

  1. Object.defineProperty 对新增属性需要手动进行 Observe。

由于 Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象,对其新增属性再使用 Object.defineProperty 进行劫持。

3、Proxy支持拦截操作比defineProperty 多

  • get(target, propKey, receiver):拦截对象属性的读取,比如 proxy.foo 和proxy['foo']。
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v 或 proxy['foo'] = v,返回一个布尔值。
  • has(target, propKey):拦截 propKey in proxy 的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截 delete proxy[propKey] 的操作,返回一个布尔值。
  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而 Object.keys() 的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截 Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截 Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截 Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截 Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。
  1. Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty 只能遍历对象属性直接修改

  2. Proxy 兼容性差

Vue.set 方法是如何实现的?

  • 如果目标是数组,直接使用数组的 splice 方法触发相应式;

  • 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 Object.defineProperty 对数据进行响应式处理

如何监听数组

对数组方法重写:push,pop,shift,unshift,splice,sort,reverse

Object.definePropert也能监控数组下标的变化,只是在 Vue 的实现中,从性能 / 体验的性价比考虑,放弃了这个特性但是只能监听 已有元素的下标,对于新增下标无法监听。

  1. 通过索引访问或设置对应元素的值时,可以触发 getter 和 setter 方法。
  2. 通过 push 或 unshift 会增加索引,对于新增加的属性,需要再手动初始化才能被 observe。
  3. 通过 pop 或 shift 删除元素,会删除并更新索引,也会触发 setter 和 getter 方法。

性价比不高原因 是数组只能监听 已有元素的下标 无法监听新增元素的下标:

const arrayProto = Array.prototype 
// 创建一个对象作为拦截器
const arrayMethods = Object.create(arrayProto) // 改变数组自身内容的7个方法 const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] 
/** * Intercept mutating methods and emit events */ console.log(arrayMethods)
methodsToPatch.forEach(
    function (method) { 
        const original = arrayProto[method] // 缓存原生方法 
        Object.defineProperty(arrayMethods, method, { 
            enumerable: false, 
            configurable: true, 
            writable: true, 
            value:function mutator(...args){ 
                const result = original.apply(this, args) return
                result
             } 
             }) }) 
let arr = [1, 2, 3]

vue中为什么需要虚拟DOM ?优劣

虚拟dom是来描述真实DOM的js对象。原生dom包含属性较多,直接操作dom可能导致页面重排,影响渲染性能。
VNode主要包括:标签名、数据、子节点、键值 Vue.js 中 Virtual DOM 是借鉴了一个开源库 snabbdom
虚拟DOM映射到真实的 DOM 实际上要经历 VNode 的 create、diff、patch 等过程。

diff算法

vue的diff算法是平级比较,不考虑跨级比较的情况。 内部采用深度递归的方式 + 双指针的方式进行比较。 Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。

nextTick在哪里使用?原理是?

nextTick的回调是在 下次 DOM更新循环 结束之后执行的延迟回调
在修改数据之后立即使用这个方法,获取更新后的DOM
(我们可以在数据变化之后立即使用Vue.nextTick(callback);这样回调函数会在DOM更新完成后被调用)

nextTick底层实现:

  1. 把回调函数放入callbacks等待执行
  2. 将执行函数放到微任务或者宏任务中
  3. 事件循环到了微任务或者宏任务,执行函数依次执行callbacks中的回调

clipboard.png 参考:

vue-js.com/learn-vue/i…

ustbhuangyi.github.io/vue-analysi…

vue更新dom机制:

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个事件队列,并缓冲在同一 事件循环 中发生的所有数据变更。

如果同一个 watcher 被多次触发,只会被推入到事件队列中一次。然后在下一个事件循环“tick”中重新渲染

(所以不会改完数据后立马更新dom,因为这次事件循环还没结束,在下一个事件循环中更新dom,为了在数据更新操作之后操作DOM,我们可以在数据变化之后立即使用Vue.nextTick(callback);这样回调函数会在DOM更新完成后被调用,就可以拿到最新的DOM元素了。)

参考:
cloud.tencent.com/developer/a… juejin.cn/post/700732…

new Vue发生了什么

选项合并

childrenchildren,refs,slotsslots,createElement等实例属性的方法初始化

自定义事件处理

数据响应式处理

生命周期钩子调用 (beforecreate created)

可能的挂载

生命周期函数(详细)

1、创建前/后:

1) beforeCreate阶段:vue实例的挂载元素el和数据对象data都为undefined,还未初始化。

说明:在当前阶段data、methods、computed以及watch上的数据和方法都不能被访问。

2) created阶段:vue实例的数据对象data有了,el还没有。

说明:可以做一些初始数据的获取,在当前阶段无法与Dom进行交互,如果非要想,可以通过vm.$nextTick来访问Dom。

2、载入前/后:

1)beforeMount阶段:vue实例的$el和data都初始化了,但还是挂载之前为虚拟的dom节点。

说明:当前阶段虚拟Dom已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated。

2)mounted阶段:vue实例挂载完成,data.message成功渲染。

说明:在当前阶段,真实的Dom挂载完毕,数据完成双向绑定,可以访问到Dom节点,使用$refs属性对Dom进行操作。

3、更新前/后

1)beforeUpdate阶段:响应式数据更新时调用,发生在虚拟DOM打补丁之前,适合在更新之前访问现有的DOM,比如手动移除已添加的事件监听器。

说明:可以在当前阶段进行更改数据,不会造成重渲染。

2) updated阶段:虚拟DOM重新渲染和打补丁之后调用,组成新的DOM已经更新,避免在这个钩子函数中操作数据,防止死循环。

说明:当前阶段组件Dom已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。

4、销毁前/后

1)beforeDestroy阶段:实例销毁前调用,实例还可以用,this能获取到实例,常用于销毁定时器,解绑事件。

说明:在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。

  1. destroyed阶段:实例销毁后调用,调用后所有事件监听器会被移除,所有的子实例都会被销毁。

说明:当前阶段组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。



作者:是妍妍吖
链接:www.jianshu.com/p/3507b078f… 来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

插槽

插槽相当于占位符,子组件提供给父组件使用的一个占位符,用 表示。父组件可以在这个占位符中填充任何模板代码,如 HTML、组件等,填充的内容会替换子组件的标签。
包含3种:匿名插槽、具名插槽、作用域插槽
作用域插槽:子组件提供给父组件的参数,该参数仅限于插槽中使用,父组件可根据子组件传过来的插槽数据来进行不同的方式展现和填充插槽内容。

//子组件 child.vue
 <slot v-bind:user="user">{{ user.firstname }}</slot>
 //父组件
 <child>
  <!-- 作用域插槽 接收子组件的插槽 prop值 并重命名为slotProps -->
      <template v-slot:default="slotProps">
        {{ slotProps.user.lastname }}
      </template>
 </child>

用途:子组件应用到多个父组件,只有一部分内容不一样时

vuex

介绍vuex

vuex是专为vue开发的状态管理模式,主要目的是方便管理组件间的共享状态,核心模块为State、 GetterMutation 、Action、 Module

  1. State:定义应用的状态数据
  2. Getter:一般用于从state中派生一些状态,类似Vue中的计算属性computed,只有原状态改变派生状态才会改变
  3. Mutation:改变状态的唯一方法,必须是同步函数
  4. Action:通过提交Mutation方式改变状态,可以包含异步操作
  5. Module:允许将单一的 Store 拆分为多个 store, 且同时保存在单一的状态树中

action和Mutation区别

  1. action 可以包含异步操作。mutation只能是同步操作。
  2. action 通过提交 mutation改变状态,不是直接变更状态。mutation可以直接变更状态。
  3. 提交方式不同,action 是用dispatc来提交。mutation是用commit来提交。

用过vuex模块(Module)吗?

当项目复杂的时候,所有的状态都几种到一个对象,很臃肿,所以将 store 分割成模块(module)。每个模块拥有自己的 state、mutations、actions、getters,甚至是嵌套子模块,从上至下进行同样方式的分割。

vuex 和全局变量相比优势

1、vuex存储数据是响应式的,当store中状态发生改变 其他相关组件中改状态也会发生改变 2、vuex中通过统一方法修改状态(显式地提交 (commit) mutation),全局变量随便修改 3、全局变量多了会造成命名污染,vuex不会

vue-router

路由模式

路由守卫

vue2 vue3区别

vue和react区别

前端性能优化

1、 编码阶段

尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher;

如果需要使用v-for给每项元素绑定事件时使用事件代理;

SPA 页面采用keep-alive缓存组件;

在更多的情况下,使用v-if替代v-show;

key保证唯一;

使用路由懒加载、异步组件;

防抖、节流;

第三方模块按需导入;

长列表滚动到可视区域动态加载;

图片懒加载; 图片优化:使用雪碧图、字体图标

2、用户体验

骨架屏;

PWA;

还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。

3、SEO优化

预渲染;

服务端渲染SSR;

4、打包优化

使用cdn加载第三方模块;

1、浏览器缓存
2、资源懒加载、预加载\

webpack打包优化:
1、优化Loader配置,用 include 或 exclude 避免处理不必要的文件
2、优化resolve.extensions配置: 频率出现高的文件后缀优先放在前面; 列表尽可能的小; 书写导入语句时,尽量写上后缀名 3、压缩: js压缩 UglifyJsPlugin gzip压缩(主要是服务端)\

4、splitChunks抽离公共文件;

5、Tree Shaking/Scope Hoisting(去除无用代码);

6、sourceMap优化;

速度: 1、缓存 cache-loader:将 loader 的编译结果写入硬盘缓存,再次构建如果文件没有发生变化则会直接拉取缓存。 多线程打包happypack; 2、多线程

HTTP

跨域问题

跨域是由浏览器同源策略引起的,是指页面请求的接口地址,必须与页面url地址处于同域上(即域名,端口,协议相同)。
这是为了防止某域名下的接口被其他域名下的网页非法调用,是浏览器对JavaScript施加的安全限制

跨域请求产生时,请求是发出去了,也是有响应的,仅仅是浏览器同源策略,认为不安全,拦截了结果,不将数据传递我们使用罢了

解决跨域的方法:
1.jsonp:

利用script标签不受同源策略的影响,后台返回调用某个函数的js代码,且将数据当做函数的参数返回。 缺点:只支持get请求(因为<script>标签只能get)
2.CORS 需要浏览器和服务端同时支持,服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 (该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。)

但是CORS默认是不会发送cookie,如果让请求带上cookie,需要在服务端将Access-Control-Allow-Credentials设置为true,同时需要在客户端将ajax请求中设置withCredentials属性为true
通过cors解决跨域问题,会在发送请求时出现两种情况,分别为简单请求复杂请求简单请求 请求方法是get post head 其一并且Content-Type 的值仅限于下列三者之一:

  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded 是简单请求 简单请求流程: 1、浏览器直接发出CORS请求,即在请求头中增加Origin字段 2、服务器给出http响应 3、浏览器判断响应头中是否包含Access-Control-Allow-Origin字段,如果没有,浏览器就知道服务器是不允许跨域访问的,就会抛出错误。 复杂请求 不属于简单请求的就是复杂请求,复杂请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。

3.代理,同源策略针对浏览器,所以代理根据服务器向服务器请求就无需遵循同源策略。 常见的ngix方向代理,proxy代理 zhuanlan.zhihu.com/p/149734572…

常见的状态码

harttle.land/2015/08/15/…

缓存

浏览器发送请求前,根据请求头的expires和cache-control判断是否命中(包括是否过期)强缓存策略,如果命中,直接从缓存获取资源,并不会发送请求。
如果没有命中强缓存规则,浏览器会发送请求,根据请求头的last-modified和etag判断是否命中协商缓存,如果命中,直接从缓存获取资源。
如果前两步都没有命中,则直接从服务端获取资源。

强缓存

不会向服务器发送请求,直接从缓存中读取资源
控制强缓存字段:ExpiresCache-Control(优先级高)
Expires: 表示缓存到期时间,是绝对时间,如果修改了本地时间,可能会造成缓存失效。
Cache-Control:缓存的相对时间 具体属性:

  • public:所有内容都将被缓存(客户端和代理服务器都可缓存)
  • private:所有内容只有客户端可以缓存,Cache-Control的默认取值
  • no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定
  • no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存
  • max-age=xxx (xxx is numeric):缓存内容将在xxx秒后失效

协商缓存

浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程
协商缓存生效,返回304和Not Modified、 协商缓存失效,返回200和请求结果 控制协商缓存字段:Last-Modified ### Etag

Last-Modified:资源最后修改的时间

Etag 一般是由文件内容 hash 生成的,也就是说它可以保证资源的唯一性,资源发生改变就会导致 Etag 发生改变。 mp.weixin.qq.com/s/Wvc0lkLpg… github.com/xiangxingch…

get post区别

get:获取数据、幂等(多次操作无影响)的、可缓存 post:提交数据、非幂等、不可缓存

http1.0 http1.1 http2.0区别

http https

浏览器

从输入URL到页面加载全过程

juejin.cn/post/701659…

重绘 重排

事件冒泡、事件委托

浏览器兼容

juejin.cn/post/698158…

能不能说一说浏览器的本地存储

浏览器的本地存储主要分为Cookie、WebStorage和IndexDB, 其中WebStorage又可以分为localStorage和sessionStorage。

共同点:

都是保存在浏览器端、且同源的

不同点:

cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递。
cookie数据还有路径(path)的概念,可以限制cookie只属于某个路径下sessionStorage和localStorage不会自动把数据发送给服务器,仅在本地保存。\

存储大小限制也不同

cookie数据不能超过4K,sessionStorage和localStorage可以达到5M\

有效期

sessionStorage:仅在当前浏览器窗口关闭之前有效;
localStorage:始终有效,窗口或浏览器关闭也一直保存,本地存储,因此用作持久数据;
cookie:只在设置的cookie过期时间之前有效,即使窗口关闭或浏览器关闭

作用域不同

sessionStorage:不在不同的浏览器窗口中共享,即使是同一个页面;
localstorage:在所有同源窗口中都是共享的;也就是说只要浏览器不关闭,数据仍然存在
cookie: 也是在所有同源窗口中都是共享的.也就是说只要浏览器不关闭,数据仍然存在

链接:www.jianshu.com/p/44df0526e…

其它

将树形结构转换成数组

//递归实现
// 1、循环每个对象 将其添加到 数组中
// 2、判断是否有子节点 如果有子节点 继续重复1-2步
function treeToList (tree, result = [], level = 0) {
  tree.forEach(node => {
    result.push(node)
    node.level = level + 1
    node.children && treeToList(node.children, result, level + 1)
  })
  return result
}
console.log(treeToList(tree))

参考:juejin.cn/post/689926…

将数组转换成树形结构

转换前:

 const arr = [
    {id:"01", name: "张大大", pid:"", job: "项目经理"},
    {id:"02", name: "小亮", pid:"01", job: "产品leader"},
    {id:"03", name: "小美", pid:"01", job: "UIleader"},
    {id:"04", name: "老马", pid:"01", job: "技术leader"},
    {id:"05", name: "老王", pid:"01", job: "测试leader"},
    {id:"06", name: "老李", pid:"01", job: "运维leader"},
    {id:"07", name: "小丽", pid:"02", job: "产品经理"},
    {id:"08", name: "大光", pid:"02", job: "产品经理"},
    {id:"09", name: "小高", pid:"03", job: "UI设计师"},
    {id:"10", name: "小刘", pid:"04", job: "前端工程师"},
    {id:"16", name: "张流大", pid:"", job: "项目经理"},
    {id:"11", name: "小华", pid:"04", job: "后端工程师"},
    {id:"12", name: "小李", pid:"04", job: "后端工程师"},
    {id:"13", name: "小赵", pid:"05", job: "测试工程师"},
    {id:"14", name: "小强", pid:"05", job: "测试工程师"},
    {id:"15", name: "小涛", pid:"07", job: "运维工程师"},
    
  ]

转换后:

image.png

<script>

  /**
  * 把平铺的数组结构转成树形结构
  */
 const arr = [
    {id:"01", name: "张大大", pid:"", job: "项目经理"},
    {id:"02", name: "小亮", pid:"01", job: "产品leader"},
    {id:"03", name: "小美", pid:"01", job: "UIleader"},
    {id:"04", name: "老马", pid:"01", job: "技术leader"},
    {id:"05", name: "老王", pid:"01", job: "测试leader"},
    {id:"06", name: "老李", pid:"01", job: "运维leader"},
    {id:"07", name: "小丽", pid:"02", job: "产品经理"},
    {id:"08", name: "大光", pid:"02", job: "产品经理"},
    {id:"09", name: "小高", pid:"03", job: "UI设计师"},
    {id:"10", name: "小刘", pid:"04", job: "前端工程师"},
    {id:"16", name: "张流大", pid:"", job: "项目经理"},
    {id:"11", name: "小华", pid:"04", job: "后端工程师"},
    {id:"12", name: "小李", pid:"04", job: "后端工程师"},
    {id:"13", name: "小赵", pid:"05", job: "测试工程师"},
    {id:"14", name: "小强", pid:"05", job: "测试工程师"},
    {id:"15", name: "小涛", pid:"07", job: "运维工程师"},
    
  ]
  //第一种 使用递归 
 /*  循环数组每一项 如果pid == 上一级id 将这条添加到上一级的children中, 并递归寻找这项的子节点 children ,children的值是递归而来
  */
function toTree(list,parId){
      let len = list.length

     //  返回子元素
      function loop(parId){
        let res = [];
        for (let i = 0; i < len; i++) {
            let item = list[i]
            if(item.pid === parId){
                item.children = loop(item.id)
                res.push(item)
            }
          
        }
        return res
      }
      return loop(parId)
     
      
  }


  let result = toTree(arr,'')
  console.log(result);
  
  
  //第二种 使用map 和for循环
  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>

参考:juejin.cn/post/702073…

参考文章: www.jianshu.com/p/3507b078f…

实现并发限制

http1 谷歌浏览器最多 可并发6个 所以需要并发限制 http2 没有此限制 segmentfault.com/a/119000001…