前端知识点总结

646 阅读43分钟

1、html语义化

增强代码可读性

有利于搜索引擎读取更多有利信息

没有css也能呈现页面结构

常用标签:header footer nav main article aside section

2、script 标签中 defer 和 async 的区别

html文档中遇到普通的script标签,暂停解析html,下载script文档,然后执行,执行完成后再去解析后面的html

html文档中遇到async的script标签,解析html的同时下载script文档,然后暂停解析html执行script,执行完成后再去解析后面的html

html文档中遇到defer的script标签,解析html的同时下载script文档,html文档全部解析完成后,再执行script

3、从浏览器地址栏输入 url 到请求返回发生了什么(待完善)

输入url后解析出协议主机路径等信息,构造出一个http请求——>

查找缓存,未过期直接取缓存内容,已过期去服务器请求协商缓存——>

dns解析——>

建立TCP连接三次握手——>

向服务器发起请求——>

服务器响应请求并返回结果——>

断开TCP连接——>

浏览器解析文件,生成dom树,css树,render渲染树,绘制页面

4、盒模型介绍

盒模型包括content、padding、border、margin
IE盒模型宽度:content+padding+border
标准盒模型宽度:content
box-sizing改变盒模型:
content-box(标准盒模型)
border-box(IE盒模型)

5、link和import的区别

1.从属关系区别

@import是 CSS 提供的语法规则,只有导入样式表的作用;link是HTML提供的标签,不仅可以加载 CSS 文件,还可以定义 RSS、rel 连接属性等。

2.加载顺序区别

加载页面时,link标签引入的 CSS 被同时加载;@import引入的 CSS 将在页面加载完毕后被加载。

3.兼容性区别

@import是 CSS2.1 才有的语法,故只可在 IE5+ 才能识别;link标签作为 HTML 元素,不存在兼容性问题。

4.DOM可控性区别

可以通过 JS 操作 DOM ,插入link标签来改变样式;由于 DOM 方法是基于文档的,无法使用@import的方式插入样式。

5.权重区别

link引入的样式权重大于@import引入的样式。

同等权重CSS样式的优先级由高到低的排序是:行内样式、内联样式、外联样式、导入样式 。如果外联样式和导入样式都有一个div{color:XX},最终的div样式是外联样式中所定义div样式。

5、常见的浏览器兼容

常见的浏览器内核可以分四种:Trident、Gecko、Blink、Webkit

IE浏览器:Trident内核,也称为IE内核

Chrome浏览器:Webkit内核,现在是Blink内核

Firefox浏览器:Gecko内核,俗称Firefox内核

Safari浏览器:Webkit内核

Opera浏览器:最初是自己的Presto内核,后来加入谷歌大军,从Webkit又到了Blink内核;

360浏览器:IE+Chrome双内核

猎豹浏览器:IE+Chrome双内核

百度浏览器:IE内核

QQ浏览器:Trident(兼容模式)+Webkit(高速模式)

浏览器内核也称渲染引擎或者排版引擎,主要对网页的语法进行解释,并且进行渲染网页,将网页的代码转换为看得到的页面,一般情况下,浏览器的内核一般采用渲染的引擎

js事件绑定:

IE:dom.attachEvent();

其他浏览器:dom.addEventListener();

ajax对象

IE:ActiveXObject

其他:xmlHttpReuest

CSS方面

IE6下图片下有空隙产生 LI中内容超过长度后以省略号显示 此技巧适用与IE、Opera、safari、chrom浏览器,FF暂不支持 盒模型的解释

5、css 选择器和优先级

!important>内联>id>类选择器,伪类,属性选择器>标签选择器,伪元素
改第三方库时,优先级是由优先级是由 A 、B、C、D这些值来决定,这些值的计算方法分别是:
A:有内联样式1,否则为0
B:id选择器出现的次数
C:类选择器,伪类,属性选择器出现的次数
D:标签选择器,伪元素出现的次数
*比较规则是: (A,B,C,D)从左往右依次进行比较 ,较大者胜出,如果相等,则继续往右移动一位进行比较
举例:
`#list .pser:last-child .item { color:#444 } (0,1,3,0)

#list .psert .item { color:#333 } (0,1,2,0)`

计算结果:上面的优先级最高,color是#333

sass和less的异同:

相同: 1、混入(Mixins)——class中的class;

//定义一个模块
.boxShadow(@x:0, @y:0, @blur:1px, @color:#000){ 
  -moz-box-shadow: @arguments; 
  -webkit-box-shadow: @arguments; 
  box-shadow: @arguments; 
}

#header { 
  .boxShadow(2px, 2px, 3px, #f36); 
}

2、参数混入——可以传递参数的class,就像函数一样;

3、嵌套规则——Class中嵌套class,从而减少重复的代码;

4、运算——CSS中用上数学;

@init: #111111; 

@transition: @init*2; 

.switchColor { 
  color: @transition; 
}

5、颜色功能——可以编辑颜色;

lighten(@color, 10%); //颜色淡化
darken(@color, 10%); //颜色加深
saturate(@color, 10%); //饱和度增加 
desaturate(@color, 10%); //饱和度降低
fadein(@color, 10%); //透明度增加
fadeout(@color, 10%); //透明度降低
spin(@color, 10); //色调调整(+10)
spin(@color, -10); //色调调整(-10)

6、名字空间(namespace)——分组样式,从而可以被调用;

#namespace when (@color = blue) { 
  .mixin() { color: red; } 
} 
p { #namespace .mixin(); }

7、作用域——局部修改样式;

8、JavaScript 赋值——在CSS中使用JavaScript表达式赋值。

区别: 1.编译环境不同

less是通过js编译 是在客户端处理

sass同通过ruby 是在服务器端处理

2.变量符不一样

less是用@,sass是用$

3.sass支持条件语句,可以使用if{}else{},for{}循环等等。而less不支持。

4.less没有输出设置,sass提供4中输出选项:nested, compact, compressed 和 expanded。

输出样式的风格可以有四种选择,默认为nested

nested:嵌套缩进的css代码

expanded:展开的多行css代码

compact:简洁格式的css代码

compressed:压缩后的css代码

5.Sass和Less的工具库不同

Sass有工具库Compass, 简单说,Sass和Compass的关系类似于像Javascript和jQuery的关系,Compass在Sass的基础上,封装了一系列有用的模块和模板,补充强化了Sass的功能。 Less有UI组件库Bootstrap,Bootstrap是web前端开发中一个比较有名的前端UI组件库,Bootstrap的样式文件部分源码就是采用Less语法编写。

6、transition和animation的区别

用法:

transform: 主要用于给元素做变换,主要由以下几种变换,rotate(旋转),scale(缩放),skew(扭曲),translate(移动)和matrix(矩阵变换)。

transition: property duration timing-function delay;用来定义某个css属性或者多个css属性的变化的过渡效果.

.transition-p 
{ width: 100px; height: 40px; opacity: 0.6; border-radius: 10px; background: #03A9F4; text-align: center; 
line-height: 40px; color: #fff; 
transition: all 0.5s ease-in; } 
.transition-p:hover 
{ opacity: 1.0; width: 120px; }

animation:动画的定义,先通过@(-webkit-)keyframes定义动画名称及动画的行为,然后再通过animation的相关属性定义动画的执行效果.


.animation-container { position: relative; padding-bottom: 50px; } 
.animation-infinite { position: absolute; left: 0; top: 0; width: 100px; height: 50px; padding: 10px; background: #03A9F4; color: #fff; animation: move-to-right 2.0s infinite; } /* 定义右移动画 */ 
@keyframes move-to-right { 0% { opacity: 1.0; width: 100px; left: 0; } 25% { opacity: 0.4; width: 120px; left: 40px; } 50% { opacity: 0.6; width: 150px; left: 80px; } 75% { opacity: 0.8; width: 120px; left: 40px; } 100% { opacity: 1; width: 100px; left: 0; } }

区别:

transition:不能自动执行,需要事件触发;不能重复发生,只有开始和结束状态,不能暂停 animation 能自动执行,重复发生,任意指定过渡状态0%-100%,可以暂停

requestAnimationFrame

  • 浏览器(所以只能在浏览器中使用)专门为动画提供的 API,让 DOM 动画、Canvas 动画、SVG 动画、WebGL 动画等有一个统一的刷新机制,类似于setTimeout。
  • 该方法告诉浏览器希望执行动画并请求浏览器在下一次重绘之前调用回调函数来更新动画。
  • 由系统来决定回调函数的执行时机,在运行时浏览器会自动优化方法的调用。通过requestAnimationFrame调用回调函数引起的页面重绘或回流的时间间隔和显示器的刷新时间间隔相同。所以 requestAnimationFrame 不需要像setTimeout那样传递时间间隔,而是浏览器通过系统获取并使用显示器刷新频率。

6、重排(reflow)和重绘(repaint)的理解

首先说明一下网页从HTML文件变成屏幕上的画面所经历的过程:

  • HTML内容被HTML解析器解析生成DOM树
  • CSS内容被CSS解析器解析生产CSSOM树
  • DOM树+CSSOM树会生产Render Tree(渲染树)
  • 生成布局,浏览器根据渲染树来布局,以计算每个节点的几何信息
  • 将各个节点绘制到屏幕上

其中第4步为布局排列(flow),第5步为绘制(paint),这两部加起来也就是我们通常所说的

重排

当DOM的变化影响了元素的几何信息(元素的的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。

重绘

我们通过构造渲染树和回流阶段,知道了哪些节点是可见的,以及可见节点的样式和具体的几何信息(位置、大小),那么我们就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘节点。

何时发生重排和重绘(重排一定会触发重绘,重绘不一定触发重排,比如节点背景颜色改变就只触发了重绘,没有重排)

重排这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要重排。比如以下情况:

  • 页面初始渲染(无法避免)

  • 添加或删除可见的DOM元素

  • 元素位置的改变,或者使用动画

  • 改变元素尺寸,比如边距、填充、边框、宽度和高度等

  • 填充内容的改变,比如文本的改变或图片大小改变而引起的计算值宽度和高度的改变

  • 浏览器窗口尺寸的变化(resize事件发生时)

  • 设置 style 属性的值,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow

  • 读取某些元素属性:offsetLeft/Top/Height/Width, clientTop/Left/Width/Height, scrollTop/Left/Width/Height, width/height, getComputedStyle(), currentStyle(IE)

为什么?

获取clientTop时,浏览器的缓存渲染队列机制将不再生效,这是因为,clientWidth 是一个元素的实时宽度,必须重排重绘以后才能得到,如果不提前进行重排重绘,clientWidth 有可能拿到的是浏览器缓存队列没执行完的时候的值。

补充:原生js如何获取和设置上述属性

  • document.body.clientHeight: 网页可见区域高
  • document.body.clientWidth: 网页可见区域宽
  • document.body.scrollHeight: 网页正文全文高度,包含可见区域和滚动区域
  • document.body.offsetHeight: 网页可见区域高度,包含边线的高度
  • document.body.offsetWidth: 网页可见区域宽度,包含边线的宽度
  • window.screen.height: 屏幕分辨率高
  • window.screent.width: 屏幕分辨率宽
// 获取元素宽度
document.getElementsByClassName('imgbox')[0].style.offsetWidth
// 设置元素宽度
document.getElementsByClassName('imgbox')[0].style.width = 100

减少重排和重绘

最小化重排和重绘,比如样式集中改变

批量操作DOM

使用absolute脱离文档流

开启GPU加速,利用 css 属性使浏览器为元素创建⼀个 GPU 图层,这使得动画元素在一个独立的层中进行渲染。当元素的内容没有发生改变,就没有必要进行重绘。

  • transform: translate3d(xx,xx,xx);
  • rotate3d(xx,xx,xx,xxdeg)
  • scale3d(xx, xx,xx);
  • transform: translateZ(0);

7、对 BFC 的理解

BFC是块级格式化上下文,可以看成元素的一种属性,元素有了这种属性后,这个元素就可以看成隔离了的独立容器,容器内其他元素就不受容器外元素的影响了。

BFC的特性:避免外边距重叠,清除浮动,阻止元素被浮动元素覆盖。

生成BFC:

  • overflow设置为非visible;
  • float设置为非none;
  • position设置为absolute,fixed;
  • display设置为inline-block,flex

8、多种方式实现两栏布局

a、左侧左浮动,宽度100,右侧设置margin-left为100.

b、左侧左浮动,宽度100,右侧设置overflow是hidden.

c、左侧设置absolute,宽度100,let,top都为0,右侧设置为margin-left为100.

d、左侧设置宽度100,右侧设置absolute,left是100,right 0 top 0 bottom 0

9、实现双飞翼和圣杯布局(待研究)

10、垂直水平居中的几种方式

宽高不定:

flex

  • absolute left:50% right 50% transform: translate(-50%, -50%);

  • 外层设置 display:table-cell vertical-align:middle text-align:center 内层设置 inline-block

  • 外层设置 display:grid 内层是 margin:auto

宽高确定:

以上都能用

absolute left:50% right 50% left:-100px right:-100px

11、flex布局

flex-flow: row no-wrap 是flex-direction flex-wrap 的简写

flex: 0 1 auto 是flex-grow flex-shrink flex-basis 放大几倍、缩小几倍、宽度

order:

justify-content: flex-start | flex-end | center | space-between | space-around;

align-items: flex-start | flex-end | center | baseline | stretch;

align-self:

align-content:

12、line-height 如何继承

父元素是10px、1.5 ,直接继承 ;父元素是200%,继承的父元素的 font-size * 200%

13、数据类型

八种数据类型 undefined null object Boolean string number symbol BigInt

symbol代表独一无二的值,最大用途是定义对象的属性值(不能用点运算符), 确保对象属性使用唯一标识符,不会发生属性冲突的危险。 应用:防止框架中同名属性被覆盖、模拟定义私有变量

let name = symbol()

let obj = { [name]: alice }

BigInt可以表示任意大小的整数,可以支持解决超出number类型的数值范围

14、set和map、WeakSet和weakMap

Map是键/值对的集合,具有极快的查找速度,这些键和值可以是任何数据类型。初始化需要一个二维数组,或者初始化一个空map;set get delete has key values

Set是一组key的集合,不存储value,key不能重复;add delete has keys values

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 区别如下。

  • WeakSet 的成员只能是对象,而不能是其他类型的值
  • WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用
    也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。WeakSet结构有助于防止内存泄漏
  • WeakSet没有size属性和forEach方法
    WeakSet 不能遍历,是因为成员都是弱引用,随时可能消失,遍历机制无法保证成员的存在,很可能刚刚遍历结束,成员就取不到了。

WeakSet 有以下三个方法:

  • WeakSet.prototype.add(value): 向 WeakSet 实例添加一个新成员。
  • WeakSet.prototype.delete(value): 清除 WeakSet 实例的指定成员。
  • WeakSet.prototype.has(value): 返回一个布尔值,表示某个值是否在 WeakSet 实例之中。

WeakMap

WeakMap结构与Map结构类似,也是用于生成键值对的集合。WeakMap与Map的区别有以下几点:

  • WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名
  • WeakMap的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内
    也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。总之,WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap结构有助于防止内存泄漏。
  • 同 WeakSet 一样,WeakMap也没有遍历操作,即 (keys()values()entries()方法)
    因为没有办法列出所有键名,某个键名是否存在完全不可预测,跟垃圾回收机制是否运行相关。这一刻可以取到键名,下一刻垃圾回收机制突然运行了,这个键名就没了,为了防止出现不确定性,就统一规定不能取到键名。
  • 不支持clear方法

WeakMap 有以下四个个方法:

  • get(): 通过键名取值
  • set(): 设置键值对
  • has(): 判断是否存在
  • delete(): 删除一项记录

14、let和const区别,箭头函数和普通函数区别,为什么箭头函数不能new

var:

  • 存在变量提升
  • 一个变量可多次声明,后面的声明会覆盖前面的声明

let:

  • 不存在变量提升,let声明变量前,该变量不能使用
  • let命令所在的代码块内有效,在块级作用域内有效
  • let不允许在相同作用域中重复声明

const

  1. const声明一个只读的变量,声明后,值就不能改变
  2. const必须初始化
  3. const并不是变量的值不能改动,而是变量指向的内存地址所保存的数据不得改动

14、数据类型的判断

typeof 只能判断基本数据类型,无法判断array function null

instanceOf 只能判断对象类型数据,无法判断number、string等,机制是判断在原型链中是否能找到该类型的原型

object.prototype.toString.call() 原始数据类型都可以判断 包括error date

如何判断一个变量是否为数组

`Array.isArray(a)

a.proto === Array.prototype

a instanceOf Array

object.prototype.toString.call() "[object Array]"`

15、手写深拷贝

new Map() 是一种新的数据结构,类似对象,但是对象的key只能是字符串,map的key可以是任何值

function deepClone(obj, map = new Map()) {
    if (typeof obj !== 'object') {
        return obj
    }
    // 用map存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。
    if (map.get(obj)) {
    return map.get(obj)
    }
    let result = {}
    if (obj instanceOf Array) {
       result = []
    }
    map.set(obj, result)
    for (let key in obj) {
        result[key] = deepClone(obj[key],map) 
    }
    return result
}

16、深拷贝和浅拷贝

实现深拷贝:

数组:

  • 1、直接遍历
  • 2、concat
  • 3、slice
  • 4、[...arr]

对象:

  • 直接遍历
  • object.assign(如果源对象是引用对象,也不能实现深拷贝)
  • {..obj}
  • json.parse()可以实现多层级的深拷贝,但存在以下问题:
  1. 当json里面有时间对象时,序列化结果中会将时间对象转换成为字符串的形式
  2. 当json里有RegExp、Error对象时,序列化的结果将只得到一个空对象;
  3. 当json里有 function,undefined时,序列化的结果中function,undefined 会丢失
  4. 当json里有 function,undefined时,序列化的结果中function,undefined 会丢失
  5. 当json里有对象是由构造函数生成的时候,序列化的结果会丢弃该对象的 constructor
  6. 当对象中有在内存中的循环引用时,该方法将会报错

17、遍历对象有哪些方式

for in 自身、继承、可枚举、非symbol for (let xx in obj)

object.keys() 自身、可枚举、非继承、非symbol

object.getOwnPropertyNames() 自身、不可枚举、非继承、非symbol

reflect.ownKeys() 自身、非继承、可枚举、symbol

for of (把对象Object.entries转化成二维数组后利用解构赋值遍历) for (let xx of array)

Object.entries Object.entries() 可以把一个对象的键值以数组的形式遍历出来,object.entries({a:0,m:9}) [[a,0],[m,9]],可以用for of遍历数组了

Object.values() 把一个对象的值以数组形式遍历出来,只遍历自身属性,且不包括symbol

Object.create(原型,创建的对象)

var obj = {
    name: "Tom",
    age: 20,
    family: ["mama", "baba"]
};
var person = Object.create(obj, {
    "job": {
        value: "IT",
        congigurable: true,
        enumerable: true,
        writable: false
    },
    "age": {
        value: 18,
        congigurable: true,
        enumerable: true,
        writable: false
    }
});

Array.from:将一个类数组对象或者可遍历对象转换成一个真正的数组 将类数组对象转换为真正数组:

let arrayLike =
{ 
    0'tom'1:'65',
    2'男',
    3: ['jane','john','Mary'],
    'length': 4 
}
let arr = Array.from(arrayLike)
console.log(arr) // ['tom','65','男',['jane','john','Mary']]

将Set结构的数据转换为真正的数组:

let arr = [12,45,97,9797,564,134,45642] 
let set = new Set(arr)
console.log(Array.from(set))  // [ 12, 45, 97, 9797, 564, 134, 45642 ]

将字符串转换为数组:

let str = 'hello world!';
console.log(Array.from(str)) // ["h", "e", "l", "l", "o", " ", "w", "o", "r", "l", "d", "!"]

18、原型和原型链

原型:每个js对象在创建时,都会与之关联一个对象,并继承这个对象的属性和方法,被关联的这个对象就是原型

原型链:相关关联的原型组成的链状结构就是原型链,通过实例.__proto__查找原型上的属性,从子类一直向上查找对象原型的属性,继而形成一个查找链即原型链. 实例.proto==构造函数.prototype==原型

19、继承实现

原型链继承: 父类的实例作为子类的原型 Child.prototype = new Parent()

问题:属性没有私有化,不能传参,原型上属性的改变会作用到所有的实例上,因为父类上的实例方法会变成子类上的原型方法。

构造函数继承:在子类内部通过call调用父类的构造函数 Parent.call(this)

问题:实例只是子类的实例,不是父类的实例,且只能继承父类的属性和方法,不能继承原型的属性和方法

组合继承:上面两种继承组合起来 但是要多加一句 Child.prototype.constructor = Child; // 修复构造函数指向

问题:调用两次父类的构造函数,实例属性会屏蔽原型属性

寄生组合继承:Child.prototype = Object.creat(Parent.prototype);Child.prototype.constructor = Child,这样就不用调用两次父类构造函数了。

ES6实现继承,通过extends关键字实现,并且需要在constructor函数中执行super(),这样才能继承父类的this,如果不调用super(),使用this会报错。

20、作用域与作用域链、执行上下文、this的指向问题

作用域:决定了代码区块中变量和其他资源的可见性,作用域就是变量的使用范围,换句话说就是这个变量在程序的哪些区域可见。作用域就是代码的执行环境,全局作用域就是全局执行环境,局部作用域就是函数的执行环境,它们都是栈内存。

作用域链:多个作用域对象连续引用形成的链式结构。当在Javascript中使用一个变量的时候,首先Javascript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域,如果在全局作用域里仍然找不到该变量,它就会直接报错。

作用域链在JS内部中是以数组的形式存储的,数组的第一个索引对应的是函数本身的执行期上下文,也就是当前执行的代码所在环境的变量对象,下一个索引对应的空间存储的是该对象的外部执行环境,依次类推,一直到全局执行环境。

this指向:

  • setTimeout中this指向window,
  • 对象obj中方法调用时this指向obj,
  • 普通函数调用this指向window,或者调用它的对象,
  • 构造函数调用时中this指向实例。
  • 闭包中的this指向window

21、闭包

闭包是有权访问另一个函数作用域中变量的函数,而且这个私有变量始终在内存中,不会被回收。

闭包使用场景:

  • 使用return返回函数
  • 函数作为参数
  • 自执行函数
  • setTimeout和自执行函数
  • 所有的回调函数

应用:解决for循环中事件赋值引用

function fn(){
  var arr = [];
  for(var i = 0;i < 5;i ++){
	arr[i] = (function(i){
		return function (){
			return i;
		};
	})(i);
  }
  return arr;
}

闭包的应用:

  • 设置私有变量:我们希望一个变量只在函数内部,或者对象内部方法访问到,外部无法触及,这样的变量,就是私有变量。把私有变量放在最外层立即执行函数中,并通过立即执行这个函数,创造了一个闭包作用域的环境。
// 利用IIFE生成闭包,返回user类
const User = (function () {
    // 定义私有变量_password
    let _password

    class User {
        constructor(username, password) {
            // 初始化私有变量_password
            _password = password
            this.username = username
        }

        login() {
            console.log(this.username, _password)

        }
    }

    return User
})()

let user = new User('小明',123465)
console.log(user.username); // 小明
console.log(user.password); // undefined
console.log(user._password); //undefined
user.login(); // 小明 undefined
复制代码

在这段代码中,私有变量_password被好好的保护在User这个立即执行函数内部,此时实例暴露的属性已经没有_password,通过闭包,我们成功利用了自由变量模拟私有变量的效果。

  • 防抖和节流函数

闭包的问题:

1、由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2、因为使用闭包时可能比较容易形成循环引用,如果闭包的作用域链中保存着一些DOM节点,只要匿名函数存在,element 的引用数至少也是1,那么就意味着该元素将无法被销毁。这时候就造成内存泄漏,但这并不是闭包问题,而这是IE的bug。

因为IE浏览器基于引用计数策略的垃圾回收机制中,如果两个对象之间形成了循环引用,那么这两个对象都无法被回收。内存泄露只是由于IE9 之前的版本对JScript对象和COM对象使用不同的垃圾收集策略,从而导致内存无法进行回收。

会造成内存泄漏的情况:

  • 不必要的全局变量
  • 遗忘清理的计时器
  • 遗忘清理的dom元素引用

经典面试题

  • for 循环和闭包(号称必刷题)
var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]()
/* 输出
    3
    3
    3
/
复制代码

这里的 i 是全局下的 i,共用一个作用域,当函数被执行的时候这时的 i=3,导致输出的结构都是3。

  • 使用闭包改善上面的写法达到预期效果,写法1:自执行函数和闭包
var data = [];

for (var i = 0; i < 3; i++) {
    (function(j){
      setTimeout( data[j] = function () {
        console.log(j);
      }, 0)
    })(i)
}

data[0]();
data[1]();
data[2]()
复制代码
  • 写法2:使用 let
var data = [];

for (let i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]()
复制代码

let 具有块级作用域,形成的3个私有作用域都是互不干扰的。

思考题和上面有何不同 (字节)

var result = [];
var a = 3;
var total = 0;

function foo(a) {
    for (var i = 0; i < 3; i++) {
        result[i] = function () {
            total += i * a;
            console.log(total);
        }
    }
}

foo(1);
result[0]();  // 3
result[1]();  // 6
result[2]();  // 9

22、call、apply、bind 实现

call和apply可以改变this指向

Function.prototype.myCall(...arg){
    let context = arg.slice(0)
    let arg = arg.slice(1)
    if (typeof context === 'object') {
        context = context || window
    } else {
        context = obj.create({})
    }
    context.fn = this  // 这里的this是函数实例;这时这个函数就是context对象中的一个属性,函数里的this就指向context对象了
    delete context.fn
    return context.key(arg)
}

Fcuntion.prototype.myBind(obj, ...args){
    let fn = this
    let F = function(){}
    if (fn.prototype) {
        F.prototype = fn.prototype
    }
    let res = function(){
        let callobj = F.prototype.isPrototypeOf(this) ? this : obj  //这句话不懂
        return fn.call(callobj,...args)
    }
    res.prototype = new F()
    return res
}

23、前端模块化common.js es6 AMD CMD(待补充)

commonJS

是node中采用的模块化规范,每个文件就是一个模块,有自己的作用域。在服务器端,模块的运行记载是同步的,浏览器端,模块是提前编译好的。

引入方式 require() 导出方式 module.exports exports

这两种方式的区别:

exports只能使用语法来向外暴露内部变量:如exports.xxx = xxx;

module.exports既可以通过语法,也可以直接赋值一个对象。

console.log(module.exports === exports)

结果为true

  • 每个模块中都有一个module对象
  • module对象中有一个exports对象
  • 我们可以把需要导出的成员都挂载到module.exports接口对象中
  • 也就是module.exports.xxx=xxx的方式
  • 但是每次都module.exports.xxx=xxx很麻烦,点儿的太多了,所以node为了你方便,同时在每一个模块中都提供了一个成员叫:exports
  • exports===module.exports结果为true
  • 所以对于module.exports.xxx=xxx完全可以:exports.xxx=xxx来写
  • 当一个模块需要导出单个成员的时候,只能使用module.exports=xxx的方式
  • 因为每个模块最终向外return的是module exports
  • exports只是module.exports的一个引用
  • 所以,即便你为exports重新赋值,也不会影响module.exports
  • 但是有一种赋值方式比较特殊:exports=module.exports这个用来重新建立引用关系的

总结

  • 导出多个成员
    exports.b='hello'
    exports.c=function(){ console.log('ccc') }
    exports.d={ foo:'bar' }

  • 只导出单个成员
    module.exports='hello'

  • 下面代码是错误的
    module.exports='hello'
    module.exports=function(){ return x+y }

  • 下面代码是正确的

    module.exports = {
        add:function(){
        return x+y},
        str:'hello'
    }
    

es6

引入方式 import 导出 export default export 这两种方式区别

在一个文件或模块中,export、import可以有多个,export default仅有一个

通过export方式导出,在导入时要加{ },export default则不需要,且可以以任意变量名导入

输出单个值,使用export default

输出多个值,使用export

AMD

可以通过异步方式引入模块

定义模块 define([module1,module2],function(m1,m2){ return 模块 })

引入模块 require([module1,module2],function(m1,m2){ 使用m1和m2 })

CMD

结合了AMD和commonjs的优点

定义模块

define(function(require,exports,module){
    require(../name.js) // 同步引入
    require.async(../name.js,function(){
    }) // 异步引入依赖
    exports = {} //导出模块
})

24、实现new函数

function myNew(func, ...args){
    let obj = new Object() //创建一个新对象
    obj._proto_ = func.prototype // 使这个对象的原型指向构造函数的原型
    let res = func.call(obj, args) // 执行构造函数,并将构造函数的this指向obj
    return typeof res === 'object' ? res : obj // 如果构造函数返回值是object类型,则返回这个object,如果不是,就返回刚刚创建的对象
}

25、JS异步

事件循环:

宏任务 settimeout setinterval ajax dom事件

微任务 nextTick then await

先宏任务再微任务,先同步后异步

26、promise和async await

promise为了解决异步处理中的回调地狱问题,链式操作更简洁易读。

分为三种状态,pending、fulfilled、rejected,且状态不可逆。

Promise是一个构造函数,原型方法包括All、Race、Reject、Resolve。Promise.all()和Promise.Race()会返回一个promise实例。

Promise的实例方法包括then,catch、filled。then里写的函数就是我们平时用的回调函数,只是用链式调用的方式把回调分离出来.

用法f1().then((data) => return f2()).then() f1和f2返回的都是promise实例。

实现promise

class myPromise {
    constructor(excutor) {
        this.result = undefined
        this.state = 'pending'
        const resovle = (val) => {
            this.state = 'fulfilled'
            this.result = val
        }
        // 省略reject
        excutor(resolve, reject)
    }
    then(onResolved,onReject){
        return new myPromise((resolve,reject) => {
           // 判断此时是完成和失败状态才直接执行
            const res = onResolved(this.result)
            if (res 是promise实例) {
                res.then((val) => resolve(val))
            } else {
                resolve(res)
            }
            // 根据状态去判断何时执行onResolved,何时执行onReject
        })
    }
}

由于promise是支持多个回调的,p1可以多次调用then方法,所有回调都会执行的。所以需要改造一下,构造函数中增加一个callbackList参数,在then方法中把传进来的回调函数push到这个数组中,在resolve中遍历执行这个数组中的回调函数;

/*
       * 如果是pending状态,则异步任务,在改变状态的时候去调用回调函数
       * 所以要保存回调函数
       * 因为promise实例可以指定多个回调,于是采用数组 
       */
      if (this.promiseState === "pending") {
        this.callbackList.push({
            onResolved: () => {
              handleCallback(onResolved)
            },
            onRejected: () => {
              handleCallback(onRejected)
            }
        })

在resovle方法中增加这一行

// 异步执行所有回调函数
      this.callbackList.forEach(cb => cb.onResolved(value));

静态方法 all,接受一个数组实例参数,返回一个promise实例,遍历数组实例,调用实例的forEach方法,等待全部返回后,resolve结果。

 static all(promiseArrays) {
    return new iPromise((resolve, reject) => {
      // 用以存储执行的结果
      let results = [];
      let length  = promiseArrays.length;
      promiseArrays.forEach((promiseObj, index, promiseArrays) => {
        promiseObj.then((val) => {
          results.push(val);
          // 由于是多个异步任务的关系,需要判断是否都执行完毕
          if (results.length === length) {
            resolve(results);
          }
        }, err => {
          // 如有错误,则reject
          reject(err);
        });
      })
    })
  • async和await 是Generator 的语法糖,是一种建立在Promise之上的编写异步或非阻塞代码的新方法,是解决js异步的新方案。
  • async修饰的函数会默认返回一个promise对象,await 后面接一个返回 promise的函数并执行,获取返回值。
  • async修饰的函数 如果返回结果为非Promise对象,则返回一个PromiseState为fulfilled,PromiseResult为返回结果的Promise;如果返回结果为一个Promise对象,则返回这个Promise对象

优点:不再链式调用,try catch捕获异常,异步代码同步的写法更简洁。

27、浏览器的垃圾回收机制

有两种垃圾回收策略:

标记清除:标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。

引用计数:它把对象是否不再需要简化定义为对象有没有其他对象引用到它。如果没有引用指向该对象(引用计数为 0),对象将被垃圾回收机制回收。 V8 的垃圾回收机制也是基于标记清除算法,不过对其做了一些优化。 针对新生区采用并行回收。 针对老生区采用增量标记与惰性回收。

28、实现一个 EventMitter 类

EventEmitter是 Node. js中一个实现观察者模式的类,主要功能是订阅和发布消息,用于解决多模块交互而产生的模块之间的耦合问题。

Node采用了事件驱动机制,而EventEmitter就是Node实现事件驱动的基础,在EventEmitter的基础上,Node几乎所有的模块都继承了这个类,这些模块拥有了自己的事件,可以绑定/触发监听器,实现了异步操作。

class EventEmitter {
    constructor() {
        this.events = {};
    }
 
    on(type, handler) {
        if (!this.events[type]) {
            this.events[type] = [];
        }
        this.events[type].push(handler);
    }
 
    addListener(type,handler){
        this.on(type,handler)
    }
 
    prependListener(type, handler) {
        if (!this.events[type]) {
            this.events[type] = [];
        }
        this.events[type].unshift(handler);
    }
 
    removeListener(type, handler) {
        if (!this.events[type]) {
            return;
        }
        this.events[type] = this.events[type].filter(item => item !== handler);
    }
 
    off(type,handler){
        this.removeListener(type,handler)
    }
 
    emit(type, ...args) {
        this.events[type].forEach((item) => {
            Reflect.apply(item, this, args);
        });
    }
 
    once(type, handler) {
        this.on(type, this._onceWrap(type, handler, this));
    }
 
    _onceWrap(type, handler, target) {
        const state = { fired: false, handler, type , target};
        const wrapFn = this._onceWrapper.bind(state);
        state.wrapFn = wrapFn;
        return wrapFn;
    }
 
    _onceWrapper(...args) {
        if (!this.fired) {
            this.fired = true;
            Reflect.apply(this.handler, this.target, args);
            this.target.off(this.type, this.wrapFn);
        }
    }
}

测试代码如下:

const ee = new EventEmitter();
 
// 注册所有事件
ee.once('wakeUp', (name) => { console.log(`${name} 1`); });
ee.on('eat', (name) => { console.log(`${name} 2`) });
ee.on('eat', (name) => { console.log(`${name} 3`) });
const meetingFn = (name) => { console.log(`${name} 4`) };
ee.on('work', meetingFn);
ee.on('work', (name) => { console.log(`${name} 5`) });
 
ee.emit('wakeUp', 'xx');
ee.emit('wakeUp', 'xx');         // 第二次没有触发
ee.emit('eat', 'xx');
ee.emit('work', 'xx');
ee.off('work', meetingFn);        // 移除事件
ee.emit('work', 'xx');           // 再次工作

29、cookie,localStorage 和 sessionStorage的区别和用法

  • cookie数据大小不能超过4k;sessionStorage和localStorage的存储比cookie大得多,可以达到5M+
  • cookie设置的过期时间之前一直有效;localStorage永久存储,浏览器关闭后数据不丢失除非主动删除数据;sessionStorage数据在当前浏览器窗口关闭后自动删除
  • cookie的数据会自动的传递到服务器;sessionStorage和localStorage数据保存在本地

30、http协议

状态码:

  • 200 - 成功。
  • 301 - 永久重定向(配合 location,浏览器自动处理)。
  • 302 - 临时重定向(配合 location,浏览器自动处理)。
  • 304 - 资源未被修改。
  • 403 - 没权限。
  • 404 - 资源未找到。
  • 500 - 服务器错误。
  • 504 - 网关超时。

HTTP1.0和HTTP2.0的区别:

HTTP1.0默认是短连接:也就是说每次与服务器交互,都需要新开一个连接。

在HTTP1.1中默认就使用持久化连接来解决:建立一次连接,多次请求均由这个连接完成。

HTTP2所有性能增强的核心在于新的分帧层(不再以文本格式来传输了),它定义了如何封装http消息并在客户端与服务器之间传输。HTTP2连接上传输的每个帧都关联到一个“流”。流是一个独立的,双向的帧序列可以通过一个HTTP2的连接在服务端与客户端之间不断的交换数据。

  1. HTTP/2采用二进制格式而非文本格式
  2. HTTP/2是完全多路复用的,而非有序并阻塞的——只需一个连接即可实现并行
  3. 使用报头压缩,HTTP/2降低了开销
  4. HTTP/2让服务器可以将响应主动“推送”到客户端缓存中

http和https的区别:

http: 是一个客户端和服务器端请求和应答的标准(TCP),用于从 WWW 服务器传输超文本到本地浏览器的超文本传输协议。

https:是以安全为目标的 HTTP 通道,即 HTTP 下 加入 SSL 层进行加密。其作用是:建立一个信息安全通道,来确保数据的传输,确保网站的真实性。 两者区别:

  • http 是超文本传输协议,信息是明文传输,HTTPS 协议要比 http 协议安全,https 是具有安全性的 ssl 加密传输协议,可防止数据在传输过程中被窃取、改变,确保数据的完整性(当然这种安全性并非绝对的,对于更深入的 Web 安全问题,此处暂且不表)。
  • http 协议的默认端口为 80,https 的默认端口为 443。
  • http 的连接很简单,是无状态的。https 握手阶段比较费时,会使页面加载时间延长 50%,增加 10%~20%的耗电。
  • https 缓存不如 http 高效,会增加数据开销。
  • Https 协议需要 ca 证书,费用较高,功能越强大的证书费用越高。
  • SSL 证书需要绑定 IP,不能再同一个 IP 上绑定多个域名,IPV4 资源支持不了这种消耗。

https工作原理:

客户端在使用 HTTPS 方式与 Web 服务器通信时有以下几个步骤:

客户端使用 https url 访问服务器,则要求 web 服务器建立 ssl 链接。

web 服务器接收到客户端的请求之后,会将网站的证书(证书中包含了公钥),传输给客户端。

客户端和 web 服务器端开始协商 SSL 链接的安全等级,也就是加密等级。

客户端浏览器通过双方协商一致的安全等级,建立会话密钥,然后通过网站的公钥来加密会话密钥,并传送给网站。

web 服务器通过自己的私钥解密出会话密钥。

web 服务器通过会话密钥加密与客户端之间的通信。

TCP三次握手:1、客户端发送建立连接请求给服务端等待确认,2、服务端确认收到了请求包,并向客户端发送了可以连接请求包 3、客户端收到请求包,向服务器发送确认

TCP四次挥手:1、客户端请求断开连接 2、服务端确认收到断开连接请求 3、服务端已经没有数据需要发送给客户端时,给客户端发送断开连接 4、客户端确认收到断开连接请求

POST和GET区别:

  • 1、get在浏览器退回时不会重复提交,但post会
  • 2、get产生的url地址,可以被收藏,post不会
  • 3、get请求会自动被缓存,post除非手动设置,否则不会
  • 4、get不安全,参数直接暴露在url上
  • 5、get参数会被保留在浏览记录中

安全类知识:

CSRF 跨站请求伪造

攻击原理:

  • 1、用户登录了站点A,验证通过后,服务器会下发cookie,会保存一段时间

  • 2、用户再没有登出A的情况下访问B站点

  • 3、B站点某个页面会向A站点发出请求,这个请求会带上A站点的cookie

  • 4、站点A根据cookie判断请求是用户C发送的 防御:

  • 1、请求中增加token验证

  • 2、通过refer来识别

  • 3、cookie改为httpOnly

  • 4、使用post别用get,但post请求也可以通过form伪造

XSS跨域脚本攻击

原理是将一段JS代码注入网页并能正确执行。区别是不需要登录验证,向页面注入脚本

防御:

  • 1、输入框内容进行转义和过滤 & < > /转成&amp
  • 2、获取用户输入不用innetHtml,用innerText
  • 3、对Url参数进行过滤,使用白名单验证
  • 4、所有要动态输出到页面的内容,都继续进行编码和转义

前后端设置的请求头: 前端: 后端:

31、http缓存策略

强缓存:Cache-Control

  • 在 Response Headers 中。
  • 控制强制缓存的逻辑。
  • 例如 Cache-Control: max-age=3153600(单位是秒) Cache-Control 有哪些值:
  • max-age:缓存最大过期时间。
  • no-cache:可以在客户端存储资源,每次都必须去服务端做新鲜度校验,来决定从服务端获取新的资源(200)还是使用客户端缓存(304)。
  • no-store:永远都不要在客户端存储资源,永远都去原始服务器去获取资源。

协商缓存

  • 服务端缓存策略。
  • 服务端判断客户端资源,是否和服务端资源一样。
  • 一致则返回 304,否则返回 200 和最新的资源。 资源标识:
  • 在 Response Headers 中,有两种。
  • Last-Modified:资源的最后修改时间。
  • Etag:资源的唯一标识(一个字符串,类似于人类的指纹)。 服务端拿到 if-Modified-Since 之后拿这个时间去和服务端资源最后修改时间做比较,如果一致则返回 304 ,不一致(也就是资源已经更新了)就返回 200 和新的资源及新的 Last-Modified。

其实 Etag 和 Last-Modified 一样的,只不过 Etag 是服务端对资源按照一定方式(比如 contenthash)计算出来的唯一标识,就像人类指纹一样,传给客户端之后,客户端再传过来时候,服务端会将其与现在的资源计算出来的唯一标识做比较,一致则返回 304,不一致就返回 200 和新的资源及新的 Etag。

发起http请求,先判断缓存是否过期,若没过期,读取缓存(强缓存判断cache-control);已过期,通过协商缓存,判断有lastModify和etag,带着这两个值去服务器请求,服务器根据资源的最后修改事件和etag判断是否返回资源。

三种刷新操作对 http 缓存的影响

  • 正常操作:地址栏输入 url,跳转链接,前进后退等。
  • 手动刷新:f5,点击刷新按钮,右键菜单刷新。
  • 强制刷新:ctrl + f5,shift+command+r。

正常操作:强制缓存有效,协商缓存有效。

手动刷新:强制缓存失效,协商缓存有效。

强制刷新:强制缓存失效,协商缓存失效。

32、跨域解决方案

跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的。

同源策略,是浏览器对 JavaScript 实施的安全限制,只要协议、域名、端口有任何一个不同,都被当作是不同的域。

跨域原理,即是通过各种方式,避开浏览器的安全限制

最初做项目的时候,使用的是jsonp,但存在一些问题,使用get请求不安全,携带数据较小,后来也用过iframe,但只有主域相同才行,也是存在些问题,后来通过了解和学习发现使用代理和proxy代理配合起来使用比较方便,就引导后台按这种方式做下服务器配置,在开发中使用proxy,在服务器上使用nginx代理,这样开发过程中彼此都方便,效率也高;现在h5新特性还有 windows.postMessage()

  • 1、jsonp 只能是get请求

  • 2、cors 跨域资源共享 设置Access-Control-Allow-Origin,带cookie跨域设置

  • 前端请求时在request对象中配置"withCredentials": true

  • 服务端在responseheader中配置"Access-Control-Allow-Origin", "http://xxx:${port}";

  • 服务端在responseheader中配置"Access-Control-Allow-Credentials", "true"

  • response.setHeader("Access-Control-Allow-Methods", "POST,OPTIONS,GET")

  • response.setHeader("Access-Control-Max-Age", "3600");

  • response.setHeader("Access-Control-Allow-Headers", "accept,x-requested-with,Content-Type");

  • 3、websocket 不受同源策略的限制,最大的特点是服务器可以主动向客户端推送消息,客户端也可以主动向服务器发送信息,建立再tcp协议上,协议表示是ws

  • 4、postMessage

a.html 
<iframe src="domain2.com/b.html"></iframe>
iframe.contentwindow.postMessage(data,url)
b.html
window.addEventListener('message',function(e){e.data})
  • 5、hash
window.hasChange() {
let data = location.hash
}

6、浏览器标签间通信

标签1、localStorage.setItem('name','lily')

标签2、window.addeventListner('storage',function(){})

信息存在cookie中,隔一段时间读取cookie

33、webpack原理,性能优化等

webpack是一个模块打包器,将根据文件的依赖关系进行静态分析,然后将这些模块按照指定规则生成静态资源。webpack通过识别入口文件,递归构建一个关系依赖图,将所有模块打包成一个或多个bundle包。

webpack的作用:

  • 模块打包。可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。利用打包我们就可以在开发的时候根据我们自己的业务自由划分文件模块,保证项目结构的清晰和可读性。
  • 编译兼容。在前端的“上古时期”,手写一堆浏览器兼容代码一直是令前端工程师头皮发麻的事情,而在今天这个问题被大大的弱化了,通过webpackLoader机制,不仅仅可以帮助我们对代码做polyfill,还可以编译转换诸如.less, .vue, .jsx这类在浏览器无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提高开发效率。
  • 能力扩展。通过webpackPlugin机制,我们在实现模块化打包和编译兼容的基础上,可以进一步实现诸如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。

打包过程: Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  • 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  • 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  • 确定入口:根据配置中的 entry 找出所有的入口文件
  • 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  • 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

其中文件的解析与构建是一个比较复杂的过程,在webpack源码中主要依赖于compilercompilation两个核心对象实现。

Module和Chunk的区别

Module模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。 Chunk代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。

compiler和compilation的区别

compiler 和 compilation 是 webpack 打包构建过程中的核心对象,记录着打包的关键信息,并提供打包流程中对应的钩子供开发者在plugin中使用。

Compiler类的实例,webpack 从开始执行到结束,Compiler只会实例化一次。compiler 对象记录了 webpack 运行环境的所有的信息,插件可以通过它获取到 webpack 的配置信息,如entry、output、module等配置。

Compilation类实例,提供了 webpack 大部分生命周期Hook API供自定义处理时做拓展使用。一个 compilation 对象记录了一次构建到生成资源过程中的信息,它储存了当前的模块资源、编译生成的资源、变化的文件、以及被跟踪依赖的状态信息。

compiler对象是一个全局单例,他负责把控整个webpack打包的构建流程。

compilation对象是每一次构建的上下文对象,它包含了当次构建所需要的所有信息,每次热更新和重新构建,compiler都会重新生成一个新的compilation对象,负责此次更新的构建过程。

什么是sourceMap?

source-map是webpack配置中devtool的一个可选项,包含多种可选值,选择一种 source map格式来增强调试过程,不同的值会明显影响到构建build和重新构建rebuild的速度。sourceMap是一项将编译、打包、压缩后的代码映射回源代码的技术,由于打包压缩后的代码并没有阅读性可言,一旦在开发中报错或者遇到问题,直接在混淆代码中debug问题会带来非常糟糕的体验,sourceMap可以帮助我们快速定位问题。

Webpack treeshaking原理

将代码中永远不会⾛到的⽚段删除掉。 在production模式下,会自动启动treeshaking。 在dev模式下,需要配置

optimization: {
    minimize: true,
    usedExports: true,
    sideEffects: true
  }

或者可以通过在启动webpack时追加参数 --optimize-minimize optimization.usedExports来实现。 原理: Tree-shaking 的实现一是先标记出模块导出值中哪些没有被用过,二是使用 Terser 删掉这些没被用到的导出语句。标记过程大致可划分为三个步骤:

  • Make 阶段,收集模块导出变量并记录到模块依赖关系图 ModuleGraph 变量中
  • Seal 阶段,遍历 ModuleGraph 标记模块导出变量有没有被使用
  • 生成产物时,若变量没有被其它模块使用则删除对应的导出语句

标记功能需要配置 optimization.usedExports = true 开启 也就是说,标记的效果就是删除没有被其它模块使用的导出语句

Babel 原理

Babel 是现代 JavaScript 语法转换器 Babel 大概分为三大部分:

  • 解析: 将代码(其实就是字符串)转换成 AST( 抽象语法树)
  • 转换: 访问 AST 的节点进行变换操作生成新的 AST
  • 生成: 以新的 AST 为基础生成代码

Webpack 的热更新原理

Webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。

HMR的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS(webpack-dev-server) 与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。

后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loadervue-loader 都是借助这些 API 实现 HMR。

使用webpack开发时,你用过哪些可以提高效率的插件?

  • webpack-dashboard:可以更友好的展示相关打包信息。
  • webpack-merge:提取公共配置,减少重复配置代码
  • speed-measure-webpack-plugin:简称 SMP,分析出 Webpack 打包过程中 Loader 和 Plugin 的耗时,有助于找到构建过程中的性能瓶颈。
  • size-plugin:监控资源体积变化,尽早发现问题
  • HotModuleReplacementPlugin:模块热替换
  • webpack-bundle-analyzer 生成 bundle 的模块组成图,显示所占体积

是否写过Loader?简单描述一下编写loader的思路?

webpack loader 的原理很简单,输入是文件的原始内容,返回的是经过 loader 处理后的内容。对于 md-loader,输入的是 .md 文档,输出的则是一个 Vue SFC 格式的字符串,这样它的输出就可以作为下一个 vue-loader 的输入做处理了。

我们先来回忆一下loader的编写及使用方法:

loader从本质上来说其实就是一个node模块。相当于一个翻译官(loader)将相关类型的文件代码(code)给它。根据我们设置的规则,经过它的一系列加工翻译后还给我们加工好的代码(code)。

loader编写原则

  • 单一原则: 每个 Loader 只做一件事
  • 链式调用: Webpack 会按顺序链式调用每个 Loader
  • 统一原则: 遵循 Webpack 制定的设计规则和结构,输入与输出均为字符串,各个 Loader 完全独立,即插即用。

loader是一个导出函数的JS模块,函数的入参和出参可以理解为文件流(String或Buffer类),函数对传入的文件流进行处理,然后返回处理后的新文件流,下面是一个简单的loader,它实现的功能是把代码中的console.log去掉。

module.exports = function(source,map){
   return source.replace(/console\.log\(.*\);?\n/g, '');
}

没有自己写过loader,但是我们在做动态表单文档编写时,参考了elementui源码,发现他们的文档编写用了mdLoader,研究了一下这个loader的编写方式,并且把这个loader应用到了我们的项目中。这个mdloader的作用是把md文档转成了vue文件。

利用mark-down-it这个插件把md文件中的非代码片段转成html模板,把代码片段生成代码块,复制一份代码块。一份是需要渲染的代码,一份代码挡在pre中跳过编译,放入html模板中。需要渲染的代码提取出script代码和template代码(放入render函数中)

是否写过Plugin?简单描述一下编写plugin的思路?

如果说Loader负责文件转换,那么Plugin便是负责功能扩展。LoaderPlugin作为Webpack的两个重要组成部分,承担着两部分不同的职责。webpack基于发布订阅模式,在运行的生命周期中会广播出许多事件,插件通过监听这些事件,就可以在特定的阶段执行自己的插件任务,从而实现自己想要的功能。既然基于发布订阅模式,那么知道Webpack到底提供了哪些事件钩子供插件开发者使用是非常重要的,上文提到过compilercompilationWebpack两个非常核心的对象,其中compiler暴露了和 Webpack整个生命周期相关的钩子([compiler-hooks],而compilation则暴露了与模块和依赖有关的粒度更小的事件钩子([Compilation Hooks])。Webpack的事件机制基于webpack自己实现的一套Tapable事件流方案。

Plugin的开发和开发Loader一样,需要遵循一些开发上的规范和原则:

  • 插件必须是一个函数或者是一个包含 apply 方法的对象,这样才能访问compiler实例;
  • 传给每个插件的 compilercompilation 对象都是同一个引用,若在一个插件中修改了它们身上的属性,会影响后面的插件;
  • 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住; 了解了以上这些内容,想要开发一个 Webpack Plugin,其实也并不困难。
class MyPlugin {
  apply (compiler) {
    // 找到合适的事件钩子,实现自己的插件功能
    compiler.hooks.emit.tap('MyPlugin', compilation => {
        // compilation: 当前打包构建流程的上下文
        console.log(compilation);
        
        // do something...
    })
  }
}

34、前端性能优化

网络优化

DNS预解析

link标签的rel属性设置dns-prefetch,提前获取域名对应的IP地址

使用缓存

减轻服务端压力,快速得到数据(强缓存和协商缓存可以看这里[1])

使用 CDN(内容分发网络)

用户与服务器的物理距离对响应时间也有影响。

内容分发网络(CDN)是一组分散在不同地理位置的 web 服务器,用来给用户更高效地发送内容。典型地,选择用来发送内容的服务器是基于网络距离的衡量标准的。例如:选跳数(hop)最少的或者响应时间最快的服务器。

压缩响应

压缩组件通过减少 HTTP 请求产生的响应包的大小,从而降低传输时间的方式来提高性能。从 HTTP1.1 开始,Web 客户端可以通过 HTTP 请求中的 Accept-Encoding 头来标识对压缩的支持(这个请求头会列出一系列的压缩方法)

如果 Web 服务器看到请求中的这个头,就会使用客户端列出的方法中的一种来压缩响应。Web 服务器通过响应中的 Content-Encoding 头来告知 Web 客户端使用哪种方法进行的压缩

目前许多网站通常会压缩 HTML 文档,脚本和样式表的压缩也是值得的(包括 XML 和 JSON 在内的任何文本响应理论上都值得被压缩)。但是,图片和 PDF 文件不应该被压缩,因为它们本来已经被压缩了。

使用多个域名

Chrome 等现代化浏览器,都会有同域名限制并发下载数的情况,不同的浏览器及版本都不一样,使用不同的域名可以最大化下载线程,但注意保持在 2~4 个域名内,以避免 DNS 查询损耗。

避免图片src为空

虽然 src 属性为空字符串,但浏览器仍然会向服务器发起一个 HTTP 请求:

IE 向页面所在的目录发送请求;Safari、Chrome、Firefox 向页面本身发送请求;Opera 不执行任何操作。

代码层面:

  • 防抖和节流(resize,scroll,input)。
  • 减少回流(重排)和重绘。
  • 事件委托 (把原本需要绑定在子元素的响应事件(click、keydown…)委托给父元素,让父元素担当事件监听的职务。原理时DOM元素的事件冒泡(先触发子元素再触发父元素)。可以大量节省内存占用,减少事件注册,可以实现当新增子对象时无需再次对其绑定。事件捕获(先触发父元素再触发子元素) addEventListener默认时false,代表事件冒泡阶段触发回调函数)
  • css 放 ,js 脚本放 最底部。
  • 减少 DOM 操作。
  • 按需加载,通常需要与 webpack 中的 require.ensure() 配合,也可以直接用vue动态路由组件。

vue代码层面:

1、代码模块化,组件化,避免重复冗余代

2、for循环设置key值,在用v-for进行数据遍历渲染的时候,为每一项都设置唯一的key值,为了让Vue内部核心代码能更快地找到该条数据,当旧值和新值去对比的时候,可以更快的定位到diff。

3、Vue路由设置成懒加载,当首屏渲染的时候,能够加快渲染速度。

4、更加理解Vue的生命周期,不要造成内部泄漏,使用过后的全局变量在组件销毁后重新置为null。

5、可以使用keep-alive,keep-alive是Vue提供的一个比较抽象的组件,用来对组件进行缓存,从而节省性能。

构建方面: webpack性能优化 安装一个speed-measure-webpack-plugin插件来检测plugin和loader中的执行时间 1.减少文件搜索范围

  • 我们可以通过 excludeinclude 配置来确保转译尽可能少的文件。顾名思义,exclude 指定要排除的文件,include 指定要包含的文件。exclude 的优先级高于 include,在 includeexclude 中使用绝对路径数组,尽量避免 exclude,更倾向于使用 include
  • 设置 resolve.alias字段,避免打包时如果使用相对路径访问或着 import文件时会层层去查找解析文件
resolve: {  alias: { '@': resolve('src')  }}
  • 合理配置 extensions扩展名,resolve.extensions能够自动解析确定的扩展,但是如果extensions扩展名过多,会导致解析过程过多,所以我们要合理配置扩展名,不要过多配置扩展名,项目引用多的文件,扩展名放在前面,我司项目中多的是vuejs文件,可以只引用这两种。

2.抽离公共代码 对于多页应用来说的,如果多个页面引入了一些公共模块,那么可以把这些公共的模块抽离出来,单独打包。公共代码只需要下载一次就缓存起来了,避免了重复下载。抽离公共代码对于单页应用和多页应该在配置上没有什么区别,都是配置在 optimization.splitChunks 中。

3、使用externals 忽略某些包,然后通过cdn引入

如果cdn中没有公共资源库,则需要使用DllPluginDllReferencePlugin来进行优化,这两个插件的目的就是将不经常变更的包(比如vuevuexvue-router)事先打包出来引入到项目中,从而每次只需要打包真正的项目代码,提升了打包速度。 配置方法:

第一步:配置webpack.dll.js

首先要声明dll的配置文件webpack.dll.js,这个配置文件的作用就是将所有依赖包整合成一个文件,entry配置为venders: ["vue","vuex","axios"],它表示会将vuevuexaxios这些依赖包整合成一个venders.js

用的是DllPlugin这个插件,它已经集成到webpack中,所以不用安装,它会生成两个文件,一个是包含依赖项的venders.js,还有表示依赖映射关系的manifest.json文件,它包含依赖包名以及对应存储路径,可以通过它知道venders.js里有哪些依赖包,而webpack可以通过这个文件知道哪些依赖包不参与打包。

/**
 * 利用dll技术,对包进行单独打包
 */
const { resolve } = require("path");
const webpack = require("webpack");
module.exports = {
  mode: "production",
  entry: {
    venders: ["vue","vuex","axios"]
  },
  output: {
    filename: "[name].js",
    path: resolve(__dirname, "dll"),
    library: "[name]_[hash]" //打包出来的库对外暴露出来的内容叫什么名字
  },
  plugins: [
    //打包生成一个manifest.json --> 提供映射关系
    new webpack.DllPlugin({
      name: "[name]_[hash]", //映射库的暴露的内容名称
      path: resolve(__dirname, "dll/manifest.json") //输出文件路径
    })
  ]
};

第二步:配置dll命令

package.json里面配置dll的命令,执行npm run dll命令,会生成venders.js

"scripts": {
  "dll": "webpack --config webpack.dll.js",
}

venders.js是什么呢?它是根据webpack.dll.js里的entrykey值为文件名来生成的,它的值是一个数组,数组里面是打包的npm包的列表,就是把需要的包打包到venders.js中。

第三步:配置DllReferencePlugin

webpack.config.js里配置DllReferencePlugin,这个包已经默认集成到webpack里,所以不要安装,它的作用就是根据manifest.json的依赖关系告诉哪些库不参与打包,从而提升打包的性能。

还有个AddAssetHtmlWebpackPlugin插件需要配置,这个需要执行npm i add-asset-html-webpack-plugin -D进行安装,它的作用就是将生成的venders.js插入到html中。

plugins: [
  //告诉webpack哪些库不参与打包,同时使用的名称也得变
  new webpack.DllReferencePlugin({
    manifest: path.resolve(__dirname, "dll/manifest.json")
  }),
  new AddAssetHtmlWebpackPlugin([    {      filepath: path.resolve(__dirname, "dll/venders.js"),      publicPath: "./"    }  ])
]

4、babel 配置的优化 在不配置 @babel/plugin-transform-runtime 时,babel 会使用很小的辅助函数来实现类似 _createClass 等公共方法。默认情况下,它将被注入(inject)到需要它的每个文件中。但是这样的结果就是导致构建出来的JS体积变大。 我们也并不需要在每个 js 中注入辅助函数,因此我们可以使用 @babel/plugin-transform-runtime@babel/plugin-transform-runtime 是一个可以重复使用 Babel 注入的帮助程序,以节省代码大小的插件。 因此我们可以在 .babelrc 中增加 @babel/plugin-transform-runtime 的配置。

5、js的压缩。可以用插件uglifyjs-webpack-plugin

6、compression-webpack-plugin 开启gzip

7、hard-source-webpack-plugin 为模块提供中间缓存步骤。为了查看结果,您需要使用此插件运行webpack两次:第一次构建将花费正常的时间。第二次构建将显着加快(大概提升90%的构建速度)

8、happypack多进程打包没有试验过(已经是2.0的配置了) 把打包构建任务分解给多个子进程去并发执行,子进程处理完成后再把结果发给主进程,将rules中的loader配置在new HappyPack中。如果项目不复杂,不要配置,进程的分配和管理也需要时间

9、thread-loader多进程打包

需要在 options 字段下配置 workers 也就是进程的个数。注意 thread-loader 并不是开的进程数越多就越好,假如你的 js 代码量很少,开多核反而会降低性能,这是因为打开进程有比较大的开销(600 ms 左右),进程间通信也有开销。 配置方法如下:

            {
                rules:
                    [
                        {
                            test: /.(js|jsx)$/,
                            exclude: /(node_modules|bower_components|dist)/,
                            use: [
                                /**
                                 * 开启多进程打包,打开进程一般 600 ms,
                                 * 通信也有开销。
                                 */
                                {
                                     loader: "thread-loader",
                                     options: {
                                         workers: 3
                                     }
                                },
                                {
                                    loader: "babel-loader",
                                    options: {
                                        cacheDirectory: true
                                    }
                                }

                            ]
                        }
                   ]
              }

其它:

  • 使用服务端渲染。
  • 图片压缩,雪碧图,fileloader把小图转成base64。
  • 使用 http 缓存,比如服务端的响应中添加 Cache-Control / Expires 。
  • CDN加速
  • 使用Preload和 Prefetch preload 使用 preload 可以对当前页面所需的脚本、样式等资源进行预加载,而无需等到解析到 script 和 link 标签时才进行加载。让浏览器提前加载指定资源(加载后并不执行),在需要执行的时候再执行。 将加载和执行分离开,可不阻塞渲染和 document 的 onload 事件。提前加载指定资源,不再出现依赖的font字体隔了一段时间才刷出

使用方式:将 link 标签的 rel 属性的值设为 preload,as 属性的值为资源类型(如脚本为 script,样式表为 style)。

prefetchpreload 一样,都是对资源进行预加载,但是 prefetch 一般预加载的是其他页面会用到的资源。prefetch 是预测会加载指定资源,如在我们的场景中,我们在页面加载后会初始化首屏组件,当用户滚动页面时,会拉取第二屏的组件,若能预测用户行为,则可以 prefetch 下一屏的组件。

当然,prefetch 不会像 preload 一样,在页面渲染的时候加载资源,而是利用浏览器空闲时间来下载。当进入下一页面,就可直接从 disk cache 里面取,既不影响当前页面的渲染,又提高了其他页面加载渲染的速度。

使用方式: 同 preload 类似,无需指定 as 属性。

性能优化指标:

FP

First Paint 首次绘制,指浏览器从开始请求网站内容(导航阶段)到首次向屏幕绘制像素点的时间,刚到 Painting 阶段(如下图所示),所以 FP 也可以理解为是白屏时间。 window.performance.getEntriesByType('paint')[0].startTime

FCP(First Contentful Paint)

衡量页面开始加载到页面中第一个元素被渲染之间的时间。元素包含文本、图片、canvas等

window.performance.getEntriesByType('paint')[1].startTime

FCP 时间如果在 0-1.8 秒之间就是一个比较优秀的指标;1.8-3.0 秒之间就比较中等了,稍微有点儿慢;超过 3 秒就是非常慢了。

LCP(Largest Contentful Paint)

衡量标准视口内可见的最大内容元素的渲染时间。元素包括img、video、div及其他块级元素。

LCP 时间如果在 0-2.5 秒之间就是一个比较优秀的指标;2.5-4 秒之间就比较中等了,稍微有点儿慢;超过 4 秒就是非常慢了。

FID(First Input Delay)

测量从用户第一次与页面交互的时间到浏览器实际上能够响应这种交互的时间。交互包括用户点击一个链接或者一个按钮等。

TTI(Time to Interactive)

测量页面所有资源加载成功并能够可靠地快速响应用户输入的时间。 可交互时间,该指标用于测量页面从开始加载到主要子资源完成渲染,并能够快速、可靠地响应用户输入所需的时间。简单的讲,TTI 是安静窗口之前最后一个长任务(超过 50 毫秒的任务)的结束时间(如果没有找到长任务,则与 FCP 值相同)。

TTI 时间如果在 0-3.8 秒之间就是一个比较优秀的指标;3.9-7.3 秒之间就比较中等了,稍微有点儿慢;超过 7.3 秒就是非常慢了。

CLS

Cumulative Layout Shift,累积布局偏移,测量整个页面生命周期内发生的所有意外布局偏移中最大一连串的布局偏移分数。

布局偏移分数 = 影响分数 * 距离分数

上面的这样的描述可能有点儿抽象,简单的讲就是页面因为一些动态改变的 DOM 或者一些异步的资源加载,导致页面元素发生了位移,这样就会让用户找不到先前阅读的位置或者点击到不期望点击的地方。

指标的测量方法

Chrome Performance

Chrome 的 Performance 页签算是最基础的性能测试工具了,但是其只是给出了部分指标,像 FIDCLS 这样的核心指标无法在此获得。

Lighthouse

Lighthouse 是一个 Chrome 插件,可以帮助我们很好地测量页面的性能指标,顺手测了一下百度的指标,如下图所示:

35、手写防抖、节流

防抖:像弹簧按下去,只有弹簧弹起时才执行操作

function debounce(fn, delay) {
    let timer = null
    return function() {
        if (timer) {
          timer = null
        }
        timer = setTime(() => {
            fn.apply(this, arguments)
            timer = null
        }, delay)
    }
}

节流:像自来水,隔一段事件才防水

function throttle(fn, delay) {
    let timer = null
    return function() {
        if (timer) {
            return
        }
        timer = setTimeout(() => {
            fn.apply(this,arguments)
            timer = null
        },delay)
    }
}

36、数组去重、快排、数组扁平化

function noRepeat(arr) {
    return Array.form(new Set(arr))
}
function noRepeat(arr) {
    let result = []
    let obj = {}
    for (let i =0;i<arr.length;i++) {
        if (!obj[arr[i]]) {
            result.push(arr[i])
            obj[arr[i]] = 1
        }
    }
    return result
}

快速排序:o(nlogn) o(logn) 思路:找到中间值;遍历数组,比中间值大的放左侧数组,比中间值小的放右侧数组。通过递归调用,左右两侧都重复排序

function quickSort(elements){
 2     if (elements.length <= 1 ){
 3         return elements;
 4     }
 5     var mid = Math.floor(elements.length / 2);
 6     var val = elements[mid];
 7 
 8     var left = [];
 9     var right = [];
10     for (let i = 0; i <elements.length; i++){
11         if (elements[i] > val){
12             right.push(elements[i]);
13         }else if(element[i] < val){
14             left.push(elements[i]);
15         }
16     }
17     // concat() 方法用于连接两个或多个数组。此方法返回一个新数组,不改变原来的数组。
18     // arrayObject.concat(array1,array2,...,arrayN)
19     return quickSort(left).conact([val], quickSort(right));
20 }

冒泡排序:o(n2) o(1) 思路:比较相邻的两个元素,如果前一个比后一个大,则交换位置;第一轮循环比较n个数,第二轮循环比较n-1个数,第三轮。。。。外层循环长度n,内层循环长度n-i

function sort(elements){
 2     for (let i = 0; i < elements.length - 1; i++){
 3         for(let j = 0; j < elements.length - 1 - i; j++){
 4             if(elements[j] < elements[j + 1]){
 5                 let temp = elements[j];
 6                 elements[j] = elements[j + 1];
 7                 elements[j + 1] = temp;
 8             }
 9         }
10     }
11 }

数组扁平化:

1、toString转换成字符串,再使用split(',')进行分组,最后把数组每一项转成Number。

2、es6的flat方法,手写实现: arr.flat(Infinity)

function flatten(arr, deep) { 
    if(deep <= 0) return arr;
    return arr.reduce((result, item)=> {
        return result.concat(Array.isArray(item) ? flatten(item, deep - 1) : item);
    }, []);
}

37、手写instanceof

function myInstance(left, right) {
    let proto = left._proto_
    let prototype = right.prototype
    if (proto = null) {
     return false
    } else if (prototype === proto){
        return true
    } else {
      myInstance(proto, prototype)
    }
}

38、js设计模式

设计模式(Design pattern)  

是解决软件开发某些特定问题而提出的一些解决方案也可以理解成解决问题的一些思路。通过设计模式可以帮助我们增强代码的可重用性、可扩充性、 可维护性、灵活性好。我们使用设计模式最终的目的是实现代码的 高内聚 和 低耦合。通俗一点讲的话 打比方面试官经常会问你如何让代码有健壮性。其实把代码中的变与不变分离,确保变化的部分灵活、不变的部分稳定,这样的封装变化就是代码健壮性的关键。而设计模式的出现,就是帮我们写出这样的代码。 设计模式就是解决编程里某类问题的通用模板,总结出来的代码套路就是设计模式。本文章总结下JS在工作中常用的设计模式 ,以帮助大家提高代码性能,增强工作效率!

1、装饰器模式

这种给对象动态地增加职责的方式称为装 饰器(decorator)模式。装饰器模式能够在不改 变对象自身的基础上,在程序运行期间给对象 动态地添加职责。 为了不被已有的业务逻辑干扰,将旧逻辑与新逻辑分离,把旧逻辑抽出去:

var horribleCode = function(){
  console.log(’我是一堆你看不懂的老逻辑')
}
var _horribleCode = horribleCode
horribleCode = function() {
    _horribleCode()
    console.log('我是新的逻辑')
}
horribleCode()

2、工厂模式:

将创建对象的过程单独封装。

function Factory(name, age, favorite) {
    switch(career) {
        case 'fruit':
            return new Vegetarian(name, age) 
            break
        case 'meat':
            return new Carnivore(name, age)
            break
        ...
}

3、单例模式

保证仅有一个实例,并提供一个访问它的全局访问点,这样的模式就叫做单例模式。然后性能得到优化!优点:适用于单一对象,只生成一个对象实例,避免频繁创建和销毁实例,减少内存占用。缺点:不适用动态扩展对象,或需创建多个相似对象的场景。

4、适配器模式

适配器模式主要用来解决两个已有接口之间不匹配的问题,它不考虑这些接口是怎样实 现的,也不考虑它们将来可能会如何演化。适配器模式不需要改变已有的接口,就能够 使它们协同作用。

5、发布订阅模式

发布—订阅模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。

39、小程序相关

**1、生命周期 **

应用生命周期: onLaunch(只触发一次) 小程序第一次加载 onShow 小程序初始化完成 onHide 小程序退到后台

页面生命周期:onLoad 小程序注册完后,加载页面 onShow 页面载入后 onReady 首次显示页面,渲染dom(只触发一次) onHide(页面跳转出去) onUnload(页面卸载 退出页面 wx.redirectTo() 返回上级页面) 组件生命周期:

20201224211641976.png

2、小程序有哪些传递数据的方法

使用全局变量

在 app.js 中的 this.globalData = { } 中放入要存储的数据。 在 组件.js 中, 头部 引入 const app = getApp(); 获取到全局变量 直接使用 app.globalData.key 来进行赋值和获取值。

使用路由

wx.navigateTo 和 wx.redirectTo 时,可以通过在 url 后 拼接 + 变量, 然后在 目标页面 通过在 onLoad 周期中,通过参数来获取传递过来的值。 使用本地缓存

3、你是怎么封装微信小程序的数据请求的?

将所有的接口放在统一的js文件中并导出 在app.js中创建封装请求数据的方法 在子页面中调用封装的请求数据

4、小程序 WXSS 与 CSS 的区别

wxss 背景图片只能引入外链,不能使用本地图片 小程序样式使用 @import 引入 外联样式文件,地址为相对路径。 尺寸单位为 rpx , rpx 是响应式像素,可以根据屏幕宽度进行自适应。

5、小程序的双向绑定和Vue哪里不一样。

小程序 直接使用this.data.key = value 是 不能更新到视图当中的。 必须使用 this.setData({ key :value }) 来更新值。

6、简述微信小程序原理?

  • 小程序本质就是一个单页面应用,所有的页面渲染和事件处理,都在一个页面内进行,但又可以通过微信客户端调用原生的各种接口;
  • 它的架构,是数据驱动的架构模式,它的UI和数据是分离的,所有的页面更新,都需要通过对数据的更改来实现;
  • 它从技术讲和现有的前端开发差不多,采用JavaScript、WXML、WXSS三种技术进行开发;
  • 功能可分为webview和appService两个部分;
  • webview用来展现UI,appService有来处理业务逻辑、数据及接口调用;
  • 两个部分在两个进程中运行,通过系统层JSBridge实现通信,实现UI的渲染、事件的处理等。

微信小程序采用JavaScript、wxml、wxss三种技术进行开发,与现有前端开发的区别:

  • JavaScript的代码是运行在微信APP中的,因此一些h5技术的应用需要微信APP提供对应的API支持;
  • wxml微信自己基于xml语法开发的,因此在开发时只能使用微信提供的现有标签,html的标签是无法使用的;
  • wxss具有css的大部分特性,但并不是所有都支持没有详细文档(wxss的图片引入需使用外链地址,没有body,样式可直接使用import导入)。

微信的架构,是数据驱动的架构模式,它的UI和数据是分离的,所有的页面更新,都需要通过对数据的更改来实现。 小程序功能分为webview和APPservice,webview主要用来展示UI,appservice用来处理业务逻辑、数据及接口调用。它们在两个进程中进行,通过系统层JSBridge实现通信,实现UI的渲染、事件处理。

小程序与H5相比优点是是什么? H5是运行在浏览器中的,浏览器是单线程 性能问题 渲染线程与 JS 引擎线程是互斥的,存在阻塞问题,长时间的 JS 脚本执行直接会影响到 UI 视图的渲染,导致页面卡顿 安全问题 危险的 HTML 标签(a、script)和 JS API(Function、eval、DOM API),攻击者很容易对页面内容进行篡改,非法获取到用户信息等 解决方案

小程序采用了双线程模型,逻辑层和渲染层是分开的,双线程同时运行。渲染层的界面使用 WebView 进行渲染;逻辑层采用 JSCore 运行 JavaScript 代码。- 不用等待浏览器主线程去下载并解析 html,遇到 JS 脚本还会阻塞,影响视图渲染,造成白屏

模型中对 HTML 标签进行了封装,开发者无法直接调用原生标签,JS 中原有的危险逻辑操作 API 能力也被屏蔽,涉及到的通信动作只能通过中间层提供的 API 去触发虚拟 DOM 更新(数据驱动视图)

缺点是双线程如果频繁的通信,操作 setDate 更新视图,对性能消耗特别严重,例如拖拽、滚动等

小程序组件 内置组件、自定义组件、原生组件

小程序的视图是在 WebView 里渲染的,所以底层还是 HTML。原生 HTML 太开放了,直接采用 HTML 作为渲染层语言,会有一些安全问题。因此,微信设计一套组件框架——Exparser。基于这个框架,内置了一套组件,以涵盖小程序的基础功能。

Exparser 是微信小程序的组件组织框架,内置在小程序基础库中,为小程序的各种组件提供基础的支持。小程序内的所有组件,包括内置组件和自定义组件,都由 Exparser 组织管理。

在内置组件中,有一些组件较为特殊,它们并不完全在 Exparser 的渲染体系下,而是由客户端原生参与组件的渲染,这类组件我们称为“原生组件”,这也是小程序 Hybrid 技术的一个应用。

原生组件在WebView这一层的渲染任务是很简单,只需要渲染一个占位元素,之后客户端在这块占位元素之上叠了一层原生界面。因此,原生组件的层级会比所有在WebView层渲染的普通组件要高。

引入原生组件主要有3个好处:

  1. 扩展Web的能力。比如像输入框组件(input, textarea)有更好地控制键盘的能力。
  2. 体验更好,同时也减轻WebView的渲染工作。比如像地图组件(map)这类较复杂的组件,其渲染工作不占用 WebView 线程,而交给更高效的客户端原生处理。
  3. 绕过 setData、数据通信和重渲染流程,使渲染性能更好。比如像画布组件(canvas)可直接用一套丰富的绘图接口进行绘制。

原生组件脱离在 WebView 渲染流程外,这带来了一些限制。最主要的限制是一些 CSS 样式无法应用于原生组件,例如,不能在父级节点使用 overflow:hidden 来裁剪原生组件的显示区域;不能使用 transformrotate 让原生组件产生旋转等。

通信原理 小程序逻辑层和渲染层的通信会由 Native (微信客户端)做中转,逻辑层发送网络请求也经由 Native 转发。WeixinJSBridge 提供了视图层 JS 与 Native、视图层与逻辑层之间消息通信的机制。 首先要知道小程序时运行在基础库之上的,但它们都是压缩打包好的,后面找到反编译出来的基础库代码,其中最重要的就是 WAService.js 和 WAWebview.js,它们分别是视图层和逻辑层的核心实现。

它们之间需要一个桥梁来进行通信,那就是 JS Bridge。JS Bridge 提供调用原生功能的接口(摄像头,定位等),它的核心是构建原生和非原生间消息通信的通道,而且这个通信的通道是双向的。通过 JS Bridge 的发布订阅方法,视图层和逻辑层进行数据通信。

通信流程

  1. wxml 转换成对应的 js 文件,等待生成虚拟dom函数 $gwx 准备完成,使用 dispatchEvent自定义事件 通知 WAWebview。

  1. WAWebview 监听到 generateFuncReady 事件触发,使用 WeixinJSBridge.publish 向逻辑层通信。

  1. 逻辑层处理逻辑,也就是我们平常写的小程序 js 文件里的东西,然后通过 JS Bridge 通知并返回数据给视图层。
  2. 视图层接收到数据,将数据传入生成虚拟dom的函数内,渲染页面,当然小程序也有相应的diff算法。
// 例如在 wxml 中绑定一个动态数据 title,视图层接收到数据后,重新生成虚拟dom
generateFunc({
  title: '小程序接口'
})

  1. 初始化完成后,就会走对应的其他生命周期,或者用户触发事件,数据都会在逻辑层处理完成后通过 JS Bridge 通知到视图层,视图层再次调用生成虚拟dom的函数,更新页面。

##双线程架构的缺陷

  1. 不能灵活操作 DOM,无法实现较为复杂的效果
  2. 部分和原生组件相关的视图有使用限制,如微信的 scrollView 内不能有 textarea
  3. 页面大小、打开页面数量都受到限制
  4. 需要单独开发适配,不能复用现有代码资源
  5. 在 JSCore 中 JS 体积比较大的情况下,其初始化时间会产生影响
  6. 传输数据中,序列化和反序列化耗时需要考虑

小程序框架原理

当你使用 megalompvue 这些 Vue 跨端框架时,看上去,我们写的是vue 的代码,然后打包编译之后就可以运行在小程序内,是不是很神奇?这些框架背后做了哪些事情呢?

实际上,这些Vue的跨端框架 核心原理都差不多,都是把 Vue 框架拿过来强行的改了一波,借助了 vue 的能力。比如说,vue 的编译打包流程(也就是vue-loader的能力), vue 的响应式双向绑定、虚拟dom、diff 算法。上面这些东西跨端框架都没有修改,直接哪来用的。

那么哪些部分是这些跨端框架自己新加的东西呢?

涉及到 Vue 框架中操作DOM节点的代码

这些跨端框架,把原本Vue框架中原生 javascript 操作 DOM 的方法,替换成小程序平台的 setData()

<template> 到 .wxml:框架是采用了 ast 来解析转化模板的

Vue 跨端框架他们拓展了 Vue 的框架,把 Vue2.0 的源码直接拷贝过来,改了里面的初始化方法,在初始化方法中调用了 Page() 方法.在 vue 实例化的时候,会调用 init 方法,在 init 方法里面会调用 Page() 函数,生成一个小程序的 page 实例。

template 模板部分会在编译打包的过程中,被 vue-loader 调用 compile 方法通过词法分析生成一个 ast 对象,然后调用代码生成器,经过遍历 AST 树递归的拼接字符串操作,最终生成一段 render 函数, render函数最后会存在打包生成的dist 文件中。

Render 函数中会调用这些方法创建不同类型的vnode,最终的产物是生成好的虚拟DOM树 vnode tree,对应上面图中 render 函数的下一个阶段 vnode。

虚拟DOM树是对真实DOM树的抽象,树中的节点被称作 vnodevnode 有一个特点, 它保存了这个DOM节点用到了哪些数据 ,这一点非常重要。

Vue拿到 虚拟dom树之后,就可以去和上次老的虚拟dom树patch diff 对比。 这一步的目的是找出,我们应该怎么样改动现存的老的DOM树,代价才最小。

patch 阶段之后,如果是运行在浏览器环境中, vue 实例就会使用真实的原生 javascript 操作DOM的方法(比如说 insertBefore , appendChild 之类的),去操作DOM节点,更新浏览器页面的视图。

端框架替换了 Vue 框架中 **JS 操作DOM 原生节点的 API **为 **setData() **来更新小程序的页面。

7、小程序怎么实现下拉刷新

两种方案: 方案一: 通过在 app.json 中, 将 "enablePullDownRefresh": true, 开启全局下拉刷新。 或者通过在 组件 .json , 将 "enablePullDownRefresh": true, 单组件下拉刷新。

方案二: scroll-view :使用该滚动组件 自定义刷新,通过 bindscrolltoupper 属性, 当滚动到顶部/左边,会触发 scrolltoupper事件,所以我们可以利用这个属性,来实现下拉刷新功能。

8、 bindtap 和 catchtap 区别

相同点: 都是点击事件 不同点: bindtap 不会阻止冒泡, catchtap 可以阻止冒泡。

9、webview中的页面怎么跳回小程序中?

首先,需要在你的html页面中引用一个js文件。 然后为你的按钮标签注册一个点击事件:

$(".kaiqi").click(function(){
        wx.miniProgram.redirectTo({url: '/pages/indexTwo/indexTwo'})
    });

这里的redirectTo跟小程序中的wx.redirectTo()跳转页面是一样的,会关闭当前页跳转到页面。 你也可以替换成navigateTo,跳转页面不会关闭当前页。

10、使用webview直接加载要注意哪些事项?

  • 一、必须要在小程序后台使用管理员添加业务域名;
  • 二、h5页面跳转至小程序的脚本必须是1.3.1以上;
  • 三、微信分享只可以都是小程序的主名称了,如果要自定义分享的内容,需小程序版本在1.7.1以上;
  • 四、h5的支付不可以是微信公众号的appid,必须是小程序的appid,而且用户的openid也必须是用户和小程序的。

11、小程序调用后台接口遇到哪些问题?

数据的大小限制,超过范围会直接导致整个小程序崩溃,除非重启小程序;

小程序不可以直接渲染文章内容这类型的html文本,显示需借助插件

注:插件渲染会导致页面加载变慢,建议在后台对文章内容的html进行过滤,后台直接处理批量替换p标签div标签为view标签。然后其他的标签让插件来做。

uniapp问题(待完善)

40、动态表单架构相关

1、客户端部分:

管理端:

采用了可拖拽式的所见即所得配置面板。这里共分为四个部分,组件面板,拖拽面板,组件属性面板和表单属性配置面板。

组件面板包含一些常用的表单控件:input、select、复选框等等,系统内置了elementUI的组件库。通过一个配置列表componentsConfig遍历就可以,这个列表有一份默认的配置,包含每个基础空间的type、options等信息。拖拽功能是通过vuedraggable组件来实现的。拖拽成功后触发(emit)on-field-add事件,并将拖拽的组件信息传入事件中。

 

拖拽面板是用来维护组件间关系的面板,提供拖拽、排序、删除、复制、预览等功能。在Widget-form组件中实现。on-field-add被触发后,接收到新增的组件信息,整理格式添加到dataList中。格式是type,options、key、model、rules。这时候就生成了jsonShema。在Widget-form中是需要根据生成的jsonSchema显示出组件,遍历dataList,判断type、models等属性,显示出组件。这里只显示没有真正的表单的交互。

 

组件属性面板是用来配置组件的字段名称、宽度、布局、校验规则等属性。在widget-config组件中实现。根据当前的表单控件显示不同的表单属性配置,比如输入框可以配置字段名、标签名、宽度、校验规则等等,下拉框可以配置下拉内容,是否从后端获取,api地址等。这里配置好后,会更新前面说的componentsConfig。

表单属性面板是用来维护标签对齐方式、组件size等全局属性。

渲染端:

GenenrateForm组件来实现的,和Widget-form类似的显示功能就不重复赘述了,嵌套功能实现:通过generateModel函数实现,data.list遍历,value值传入,校验规则注入到渲染的表单控件中。核心功能表单的交互:models值传入表单控件,同时监听每个表单控件value的变化,有变化时,同时更新传入的models,利用语法糖this.$emit(update:name, newName)  :name.sync

提供getData函数,获取校验后的数据.

提供on-field-change函数,监听每个控件变化(控件的value值变化时,触发on-filed-change函数),实现表单间联动。

提供disable、display、hide函数,为每个属性置灰、隐藏

渲染端组件发布到

2、服务端部分:

动态表单系统支持表单保存、查询、项目模块管理。使用koa-generate脚手架创建项目,引入mongoose插件,建立项目列表模型、表单数据模型。实现项目与表单数据的增删改查功能。

Koa引入了哪些中间件:koa-bodyparser 处理返回的数据  Koa logger 打印日志 Koa-router路由中间件

41、nodejs相关面试题

node是一个基于ChromeV8引擎的JavaScript运行环境,是运行在服务端的js,是一个事件驱动、非阻塞式的I/O模型来实现的服务端JavaScript运行环境,基于Google的V8引擎实现的单线程、高性能运行在服务端的JavaScript语言。

1、为什么node是事件驱动模型?

事件驱动模型,形象的说就是你去快餐店点餐,收银员给你点餐后就给你号牌让你去等待取餐,然后服务下一位顾客,而不是等待出餐后再服务别人,这就是非阻塞的模型,你的点餐服务没有阻塞后续顾客的点餐。

node 的webserver接收到请求后,先关闭后放到事件队列中,然后马上处理下一个请求。通过一个循环来检测队列中的事件状态变化,如果检测到有状态变化的事件也就是请求完成,那么就执行该事件对应的处理代码,一般都是回调函数。Node.js 可以轻松处理更多并发客户端请求。事件循环是 Node.js 处理模型的核心。

这个模型非常高效可扩展性非常强,因为 webserver 一直接受请求而不等待任何读写操作。(这也称之为非阻塞式IO或者事件驱动IO)

在事件驱动模型中,会生成一个主循环来监听事件,当检测到事件时触发回调函数。

整个事件驱动的流程有点类似于观察者模式,事件相当于一个主题(Subject),而所有注册到这个事件上的处理函数相当于观察者(Observer)。

Node.js 有多个内置的事件,我们可以通过引入 events 模块,并通过实例化 EventEmitter 类来绑定和监听事件。

// 引入 events 模块
var events = require('events');
// 创建 eventEmitter 对象
var eventEmitter = new events.EventEmitter();
// 绑定事件及事件的处理程序
eventEmitter.on('eventName', eventHandler);
// 触发 connection 事件 
eventEmitter.emit('connection');

Node.js 事件循环

Node.js 是单进程单线程应用程序,但是因为 V8 引擎提供的异步执行回调接口,通过这些接口可以处理大量的并发,所以性能非常高。Node.js 几乎每一个 API 都是支持回调函数的。 Node.js 基本上所有的事件机制都是用设计模式中观察者模式实现。 Node.js 单线程类似进入一个while(true)的事件循环,直到没有事件观察者退出,每个异步事件都生成一个事件观察者,如果有事件发生就调用该回调函数。

1、每个Node.js进程只有一个主线程在执行程序代码,形成一个执行栈(execution context stack)。
2、主线程之外,还维护了一个"事件队列"(Event queue)。当用户的网络请求或者其它的异步操作到来时,node都会把它放到Event Queue之中,此时并不会立即执行它,代码也不会被阻塞,继续往下走,直到主线程代码执行完毕。
3、主线程代码执行完毕完成后,然后通过Event Loop,也就是事件循环机制,开始到Event Queue的开头取出第一个事件,从线程池中分配一个线程去执行这个事件,接下来继续取出第二个事件,再从线程池中分配一个线程去执行,然后第三个,第四个。主线程不断的检查事件队列中是否有未执行的事件,直到事件队列中所有事件都执行完了,此后每当有新的事件加入到事件队列中,都会通知主线程按顺序取出交EventLoop处理。当有事件执行完毕后,会通知主线程,主线程执行回调,线程归还给线程池。
4、主线程不断重复上面的第三步。

我们所看到的node.js单线程只是一个js主线程,本质上的异步操作还是由线程池完成的,node将所有的阻塞操作都交给了内部的线程池去实现,本身只负责不断的往返调度,并没有进行真正的I/O操作,从而实现异步非阻塞I/O,这便是node单线程和事件驱动的精髓之处了。

2、异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

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

3、为什么要用nodejs?node的优缺点

1、非阻塞式I/O,在较慢的网络环境中,可以分块传输数据,事件驱动,擅长高并发访问。nodejs利用单线程模型省去了系统维护和切换多进(线)程的开销,同时多路复用的I/O模型可以让nodejs的单线程不会阻塞在某一个连接上。在高并发场景下,nodejs应用只需要创建和管理多个客户端连接对应的socket描述符而不需要创建对应的进程或线程,系统开销上大大减少,所以能同时处理更多的客户端连接。

2、nodejs并不能提升底层真正I/O操作的效率。如果底层I/O成为系统的性能瓶颈,nodejs依然无法解决,即nodejs可以接收高并发请求,但如果需要处理大量慢I/O操作(比如读写磁盘),仍可能造成系统资源过载。所以高并发并不能简单的通过单线程 + 非阻塞I/O模型来解决

3、CPU密集型应用可能会让nodejs的单线程模型成为性能瓶颈 nodejs适合高并发处理少量业务逻辑或快I/O(比如读写内存)。无法利用CPU的多核,同时代码抛出异常使得程序停止。

4、js不是完全的面向对象(封装、继承、多态,易维护、可扩展、可复用)编程,是弱类型语言只有number和string类型

4、为什么单线程却能够支持高并发

2、nodejs所谓的单线程,只是主线程是单线程,所有的网络请求或者异步任务都交给了内部的线程池去实现,本身只负责不断的往返调度,由事件循环不断驱动事件执行。

3、Nodejs之所以单线程可以处理高并发的原因,得益于libuv层的事件循环机制,和底层线程池实现。

4、Event loop就是主线程从主线程的事件队列里面不停循环的读取事件,驱动了所有的异步回调函数的执行,Event loop总共7个阶段,每个阶段都有一个任务队列,当所有阶段被顺序执行一次后,event loop 完成了一个 tick。

4、Node. js有哪些全局对象?

global、 process, console、 module和 exports。

5、EventEmitter 做了什么?

Node.js 中任何对象发出的事件都是 EventEmitter 类的实例,就像 http 模块。 所有 EventEmitter 类都可以使用 eventEmitter.on() 函数将事件侦听器附加到事件。然后一旦捕捉到这样的事件,就会同步地逐个调用它的侦听器。

6、restful风格接口

它是一种特殊风格的接口,有以下几个特点

url中路径表示资源,路径中不能有动词,例如create、delete、、update

操作资源要与http请求方法对应:新增用户 POST /user 删除用户 DELETE /user/0001 PUT PATCH GET

操作结果要与http响应状态码对应

7、为什么在浏览器中运行的Javascript能与操作系统进行如此底层的交互

Nodejs与操作系统交互,我们在 Javascript中调用的方法,最终都会通过 process.binding 传递到 C/C++ 层面,最终由他们来执行真正的操作。Node.js 即这样与操作系统进行互动。

42、koa、egg、mongodb(看另一篇文章)

43、vue相关

对SAP单页面的理解,及其优缺点:

只有一个页面的应用,仅在 Web 页面初始化时就加载相应的 HTML、JavaScript 和 CSS,不会因为用户的操作去刷新页面,而是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。

优点:交互体验好,页面流畅;前后端分离,后端只需要输出数据和逻辑,减轻服务端压力;

缺点:不利于seo,不能使用浏览器自带的前进后退按钮切换页面,首屏加载过慢(初次加载耗时多),原因是:为了实现单页web应用功能及展示效果,在页面初始化的时候就会将js,css等统一加载,部分页面在需要时加载。

当然也有解决方法。 解决方法:①使用路由懒加载 ②开启Gzip压缩 ③使用webpack的externals属性把不需要的库文件分离出去,减少打包后文件的大小 ④使用vue的服务端渲染(SSR)

生命周期:

vue的生命周期总共分为三个阶段:初始化,运行中,销毁

  • 1、组件通过new vue初始化后,进入beforeCreate生命周期,这个时候数据还没有挂在,只是一个空壳,无法访问到数据和真实的dom,只能挂载数据,绑定事件
  • 2、之后进入created函数,这个时候可以使用数据,更改数据
  • 3、接下来寻找实例或组件对应的模板,编译模板为虚拟DOM放入render函数中准备渲染,然后执行beforeMounted钩子函数,在这个函数中虚拟dom已经创建完成,马上就要渲染,在这里也可以更改数据,不会触发updated。
  • 4、接下来开始render,渲染出真实的dom,然后执行mouted钩子函数,此时数据、真是dom和事件都已经挂载完成了,在这里可以操作真实的dom。
  • 5、当组件或实例数据更改之后,会立即执行beforeUpdate,然后vue的虚拟dom会重新构建虚拟dom与上一次的虚拟dom树,利用diff算法进行对比之后重新渲染。
  • 6、更新完成,执行updated,数据已经更新完成,dom也重新render完成了,可以操作更新后的虚拟dom
  • 7、当通过某种途径调用destory方法后,立即执行beforeDestory,一般在这里做一些善后工作,例如计时器清除,清除非指令绑定事件
  • 8、组件的数据绑定、监听...去掉后只剩下dom空壳,这个时候,执行destory,在这里做善后工作。

MVVM的理解

视图模型双向绑定,是Model-View-ViewModel的缩写,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModelViewModel层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。以前是操作DOM结构更新视图,现在是数据驱动视图

双向绑定原理(实现一个简单的vue):

实现原理依赖订阅者模式。在observer实例种监听属性变化后通知dep(消息订阅器),dep内部维护了一个数组,用来收集订阅者watcher。数据变化触发dep的notify函数,从而触发watcher的updated函数。

在vue中,当执行new vue()时,进入初始化,vue会遍历data中的属性,并用observer实例中的defineProperty实现数据监听。同时vue的指令编译器compiler对元素节点的指令进行解析,初始化视图,并订阅watcher来更新视图,watcher会将自己添加到dep中,此时初始化完毕。当数据发生变化时,observer中的setter方法被触发会立即调用dep.notify(),dep开始遍历所有订阅者,调用订阅者的update方法,对应视图更新。

代码实现:

新建observer构造函数,遍历data中的属性,实例化一个dep,在defineProperty中通过get方法添加订阅者dep.addSub(),在set方法中触发dep.notify()。通知方法会遍历所有订阅者,并触发订阅者(watcher)的update方法,从而实现更新视图。

vue和react对比,选型考虑

Vue和React存在着很多的共同点:

  • -数据驱动视图
  • -组件化思想
  • -都使用 Virtual DOM

不同点:

1、核心思想(特性)

Vue的定位是渐进式框架,灵活易用。利用对数据的劫持/代理,做到了响应数据的变化的快速和精确、灵敏。同时v-model这样的双向绑定的语法糖,也是非常的人性化。

React一开始就是要颠覆传统,推崇函数式编程,单向数据流的特性,需要双向的地方使用 onchange 和 setState 手动的去实现。

2、组件写法

Vue的单文件组件,基于模板+JS+css的模式,来组织代码,与传统的web开发习惯,更加契合;

React 则是 用JSX的方式来组织代码,所谓的 ALL IN JS ,一切都是 JS来控制,体验很大不一样

3、diff算法实现

react依次对新集合进行遍历,for( name in nextChildren)。 通过唯一key来判断老集合中是否存在相同的节点。如果没有的话创建,如果有,则继续比较两个节点的位置index,进行移动操作。 如果节点在老集合中有,新集合中没有,那就删除掉。

vue中旧children和新children各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。 -如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明旧children和新children至少有一个已经遍历完了,就会结束比较。

Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。

diff算法原理实现

什么是Diff算法

上面咱们说了虚拟DOM,也知道了只有虚拟DOM + Diff算法才能真正的提高性能,那讲完虚拟DOM,我们再来讲讲Diff算法吧,还是上面的例子(这张图被压缩的有点小,大家可以打开看,比较清晰):

截屏2021-08-07 下午10.59.31.png

上图中,其实只有一个li标签修改了文本,其他都是不变的,所以没必要所有的节点都要更新,只更新这个li标签就行,Diff算法就是查出这个li标签的算法。

总结:Diff算法是一种对比算法。对比两者是旧虚拟DOM和新虚拟DOM,对比出是哪个虚拟节点更改了,找出这个虚拟节点,并只更新这个虚拟节点所对应的真实节点,而不用更新其他数据没发生改变的节点,实现精准地更新真实DOM,进而提高效率

使用虚拟DOM算法的损耗计算: 总损耗 = 虚拟DOM增删改+(与Diff算法效率有关)真实DOM差异增删改+(较少的节点)排版与重绘

直接操作真实DOM的损耗计算: 总损耗 = 真实DOM完全增删改+(可能较多的节点)排版与重绘

Diff算法的原理

Diff同层对比

新旧虚拟DOM对比的时候,Diff算法比较只会在同层级进行, 不会跨层级比较。 所以Diff算法是:深度优先算法。 时间复杂度:O(n)

截屏2021-08-08 上午11.32.47.png

Diff对比流程

当数据改变时,会触发setter,并且通过Dep.notify去通知所有订阅者Watcher,订阅者们就会调用patch方法,给真实DOM打补丁,更新相应的视图。对于这一步不太了解的可以看一下我之前写Vue源码系列

newVnode和oldVnode:同层的新旧虚拟节点 截屏2021-08-08 上午11.49.38.png

patch方法

这个方法作用就是,对比当前同层的虚拟节点是否为同一种类型的标签(同一类型的标准,下面会讲)

  • 是:继续执行patchVnode方法进行深层比对
  • 否:没必要比对了,直接整个节点替换成新虚拟节点

来看看patch的核心原理代码

function patch(oldVnode, newVnode) {
  // 比较是否为一个类型的节点
  if (sameVnode(oldVnode, newVnode)) {
    // 是:继续进行深层比较
    patchVnode(oldVnode, newVnode)
  } else {
    // 否
    const oldEl = oldVnode.el // 旧虚拟节点的真实DOM节点
    const parentEle = api.parentNode(oldEl) // 获取父节点
    createEle(newVnode) // 创建新虚拟节点对应的真实DOM节点
    if (parentEle !== null) {
      api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
      api.removeChild(parentEle, oldVnode.el)  // 移除以前的旧元素节点
      // 设置null,释放内存
      oldVnode = null
    }
  }

  return newVnode
}

sameVnode方法

patch关键的一步就是sameVnode方法判断是否为同一类型节点,那问题来了,怎么才算是同一类型节点呢?这个类型的标准是什么呢?

咱们来看看sameVnode方法的核心原理代码,就一目了然了

function sameVnode(oldVnode, newVnode) {
  return (
    oldVnode.key === newVnode.key && // key值是否一样
    oldVnode.tagName === newVnode.tagName && // 标签名是否一样
    oldVnode.isComment === newVnode.isComment && // 是否都为注释节点
    isDef(oldVnode.data) === isDef(newVnode.data) && // 是否都定义了data
    sameInputType(oldVnode, newVnode) // 当标签为input时,type必须是否相同
  )
}

patchVnode方法

这个函数做了以下事情:

  • 找到对应的真实DOM,称为el
  • 判断newVnodeoldVnode是否指向同一个对象,如果是,那么直接return
  • 如果他们都有文本节点并且不相等,那么将el的文本节点设置为newVnode的文本节点。
  • 如果oldVnode有子节点而newVnode没有,则删除el的子节点
  • 如果oldVnode没有子节点而newVnode有,则将newVnode的子节点真实化之后添加到el
  • 如果两者都有子节点,则执行updateChildren函数比较子节点,这一步很重要
function patchVnode(oldVnode, newVnode) {
  const el = newVnode.el = oldVnode.el // 获取真实DOM对象
  // 获取新旧虚拟节点的子节点数组
  const oldCh = oldVnode.children, newCh = newVnode.children
  // 如果新旧虚拟节点是同一个对象,则终止
  if (oldVnode === newVnode) return
  // 如果新旧虚拟节点是文本节点,且文本不一样
  if (oldVnode.text !== null && newVnode.text !== null && oldVnode.text !== newVnode.text) {
    // 则直接将真实DOM中文本更新为新虚拟节点的文本
    api.setTextContent(el, newVnode.text)
  } else {
    // 否则

    if (oldCh && newCh && oldCh !== newCh) {
      // 新旧虚拟节点都有子节点,且子节点不一样

      // 对比子节点,并更新
      updateChildren(el, oldCh, newCh)
    } else if (newCh) {
      // 新虚拟节点有子节点,旧虚拟节点没有

      // 创建新虚拟节点的子节点,并更新到真实DOM上去
      createEle(newVnode)
    } else if (oldCh) {
      // 旧虚拟节点有子节点,新虚拟节点没有

      //直接删除真实DOM里对应的子节点
      api.removeChild(el)
    }
  }
}

其他几个点都很好理解,我们详细来讲一下updateChildren

updateChildren方法

这是patchVnode里最重要的一个方法,新旧虚拟节点的子节点对比,就是发生在updateChildren方法中,接下来就结合一些图来讲,让大家更好理解吧

是怎么样一个对比方法呢?就是首尾指针法

vuex介绍

是一个专为 Vue.js 应用程序开发的状态管理模式, 采用集中式存储管理应用的所有组件的状态,解决多组件数据通信。

使用Vuex的好处:

1、数据的存取一步到位,不需要层层传递

2、数据的流动非常清晰

3、存储在Vuex中的数据都是响应式的

Vuex的作用就是:频繁、大范围的数据共享

  1. State:定义了应用的状态数据
  2. Getter:在 store 中定义“getter”(可以认为是 store 的计算属性),就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来, 且只有当它的依赖值发生了改变才会被重新计算
  3. Mutation:是唯一更改 store 中状态的方法,且必须是同步函数
  4. Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作
  5. Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中

为了处理异步,当你触发一个点击事件时,会通过dispatch来访问actions中的方法,actions中的commit会触发mutations中的方法从而修改state的值,通过getters来把数据反映到视图。

vuex中的 mapState,mapGetters 属于辅助函数,其实就是vuex的一个[语法糖],使代码更简洁更优雅。

1)vuex专做态管理,由一个统一的方法去修改数据,全部变量是可以任意修改的 2)做日志搜集,埋点的时候,有vuex更方便 3)全部变量多了会造成命名污染,vuex不会,同时解决了父组件与孙组件,以及兄弟组件之间通信的问题

vuex中mutation为什么不能处理异步方法

在 vuex 里面 actions 只是一个架构性的概念,这个函数想实现什么同步或者异步看你自己的需求,并不作限制,但是若想改变state的状态,需要通过mutation去触发。vuex 真正限制的只有 mutation 必须是同步的这一点。其实是为了能用 devtools 追踪状态变化,

同步的意义在于每一个 mutation 执行完成后都可以对应到一个新的状态(和 reducer 一样),这样 devtools 就可以打个 snapshot 存下来,然后就可以随便 time-travel 了。如果你开着 devtool 调用一个异步的 action,你可以清楚地看到它所调用的 mutation 是何时被记录下来的,并且可以立刻查看它们对应的状态。

在Pinia中, 将同步异步,统一合并到了action里面,并区分判断同步异步

vueRouter原理:

vue-router 可以通过 mode 参数设置为三种模式:hash 模式、history 模式、abstract 模式。

hash 模式。

默认是 hash 模式,基于浏览器 history api,使用 window.addEventListener("hashchange", callback, false) 对浏览进行监听。当调用 push 时,把新路由添加到浏览器访问历史的栈顶。使用 replace 时,把浏览器访问历史的栈顶路由替换成新路由 hash 的值(等于 url 中 # 及其以后的内容)。浏览器是根据 hash 值的变化,将页面加载到相应的 DOM 位置。锚点变化只是浏览器的行为,每次锚点变化后依然会在浏览器中留下一条历史记录,可以通过浏览器的后退按钮回到上一个位置。

history 模式。基于浏览器 history api,使用 window.onpopstate 对浏览器地址进行监听。对浏览器 history api 中的 pushState()、replaceState() 进行封装,当方法调用,会对浏览器的历史栈进行修改。从而实现 URL 的跳转而无需加载页面,但是它的问题在于当刷新页面的时候会走后端路由,所以需要服务端的辅助来完成,避免 url 无法匹配到资源时能返回页面。

abstract 。不涉及和浏览器地址的相关记录。流程跟 hash 模式一样,通过数组维护模拟浏览器的历史记录栈 服务端下使用。使用一个不依赖于浏览器的浏览器历史虚拟管理后台.

总结: hash 模式和 history 模式都是通过 window.addEventListener() 方法监听 hashchange 和 popState 进行相应路由的操作。可以通过 back、foward、go 等方法访问浏览器的历史记录栈 进行各种跳转。而 abstract 模式是自己维护一个模拟的浏览器历史记录栈的数组。

router和route的区别

router是VueRouter的一个实例对象,通过Vue.use(VueRouter)和VueRouter构造函数得到一个router的实例对象,这个对象中是一个全局的对象,包含了所有的路由包含了许多关键的对象和属性。例如history对象。push和replace方法可以跳转页面。

route是一个跳转的路由对象,每一个路由都会有一个route对象,是一个局部的对象,可以获取对应的name,path,params,query等属性。

resolve => require(['@/components/home'],resolve)e-router路由跳转方式

路由守卫:

是路由跳转前、中、后过程中的一些钩子函数 路由守卫分为三种,全局路由、组件内路由,路由独享。

全局路由钩子函数有:beforeEach(路由即将改变前)、beforeResolve、afterEach(参数中没有next)

组件内路由的钩子函数有:beforeRouterEnter(在路由进入之前,组件实例还未渲染,所以无法获取this实例,只能通过vm来访问组件实例 next(vm => {})

beforeRouteUpdate(同一页面,刷新不同数据时调用)、beforeRouteLeave(离开当前路由页面时调用)

路由独享的钩子函数有:beforeEnter

nextTick原理:

它可以在DOM更新完毕之后执行一个回调

vue是采用异步更新,会循环检测数据变化后统一更新视图。vue里面并不是每次数据改变都会触发更新dom,而是将这些操作都缓存在一个队列,在一个事件循环结束之后,刷新队列,统一执行dom更新操作。

  • nextTickVue提供的一个全局API,是在下次DOM更新循环结束之后执行延迟回调,在修改数据之后使用$nextTick,则可以在回调中获取更新后的DOM
  • Vue在更新DOM时是异步执行的。只要侦听到数据变化,Vue将开启1个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个watcher被多次触发,只会被推入到队列中-次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作是非常重要的。nextTick方法会在队列中加入一个回调函数,确保该函数在前面的dom操作完成后才调用;
  • 比如,我在干什么的时候就会使用nextTick,传一个回调函数进去,在里面执行dom操作即可;
  • 我也有简单了解nextTick实现,它会在callbacks里面加入我们传入的函数,然后用timerFunc异步方式调用它们,首选的异步方式会是Promise。这让我明白了为什么可以在nextTick中看到dom操作结果。

在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后立即使用 nextTick 来获取更新后的 DOM。 nextTick主要使用了宏任务和微任务。 根据执行环境分别尝试采用Promise、MutationObserver、setImmediate,如果以上都不行则采用setTimeout定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。 Vue.nextTick 或者 vm.$nextTick 的原理其实很简单,就做了两件事: 将传递的回调函数用 try catch 包裹然后放入 callbacks 数组 执行 timerFunc 函数,在浏览器的异步任务队列放入一个刷新 callbacks 数组的函数

为什么vue采用异步渲染?Vue 的异步更新机制是如何实现的?

  1. 修改Vue中的Data时,就会触发所有和这个Data相关的Watcher进行更新。\
  2. 首先,会将所有的Watcher加入队列Queue。\
  3. 然后,调用nextTick方法,执行异步任务。\
  4. 在异步任务的回调中,对Queue中的Watcher进行排序,然后执行对应的DOM更新。 Vue 的异步更新机制的核心是利用了浏览器的异步任务队列来实现的,首选微任务队列,宏任务队列次之。

当响应式数据更新后,会调用 dep.notify 方法,通知 dep 中收集的 watcher 去执行 update 方法,watcher.update 将 watcher 自己放入一个 watcher 队列(全局的 queue 数组)。

然后通过 nextTick 方法将一个刷新 watcher 队列的方法(flushSchedulerQueue)放入一个全局的 callbacks 数组中。

如果此时浏览器的异步任务队列中没有一个叫 flushCallbacks 的函数,则执行 timerFunc 函数,将 flushCallbacks 函数放入异步任务队列。如果异步任务队列中已经存在 flushCallbacks 函数,等待其执行完成以后再放入下一个 flushCallbacks 函数。

flushCallbacks 函数负责执行 callbacks 数组中的所有 flushSchedulerQueue 函数。

flushSchedulerQueue 函数负责刷新 watcher 队列,即执行 queue 数组中每一个 watcher 的 run 方法,从而进入更新阶段,比如执行组件更新函数或者执行用户 watch 的回调函数。

Vue初始化过程中(new Vue(options))都做了什么?

在构造函数Vue中调用了this._init(options)方法,触发了initMixin中的_init方法

  • 处理组件配置项
  • 初始化根组件时进行了选项合并操作,将全局配置合并到根组件的局部配置上
  • 初始化每个子组件时做了一些性能优化,将组件配置对象上的一些深层次属性放到 vm.options 选项中,以提高代码的执行效率
  • 初始化组件实例的关系属性,比如 parent、children、root、refs 等
  • 处理自定义事件
  • 调用 beforeCreate 钩子函数
  • 初始化组件的 inject 配置项,得到 ret[key] = val 形式的配置对象,然后对该配置对象进行浅层的响应式处理(只处理了对象第一层数据),并代理每个 key 到 vm 实例上
  • 数据响应式,处理 props、methods、data、computed、watch 等选项
  • 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上
  • 调用 created 钩子函数
  • 如果发现配置项上有 el 选项,则自动调用 mount 方法,也就是说有了 el 选项,就不需要再手动调用 mount 方法,反之,没提供 el 选项则必须调用 mount
  • 接下来则进入挂载阶段

provide和inject

在父子组件传递数据时,通常使用的是 props 和 emit,父传子时,使用的是 props,如果是父组件传孙组件时,就需要先传给子组件,子组件再传给孙组件,如果多个子组件或多个孙组件使用时,就需要传很多次,会很麻烦。

像这种情况,可以使用 provide 和 inject 解决这种问题,不论组件嵌套多深,父组件都可以为所有子组件或孙组件提供数据,父组件使用 provide 提供数据,子组件或孙组件 inject 注入数据。同时兄弟组件之间传值更方便。

Vue3有了解过吗?能说说跟Vue2的区别吗?

  • 1、vue3默认进行懒观察(lazy observation) vue2.0中数据一开始就创建了观察者,数据很大的时候,就会出现问题,vue3中进行了优化 只有用于渲染初始化可见部分的数据,才会创建观察者,效率更高。

  • 2、vue3.0中加入了typeScript以及PWA的支持

  • 3、开发组件区别,compositon Api代替现有的Options API。旧的选项型API在代码里分割了不同的属性(properties):data,computed属性,methods,等等。新的合成型API能让我们用方法(function)来分割,相比于旧的API使用属性来分组,这样代码会更加简便和整洁。 3.1支持碎片化模板,组件可以拥有多个根节点

3.2 vue3引入reactive,使用reactive()方法来声名我们的数据为响应型数据,使用setup()方法来返回我们的响应型数据,从而我们的template可以获取这些响应型数据。

3.3 Vue3 的合成型API里面的setup()方法也是可以用来操控methods的。创建声名方法其实和声名数据状态是一样的。我们需要先声名一个方法然后在setup()方法中返回(return).

3.4 Vue3 生周期钩子不是全局可调用的了,需要另外从vue中引入。和刚刚引入reactive一样,生命周期的挂载钩子叫onMounted。引入后我们就可以在setup()方法里面使用onMounted挂载的钩子了

3.5 Vue3 使用计算属性,我们先需要在组件内引入computed。使用方式就和响应式数据(reactive data)一样,在state中加入一个计算属性。

3.6 在 Vue3 中,this无法直接拿到props属性,emit events(触发事件)和组件内的其他属性。不过全新的setup()方法可以接收两个参数:

1.  `props` - 不可变的组件参数
2.  `context` - Vue3 暴露出来的属性(emit,slots,attrs)

3.7 在setup()中的第二个参数content对象中就有emit,这个是和this.$emit是一样的。那么我们只要在setup()接收第二个参数中使用分解对象法取出emit就可以在setup方法中随意使用了。

3.8 生命周期:steup代替了beforeCreate和created,其他都大致类似

export default {
  props: {
    title: String
  },
  setup (props, { emit }) {
    const state = reactive({
      username: '',
      password: '',
      lowerCaseUsername: computed(() => state.username.toLowerCase())
    })

    onMounted(() => {
      console.log('title: ' + props.title)
    })

    const login = () => {
      emit('login', {
        username: state.username,
        password: state.password
      })
    }

    return { 
      login,
      state
    }
  }
}
</script>
  • 4、## 更精准的变更通知

    2.x 版本中,你使用 Vue.set 来给对象新增一个属性时,这个对象的所有 watcher 都会重新运行; 3.x 版本中,只有依赖那个属性的 watcher 才会重新运行

setUp函数中如何获取路由

1、通过 getCurrentInstance 方法获取当前组件实例,从而获取 route 和 router

2、通过从路由中导入 useRoute 使用 route 和 router

setup语法糖

setup script是 vue3 的一个新的语法糖,用起来特别简单。只需要在 script 标签中加上 setup 关键字。

setup script 中声明的函数、变量以及import引入的内容、组件都能在模板中直接使用

在script setup中必须使用 defineProps和 defineEmitsAPI 来声明 props 和 emits ,它们具备完整的类型推断并且在script setup中是直接可用的

vue3中使用状态管理机制pina

  • 兼容 Vue2 和 Vue3

  • 抛弃传统的 Mutation ,只有 state, getteraction ,简化状态管理库

  • 不需要嵌套 modules ,使用 Composition api

  • TypeScript支持 (不需要自个添加TS包装器)

  • 扁平化设计,没有 命名空间模块。鉴于 Store 的扁平架构,“命名空间” Store 是其定义方式所固有的,也可以说是所有 Store 都是命名空间的

创建 store/user.ts,下面使用 Setup Stores 方式创建一个 Store

import { defineStore } from "pinia"
import { computed, reactive } from "vue"

type userT = string | number

export const useUserStore = defineStore('user', () => {
// state
const count = 1
let userList: userT[] = reactive([])
// getters
const userNum = computed(() => userList.length)
// actions
function addUser(userName:userT) {
 userList.push(userName)
}
function resetUser() {
 userList.length = 0
}

return {
 count,
 userList,
 userNum,
 addUser,
 resetUser
}
})

新建 UserList.vue 组件,在这个组件中使用 state 和 getters、actions:

  import { useUserStore } from '../store/user'
  const userStore = useUserStore()
  <h3>当前有 {{userStore.userNum}} 个用户 </h3> <ul> <li v-for="item in userStore.userList">{{ item }}</li> </ul>
  function handleAddUser() { 
  userStore.addUser('用户') 
  }     
  function handleResetUser() { 
  userStore.resetUser() 
  }

要操作 state 中的值时,上面用到了在 actions 中操作和直接赋值修改 state.count = 10,除了这两种方式以外还可以,使用 $patch 这个API。

  function handlePatch1() {
    store.$patch({
      count: 99,
      text: store.text === '文字' ? 'text' : '文字'
    })
  }
  function handlePatch2() {
  store.$patch(state => {
    if (state.count > 10) {
      state.count--
    } else if(state.count < 5) {
      state.count++
    } else {
      state.count = 15
    }
  })
}

虽然有很多方法能够成功修改 state,但因为是全局的状态管理,所以还是尽量放在 actions 中统一进行修改,尽量不在单独的组件处随意修改。这样能使代码结构能够更加合理,可读性更强。

Vue3.x响应式数据原理

通过Proxy(代理),对对象的属性值进行读写、添加、删除并进行劫持,通过Reflect(反射)动态对被代理的对象属性进行特定的操作

function reactive(target = {}) {
    if (typeof target !== "object" || target == null) {
        // 不是对象或数组,则返回
        return target;
    }

    // proxy的代理配置,单独拿出来写
    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);
            return result; // 是否设置成功
        },
        deleteProperty(target, key) {
            const result = Reflect.deleteProperty(target, key);
            console.log("delete property", key);
            return result; // 是否删除成功
        },
    };

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

Vue3.0 里为什么要用 Proxy API替代 defineProperty API?

1、defineProperty无法监控到数组下标变化。

2、defineProperty无法检测到对象属性的新增或删除,不过可以用Vue.set()来添加新的属性

3、Proxy可以劫持整个data对象,然后递归返回属性的值的代理即可实现响应式,但是它的兼容性不是很好;Object.defineProperty,它只能劫持对象的属性,所以它需要深度遍历data中的每个属性,这种方式对于数组很不友好;

4、proxy性能更好。

vue中组件的data为什么是一个函数?而new Vue 实例里,data 可以直接是一个对象

一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数。

vue中data的属性可以和methods中方法同名吗,为什么?

初始化vm的过程,会先把data绑定到vm,再把computed、methods的值绑定到vm,会把data覆盖了

Vue中computed与watch的区别

computed 多对一,有缓存,不能有异步操作、包括get和set方法,默认走get。 watch 一对多,没缓存,可以有异步操作

虚拟DOM中key的作用(待完善)

用index作为key可能会引发的问题

1、比如对数据列表进行逆序添加/删除,这种破坏顺序操作的时候,会产生没有必要的真实DOM更新(对比发现变动会直接覆盖),虽然界面的显示没有问题,若数据过多,会影响效率低小;

2、如果在列表中存在输入类的表单dom这种,可能就会产生错误DOM更新,界面也会有问题;

vue中对mixins的理解和使用

mixin 项目变得复杂的时候,多个组件间有重复的逻辑就会用到mixin,多个组件有相同的逻辑,抽离出来

mixin并不是完美的解决方案,会有一些问题

vue3提出的Composition API旨在解决这些问题【追求完美是要消耗一定的成本的,如开发成本】

场景:PC端新闻列表和详情页一样的右侧栏目,可以使用mixin进行混合

劣势:

1.变量来源不明确,不利于阅读

2.多mixin可能会造成命名冲突

3.mixin和组件可能出现多对多的关系,使得项目复杂度变高

vue中的插槽

  • 匿名和具名插槽强调的是填充占位的【位置】;<slot name="one"></slot> <template v-slot:two>我是要给two 插槽的信息</template>
  • 作用域插槽强调的则是数据作用的【范围】;
  • 作用域插槽,就是带参数(数据)的插槽,可以从父组件向子组件传参;
<slot :obj="obj">默认信息</slot>
<template v-slot="slotProps">
  <p>{{slotProps.obj.msg}}---{{slotProps.obj.message}}</p>
</template>

Vue中常用的一些指令,vue的自定义指令,你有写过自定义指令吗?自定义指令的应用场景有哪些?(待完善)

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

1.当 v-for 和 v-if 处于同一个节点时,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中。如果要遍历的数组很大,而真正要展示的数据很少时,这将造成很大的性能浪费

2.这种场景建议使用 computed,先对数据进行过滤

Vue.set 改变数组和对象中的属性?vm.$set(obj, key, val) 做了什么?

为对象添加一个新的响应式数据:调用 defineReactive 方法为对象增加响应式数据,然后执行 dep.notify 进行依赖通知,更新视图。

第一次页面加载会触发哪几个钩子?

beforeCreate, created, beforeMount, mounted

Vue组件通信有哪些方式?

props / $emit:

Bus事件总线: 新建一个文件夹bus,里面有个index.js文件,创建一个新的Vue实例,然后导出模块。Vue.prototype.bus = bus`` import bus from ``'./bus/index'全局引入。通过this.bus.on()监听,this.bus.on()监听,this.bus.emit()触发。

children / parent 通过parent和children就可以访问组件的实例,拿到实例代表什么?代表可以访问此组件的所有方法和data。this.$children[0].messageA = 'this is new value;this.$parent.msg;

ref / refs this.$resf.componentA.name 获取子组件属性

provide/ inject 父组件中通过provide来提供变量, 然后再子组件中通过inject来注入变量。不论子组件嵌套有多深, 只要调用了inject 那么就可以注入provide中的数据

keep-alive了解吗?

作用:实现组件缓存,保持这些组件的状态,以避免反复渲染导致的性能问题。

场景:tabs标签页 后台导航,vue性能优化

原理:Vue.js内部将DOM节点抽象成了一个个的VNode节点,keep-alive组件的缓存也是基于VNode节点的而不是直接存储DOM结构。它将满足条件(pruneCache与pruneCache)的组件在cache对象中缓存起来,在需要重新渲染的时候再将vnode节点从cache对象中取出并渲染。 被包含在 keep-alive 中创建的组件,会多出两个生命周期的钩子: activated 与 deactivated

axios 是什么,其特点和常用语法

axios时目前最流行的ajax封装库之一,用于很方便地实现ajax请求的发送。支持的功能:

1、从浏览器发出 XMLHttpRequests请求。

axios({
    method:"POST",
    url:"http://localhost:3000/posts",
    data:{
        title:"axios学习",
        author:"Yehaocong"
    }
    }).then(response=>{
       console.log(response);
    })

2、从 node.js 发出 http 请求

3、支持 Promise API

4、能拦截请求和响应

  • 请求拦截器:用于拦截请求,自定义做一个逻辑后再把请求发送,可以用于配置公用的逻辑,就不用每个请求都配一遍。
  • 响应拦截器:用于拦截响应,做一些处理后再出发响应回调。

5、能转换请求和响应数据

6、取消请求

7、实现JSON数据的自动转换

8、客户端支持防止 XSRF攻击

封装:

const http = axios.create({
    baseURL: "https://www.liulongbin.top:8888/api/private/v1",
    timeout: 6000
})
// 请求拦截
http.interceptors.request.use(config => {
    //请求头设置
    let token = localStorage.getItem('token') || ''
    config.headers.Authorization = token
    return config
}, err => {
    console.log(err);
})
// 响应拦截
http.interceptors.response.use()

对SSR有了解吗,它主要解决什么问题?

假设有项目需要渲染一个首页,平时我们的项目启动后,开始渲染,请求页面,返回的body为空,然后执行js将html结构注入body中,再结合css来渲染样式,展现出来。

而使用了服务端渲染(s-s-r)后,简单理解是将组件或页面通过服务器生成html字符串,再发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序。渲染时请求页面,返回的body里已经存在服务器生成的html结构,之后只需要结合css显示出来。这就节省了访问时间和优化了资源的请求。

使用s-s-r后的优点?

  1. 更利于网站的SEO

  2. 更利于首屏页面的渲染

使用s-s-r后的局限性?

  1. 服务端压力较大

  本来是通过客户端完成渲染,现在统一到服务端node服务去做。尤其是高并发访问的情况,会大量占用服务端CPU资源。

  2. 开发条件受限

  在服务端渲染中,只会执行到componentDidMount之前的生命周期钩子,因此项目引用的第三方的库也不可用其它生命周期钩子,这对引用库的选择产生了很大的限制。

  3. 学习成本相对较高

  除了对本身项目使用的前端框架要熟悉,还需要掌握webpack、node、Koa2等相关技术。相对于客户端渲染,项目构建、部署过程更加复杂。

Vue要做权限管理该怎么做?控制到按钮级别的权限怎么做?

前端权限控制可以分为四个方面:

  • 接口权限:利用axios拦截器,后端接口返回401,做统一处理,跳转到无权限页面
  • 按钮权限: 通过自定义指令来实现。例如定义v-has指令,利用Vue.directive('has', { bind : function(){}}),bind中判断当前按钮所需要的权限,与当前登录用户具有的权限是否匹配。不匹配则删除该dom节点。
  • 菜单权限: 菜单与路由分离,菜单由后端返回前端定义路由信息。每次路由跳转的时候都要判断权限,这里的判断也很简单,因为菜单的name与路由的name是一一对应的,而后端返回的菜单就已经是经过权限过滤的如果根据路由name找不到对应的菜单,就表示用户有没权限访问
  • 路由权限:初始化的时候先挂载不需要权限控制的路由,比如登录页,404等错误页。如果用户通过URL进行强制访问,则会直接进入404,相当于从源头上做了控制登录后,获取用户的权限信息,然后筛选有权限访问的路由,在全局路由守卫里进行调用addRoutes添加路由。
import Vue from 'vue'
 /**权限指令**/
 const has = Vue.directive('has', {
      bindfunction (el, binding, vnode) {
          // 获取页面按钮权限
        let btnPermissionsArr = [];
       if(binding.value){
            // 如果指令传值,获取指令参数,根据指令参数和当前登录人按钮权限做比较。
            btnPermissionsArr = Array.of(binding.value);
        }else{
             // 否则获取路由中的参数,根据路由的btnPermissionsArr和当前登录人按钮权限做比较。
            btnPermissionsArr = vnode.context.$route.meta.btnPermissions;
        }
         if (!Vue.prototype.$_has(btnPermissionsArr)) {
             el.parentNode.removeChild(el);
         }
      }
  });
 // 权限检查方法
  Vue.prototype.$_has = function (value) {
      let isExist = false;
     // 获取用户按钮权限
    let btnPermissionsStr = sessionStorage.getItem("btnPermissions");
      if (btnPermissionsStr == undefined || btnPermissionsStr == null) {
         return false;
      }
     if (value.indexOf(btnPermissionsStr) > -1) {
          isExist = true;
     }
     return isExist;
  };
  export {has}
<el-button @click='editClick' type="primary" v-has>编辑</el-button>

44、项目中遇到的问题

1、上传文件后会把表单其他内容清空:由于在文件组件中传入的value值是引用类型,把传入的props赋值给一个本地变量时,没有深拷贝,导致在改变本地变量时,父组件的值也发生了变化,父组件中有一些逻辑是,当value值修改时,会认为是首次进入表单页面,然后重置整个表单对象的数据,最后改为了深拷贝,就没有出现这个问题。

2、大文件上传和断点续传 与后端配合采用断点续传方式(如何写组件发布到npm上) 核心是利用 Blob.prototype.slice 方法,和数组的 slice 方法相似,文件的 slice 方法可以返回原文件的某个切片

预先定义好单个切片大小,将文件切分为一个个切片,然后借助 http 的可并发性,同时上传多个切片。这样从原本传一个大文件,变成了并发传多个小的文件切片,可以大大减少上传时间

另外由于是并发,传输到服务端的顺序可能会发生变化,因此我们还需要给每个切片记录顺序。服务端负责接受前端传输的切片,并在接收到所有切片后合并所有切片

file.slice(cur, cur + size) 切片后生成多个request

Promise.all(requestList) 多次请求返回成功后,给发端发送合并请求。

在生成文件切片时,需要给每个切片一个标识作为 hash,这里暂时使用文件名 + 下标,这样后端可以知道当前切片是第几个切片,用于之后的合并切片。

显示进度条:xhr.upload.onprogress = onProgress;返回上传进度,loaded/总size。在axios中配置onUploadProgress。

断点续传:断点续传的原理在于前端/服务端需要记住已上传的切片,这样下次上传就可以跳过之前已上传的部分,有两种方案实现记忆的功能

  • 前端使用 localStorage 记录已上传的切片 hash
  • 服务端保存已上传的切片 hash,前端每次上传前向服务端获取已上传的切片

第一种是前端的解决方案,第二种是服务端,而前端方案有一个缺陷,如果换了个浏览器就失去了记忆的效果,所以这里选后者

暂停上传使用xhr.abort.在axios中用cancelToken

export const get = (url, params, headers, abortFunc) => {
	const CancelToken = await axios.CancelToken;
	const res = await axios.get(url,{
	    params,
	    cancelToken: new CancelToken(function executor(c) {
	         // 将executor参数传出去,供外部调用
	         abortFunc(c);
	    })
	}, { headers });
	return res;
}

由于当文件切片上传后,服务端会建立一个文件夹存储所有上传的切片。 前端每次上传前发送一个验证的请求,返回两种结果

  • 服务端已存在该文件,不需要再次上传
  • 服务端不存在该文件或者已上传部分文件切片,通知前端进行上传,并把已上传的文件切片名返回给前端

前端hash值生成

是前端还是服务端,都必须要生成文件和切片的 hash,之前我们使用文件名 + 切片下标作为切片 hash,这样做文件名一旦修改就失去了效果,而事实上只要文件内容不变,hash 就不应该变化,所以正确的做法是根据文件内容生成 hash,所以我们修改一下 hash 的生成规则。

这里用到另一个库 spark-md5,它可以根据文件内容计算出文件的 hash 值

另外考虑到如果上传一个超大文件,读取文件内容计算 hash 是非常耗费时间的,并且会引起 UI 的阻塞,导致页面假死状态,所以我们使用 web-worker 在 worker 线程计算 hash,这样用户仍可以在主界面正常的交互

由于实例化 web-worker 时,参数是一个 js 文件路径且不能跨域,所以我们单独创建一个 hash.js 文件放在 public 目录下,另外在 worker 中也是不允许访问 dom 的,但它提供了importScripts 函数用于导入外部脚本,通过它导入 spark-md5

// /public/hash.js
​
// 导入脚本
self.importScripts("/spark-md5.min.js");
​
// 生成文件 hash
self.onmessage = e => {
  const { fileChunkList } = e.data;
  const spark = new self.SparkMD5.ArrayBuffer();
  let percentage = 0;
  let count = 0;
  const loadNext = index => {
    const reader = new FileReader();
    reader.readAsArrayBuffer(fileChunkList[index].file);
    reader.onload = e => {
      count++;
      spark.append(e.target.result);
      if (count === fileChunkList.length) {
        self.postMessage({
          percentage: 100,
          hash: spark.end()
        });
        self.close();
      } else {
        percentage += 100 / fileChunkList.length;
        self.postMessage({
          percentage
        });
        // calculate recursively
        loadNext(count);
      }
    };
  };
  loadNext(0);
};

在 worker 线程中,接受文件切片 fileChunkList,利用 fileReader 读取每个切片的 ArrayBuffer 并不断传入 spark-md5 中,每计算完一个切片通过 postMessage 向主线程发送一个进度事件,全部完成后将最终的 hash 发送给主线程

spark-md5 文档中要求传入所有切片并算出 hash 值,不能直接将整个文件放入计算,否则即使不同文件也会有相同的 hash

接着编写主线程与 worker 线程通讯的逻辑

+    // 生成文件 hash(web-worker)
+    calculateHash(fileChunkList) {
+      return new Promise(resolve => {
+        // 添加 worker 属性
+        this.container.worker = new Worker("/hash.js");
+        this.container.worker.postMessage({ fileChunkList });
+        this.container.worker.onmessage = e => {
+          const { percentage, hash } = e.data;
+          this.hashPercentage = percentage;
+          if (hash) {
+            resolve(hash);
+          }
+        };
+      });
    },
    async handleUpload() {
      if (!this.container.file) return;
      const fileChunkList = this.createFileChunk(this.container.file);
+     this.container.hash = await this.calculateHash(fileChunkList);
      this.data = fileChunkList.map(({ file },index) => ({
+       fileHash: this.container.hash,
        chunk: file,
        hash: this.container.file.name + "-" + index,
        percentage:0
      }));
      await this.uploadChunks();
    }   

Worker 主线程使用 postMessage 给 worker 线程传入所有切片 fileChunkList,并监听 worker 线程发出的 postMessage 事件拿到文件 hash。就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。

3、面对奇葩需求的解决方案 不同openId小程序之间跳转,由于小程序冷启动问题和热启动问题,无法实现根据不同openId显示不同二维码,体验太差,最终说服产品,并拿出可靠方案来实现。

4、项目沟通问题 小程序实人认证无法获取人脸照片,去找线索联系腾讯工作人员,不断沟通,去推动市场同事拿到盖章文件。

45、做系统架构设计要考虑哪些问题

1、基础层设计

  • gitlab创建、制定分支管理规范
  • 自动编译发布的配置,如果没有自动编译发布系统,需要编写脚本发布
  • 统一脚手架
  • 安全问题考虑
  • nginnx配置实现负载均衡和代理

2、应用层设计

  • 引入eslint、规范代码
  • 浏览器兼容考虑,避免重复bug,引入postcss,自动增加前缀,规范团队代码规范,如移动端尽量不要使用fixed,问题记录和整理
  • 权限管理设计
  • 登录系统设计
  • 通用utils引入
  • 基础组件库的建设
  • 接口请求拦截配合路由守卫
  • webpack配置优化、代码优化