前端面试总结

823 阅读27分钟

JavaScript篇

1、JavaScript有哪些数据类型,它们的区别?

八种数据类型:Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt

  • Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
  • BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

这些数据可以分为原始数据类型和引用数据类型:

  • 栈:原始数据类型(Undefined、Null、Boolean、Number、String)
  • 堆:引用数据类型(对象、数组和函数):引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。

2、数据类型检测的方式有哪些

(1)typeof

其中数组、对象、null都会被判断为object,其他判断都正确。

console.log(typeof 2);               // number
console.log(typeof {});              // object

(2)instanceof

instanceof可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型

可以看到,instanceof只能正确判断引用数据类型,而不能判断基本数据类型。instanceof 运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

console.log(2 instanceof Number);                    // false
console.log([] instanceof Array);                    // true
console.log(function(){} instanceof Function);       // true
console.log({} instanceof Object);                   // true

(3)constructor:对象实例通过 constrcutor 对象访问它的构造函数

console.log((2).constructor === Number); // true
console.log(({}).constructor === Object); // true

(4) Object.prototype.toString.call() :使用 Object 对象的原型方法 toString 来判断数据类型

var a = Object.prototype.toString;
 
console.log(a.call(2));
console.log(a.call(true));

3、判断数组的方式

  • 通过Object.prototype.toString.call()做判断
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
  • 通过原型链做判断
obj.__proto__ === Array.prototype;
  • 通过ES6的Array.isArray()做判断
Array.isArrray(obj);
  • 通过instanceof做判断
obj instanceof Array
  • 通过Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(obj)

4、intanceof 操作符的实现原理及实现

instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。

function myInstanceof(left, right) {
  // 获取对象的原型
  let proto = Object.getPrototypeOf(left)
  // 获取构造函数的 prototype 对象
  let prototype = right.prototype; 
 
  // 判断构造函数的 prototype 对象是否在对象的原型链上
  while (true) {
    if (!proto) return false;
    if (proto === prototype) return true;
    // 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
    proto = Object.getPrototypeOf(proto);
  }
}

5. || 和 && 操作符的返回值?

|| 和 && 首先会对第一个操作数执行条件判断,如果其不是布尔值就先强制转换为布尔类型,然后再执行条件判断。

  • 对于 || 来说,如果条件判断结果为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。
  • && 则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。

|| 和 && 返回它们其中一个操作数的值,而非条件判断的结果

6. 箭头函数与普通函数的区别

  1. 箭头函数写法比普通函数更加简洁
  2. 箭头函数没有自己的this,没有prototype,不能作为构造函数使用
  3. 箭头函数继承来的this指向永远不会改变,call()、apply()、bind()等方法不能改变箭头函数中this的指向
  4. 箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值。
  5. 箭头函数不能用作Generator函数,不能使用yeild关键字

7、let、const、var的区别

  1. let和const具有块级作用域,var不存在块级作用域
  2. var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用
  3. 当var声明的变量为全局变量,会将该变量添加为全局对象的属性,但是let和const不会
  4. 在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值
  5. let创建的变量是可以更改指针指向(可以重新赋值),但const声明的变量是不允许改变指针的指向

8. new操作符的实现原理

new操作符的执行过程:

(1)首先创建了一个新的空对象

(2)设置原型,将对象的原型设置为函数的 prototype 对象。

(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)

(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

9、为什么函数的 arguments 参数是类数组而不是数组?如何遍历类数组?

arguments是一个对象,它的属性是从 0 开始依次递增的数字,还有calleelength等属性,与数组相似;但是它却没有数组常见的方法属性,如forEach, reduce等,所以叫它们类数组。

类数组转换为数组方法:

1、使用Array.from方法:Array.from(array)

2、使用展开运算符将类数组转化成数组:const arrArgs = [...arguments]

10、数组有哪些原生方法

push()、pop()、shift() 、unshift()、slice()、concat()、sort()

11、对AJAX的理解

ajax是一种异步通信的方法,从服务端获取数据,达到局部刷新页面的效果

image.png 0. 创建XMLHttpRequest对象; 0. 调用open方法传入三个参数 请求方式(GET/POST)、url、同步异步(true/false); 0. 监听onreadystatechange事件,当readystate等于4时返回responseText; 0. 调用send方法传递参数。

创建AJAX请求:

const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", url, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
  if (this.readyState !== 4) return;
  // 当请求成功时
  if (this.status === 200) {
    handle(this.response);
  } else {
    console.error(this.statusText);
  }
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
  console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);

12、ES6模块与CommonJS模块有什么异同?

ES6 Module和CommonJS模块的区别:

  • CommonJS是对模块的浅拷⻉,ES6 Module是对模块的引⽤,即ES6 Module只存只读,不能改变其值,也就是指针指向不能变,类似const;
  • import的接⼝是read-only(只读状态),不能修改其变量值。 即不能修改其变量的指针指向,但可以改变变量内部指针指向,可以对commonJS对重新赋值(改变指针指向),但是对ES6 Module赋值会编译报错。

ES6 Module和CommonJS模块的共同点:

  • CommonJS和ES6 Module都可以对引⼊的对象进⾏赋值,即对对象内部属性的值进⾏改变。

13、闭包

闭包:是指有权访问另一个函数作用域中的变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

  • 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
  • 闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
  • ·闭包缺点:会导致函数的变量一直保存在内存中,过多的闭包可能会导致内存泄漏

闭包作用: 局部变量无法共享和长久的保存,而全局变量可能造成变量污染,所以我们希望有一种机制既可以长久的保存变量又不会造成全局污染。

自由变量的查找,是在函数定义的地方,向上级作用域查找。不是在执行的地方。

14、原型与原型链

原型:在JavaScript中是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性,它的属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。

原型链:当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念

15、执行上下文

(1)全局执行上下文

任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文。

(2)函数执行上下文

当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。

创建执行上下文

在执行一点JS代码之前,需要先解析代码。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。这一步执行完了,才开始正式的执行程序。

在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。

  • 全局上下文:变量定义,函数声明
  • 函数上下文:变量定义,函数声明,thisarguments

执行上下文栈

当JavaScript执行代码时,首先遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文。

16、this指向

this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。

  • 第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。
  • 第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。
  • 第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
  • 第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。

17、对promise的理解

Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,避免了地狱回调

(1)Promise的实例有三个状态,当把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、没有完成失败了就变成了Rejected。

  • Pending(进行中)
  • Resolved(已完成)
  • Rejected(已拒绝)

2)Promise的实例有两个过程

  • pending -> fulfilled : Resolved(已完成)
  • pending -> rejected:Rejected(已拒绝)

注意:一旦从进行状态变成为其他状态就永远不能更改状态了。

Promise的缺点:

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

18、对async/await 的理解

async 函数返回的是一个 Promise 对象,await 可以用于等待一个 async 函数的返回值

async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行

19、垃圾回收与内存泄漏

垃圾回收:JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。

回收机制

  • Javascript 具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。
  • JavaScript中存在两种变量:局部变量和全局变量。全局变量的生命周期会持续要页面卸载;而局部变量声明在函数中,它的生命周期从函数执行开始,直到函数执行结束,在这个过程中,局部变量会在堆或栈中存储它们的值,当函数执行结束后,这些局部变量不再被使用,它们所占有的空间就会被释放。
  • 不过,当局部变量被外部函数使用时,其中一种情况就是闭包,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会回收。

哪些情况会导致内存泄漏:

  • 意外的全局变量: 由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
  • 被遗忘的计时器或回调函数: 设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
  • 脱离 DOM 的引用: 获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
  • 闭包: 不合理的使用闭包,从而导致某些变量一直被留在内存当中。

20、对节流与防抖的理解

  • 函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。
  • 函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。

防抖函数的应用场景:

  • 按钮提交场景:防⽌多次提交按钮,只执⾏最后提交的⼀次
  • 服务端验证场景:表单验证需要服务端配合,只执⾏⼀段连续的输⼊事件的最后⼀次,还有搜索联想词功能类似⽣存环境请⽤lodash.debounce

节流函数的适⽤场景:

  • 拖拽场景:固定时间内只执⾏⼀次,防⽌超⾼频次触发位置变动
  • 缩放场景:监控浏览器resize
  • 动画场景:避免短时间内多次触发动画引起性能问题

函数防抖的实现:

function debounce(fn, wait) {
  var timer = null;
  return function() {
    var context = this,
      args = [...arguments];
    // 如果此时存在定时器的话,则取消之前的定时器重新记时
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
    // 设置定时器,使事件间隔指定事件后执行
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, wait);
  };
}
复制代码

函数节流的实现:

// 定时器版
function throttle (fun, wait){
  let timeout = null
  return function(){
    let context = this
    let args = [...arguments]
    if(!timeout){
      timeout = setTimeout(() => {
        fun.apply(context, args)
        timeout = null 
      }, wait)
    }
  }
}

21、Set 和 Map有什么区别?

  1. Map是键值对,Set是值得集合,当然键和值可以是任何得值
  2. Map可以通过get方法获取值,而set不能因为它只有值
  3. 都能通过迭代器进行for...of 遍历
  4. Set的值是唯一的可以做数组去重,而Map由于没有格式限制,可以做数据存储

22、for...in 和 for...of的区别?

  1. for...of遍历获取的是对象的键值, for...in获取的是对象的键名;
    1. for in 是ES5的语法标准,而for of则是ES6语法标准。
  2. for...in会遍历对象的整个原型链, 性能非常差不推荐使用,而for...of只遍历当前对象不会遍历原型链;
  3. 对于数组的遍历,for...in会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for...of只返回数组的下标对应的属性值;
  4. for...of 循环可以与break、continue 和 return 配合使用,跳出循环

总结:for...in循环主要是为了遍历对象而生,不适用遍历数组; for....of循环可以用来遍历数组、类数组对象、字符串、Set、Map以及Generator对象

23、require 和 import 的区别

node编程中最重要的思想就是模块化,import 和 require 都是被模块化所使用。在 ES6 当中,用 export 导出接口,用 import 引入模块。但是在 node 模块中,使用module.exports导出接口,使用 require 引入模块。

两者的区别如下:

遵循规范:

  • require 是 AMD 规范引入方式
  • import是 ES6 的一个语法标准,如果要兼容浏览器的话必须转化成 ES5 的语法

调用时间:

  • require是运行时调用,所以require理论上可以运用在代码的任何地方
  • import是编译时调用,所以必须放在文件开头

本质:

  • require 是赋值过程。module.exports后面的内容是什么,require的结果就是什么,比如对象、数字、字符串、函数等,然后再把require的结果赋值给某个变量,它相当于module.exports的传送门
  • import 是解构过程,但是目前所有的引擎都还没有实现import,我们在node中使用babel支持ES6,也仅仅是将ES6转码为ES5再执行,import语法会被转码为require

import 虽然是 es6 中的语法,但就目前来说,所有的引擎都还没有实现import。

24、深克隆和浅克隆的区别?

浅克隆: 只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做“(浅复制)浅拷贝”,换句话说,浅复制仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么浅复制出来的对象也会相应改变。

深克隆: 创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。 JSON.parse、JSON.stringify()

25、map和forEach的区别

相同点

  • 都是循环遍历数组中的每一项 forEach和map方法里每次执行匿名函数都支持3个参数,参数分别是item(当前每一项)、index(索引值)、arr(原数组),需要用哪个的时候就写哪个 匿名函数中的this都是指向window 只能遍历数组

不同点

  • map方法返回一个新的数组,数组中的元素为原始数组调用函数处理后的值。(原数组进行处理之后对应的一个新的数组。) map()方法不会改变原始数组 map()方法不会对空数组进行检测 forEach()方法用于调用数组的每个元素,将元素传给回调函数.(没有return,返回值是undefined)

注意:forEach对于空数组是不会调用回调函数的。

26、说一下前端登录的流程?

初次登录的时候,前端调后调的登录接口,发送用户名和密码,后端收到请求,验证用户名和密码,验证成功,就给前端返回一个token,和一个用户信息的值,

前端拿到token,将token储存到Vuex中,然后从Vuex中把token的值存入浏览器Cookies中。把用户信息存到Vuex然后再存储到LocalStroage中,然后跳转到下一个页面,

根据后端接口的要求,只要不登录就不能访问的页面需要在前端每次跳转页面师判断Cookies中是否有token,没有就跳转到登录页,有就跳转到相应的页面,

我们应该再每次发送post/get请求的时候应该加入token,常用方法再项目utils/service.js中添加全局拦截器,将token的值放入请求头中 后端判断请求头中有无token,有token,就拿到token并验证token是否过期,在这里过期会返回无效的token然后有个跳回登录页面重新登录并且清除本地用户的信息

27、canvas

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>canvas - 创建</title>
</head>
<body>
  <canvas
    id="canvas"
    width="500" 
    height="500" 
    style="box-shadow: 0px 0px 5px #ccc; border-radius: 8px;">
    当前浏览器不支持canvas元素,请升级或更换浏览器!
  </canvas>
  <script>
    const canvas = document.getElementById('canvas'); // 获取Canvas
    const ctx = canvas.getContext('2d'); // 获取绘制上下文
    ctx.strokeStyle = "#f00" // 描边样式设置为红色
    ctx.rect(50, 50, 200,100) // 绘制矩形
    ctx.stroke() // 描边一个矩形轮廓
    ctx.beginPath() // 开启新路径
    ctx.fillStyle = "#0f0" // 描边样式设置为绿色
    ctx.rect(50, 200, 200,100) // 绘制矩形
    ctx.fill() // 填充一个矩形
  </script>
</body>
</html>

image.png

28、canvas绘制矩形

矩形的绘制我们依旧可以用直线来绘制,四条直线首尾相连就可以绘制出一个矩形。但这样绘制矩形太繁琐,因此Canvas为我们提供了简易的Api方法,下面我们依次介绍一下。

// 矩形描边
rect(x, y, width, height)

// 绘制矩形
strokeRect(x, y, width, height)

// 填充矩形
fillRect(x, y, width, height)

rect()

rect()方法可以创建一个矩形路径,想要绘制矩形还需要结合描边stroke() 和填充fill()的方法来实现

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>canvas - 矩形</title>
</head>
<body>
  <canvas
    id="canvas"
    width="500" 
    height="500" 
    style="box-shadow: 0px 0px 5px #ccc; border-radius: 8px;">
    当前浏览器不支持canvas元素,请升级或更换浏览器!
  </canvas>
  <script>
    const canvas = document.getElementById('canvas'); // 获取Canvas
    const ctx = canvas.getContext('2d'); // 获取绘制上下文
    ctx.strokeStyle = "#f00" // 描边样式设置为红色
    ctx.fillStyle = "#00f" // 填充样式设置为蓝色
    ctx.lineWidth = 5

    // 先创将矩形路径,再描边矩形
    ctx.rect(50, 50, 300, 100)
    ctx.fill()

    ctx.beginPath()

    // 直接创建矩形路径并描边
    ctx.fillRect(50, 300, 300, 100)

  </script>
</body>
</html>

image.png

裁剪图像

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>canvas - 图像绘制</title>
</head>
<body>
  <canvas
    id="canvas"
    width="550" 
    height="500" 
    style="box-shadow: 0px 0px 5px #ccc; border-radius: 8px;">
    当前浏览器不支持canvas元素,请升级或更换浏览器!
  </canvas>
  <script>
    const canvas = document.getElementById('canvas'); // 获取Canvas
    const ctx = canvas.getContext('2d'); // 获取绘制上下文

    var img = new Image();
    img.src = 'https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9f56ebb2a6674e1fbd55a3d92df042bd~tplv-k3u1fbpfcp-watermark.image';
    img.onload = function(){
      ctx.drawImage(img, 0, 150, 1650, 700, 0, 0, 550, 500);
    }
  </script>
</body>
</html>

image.png

CSS篇

1、css实现水平居中垂直居中的方式

行内元素:text-algin:center line-height和height高度设置成一样

相对点位:position:absolute top:50% left:50% transform:translate(-50%,-50%)

flex布局:dispaly:flex;aligin-item:center;justify-content:center

2、重绘和重排的区别

重绘(repaint或redraw) :重绘发生在元素的可见的外观被改变,但并没有影响到布局的时候。比如,仅修改DOM元素的字体颜色(只有Repaint,因为不需要调整布局)

重排(重构/回流/reflow) :当改变 DOM 元素位置或大小时,会导致浏览器重新生成渲染树,这个过程叫重排。每个页面至少需要一次回流,就是在页面第一次加载的时候。

触发重排的条件:任何页面布局和几何属性的改变都会触发重排:

  • 页面渲染初始化(无法避免)
  • 添加或删除可见的DOM元素
  • 元素位置的改变,或者使用动画
  • 元素尺寸的改变——大小,外边距,边框
  • 浏览器窗口尺寸的变化
  • 填充内容的改变,比如文本的改变或图片大小改变而引起的计算值宽度和高度的改变

重排必定会引发重绘,但重绘不一定会引发重排。

针对重绘回流的优化方案

  • 1、元素位置移动变换时尽量使用CSS3的transform来代替top,left等操作
  • 2、不要使用table布局
  • 3、将多次改变样式属性的操作合并成一次操作
  • 4、利用文档素碎片(documentFragment),vue使用了该方式提升性能
  • 5、动画实现过程中,启用GPU硬件加速:transform:tranlateZ(0)
  • 6、为动画元素新建图层,提高动画元素的z-index
  • 7、编写动画时,尽量使用requestAnimationFrame

3、CSS选择器及其优先级

id选择器 > 类选择器 > 属性选择器 > 伪类选择器(li:last-child) > 标签选择器 > 伪元素选择器(li:after) > 兄弟选择器 > 子选择器 > 后代选择器 > 通配符选择器

4、animation

div {
  // 简写
  -webkit-animation: 1s rainbow;
  animation: 1s rainbow;
  // 属性缩写
  animation: 1s rainbow linear 3 forwards normal;
}
​
@keyframes rainbow {
  0% { background: #c00; }
  50% { background: orange; }
  100% { background: yellowgreen; }
}

5、rem

借助postcss-px2rem和px2rem-loader插件实现

@media

// 最大宽度为1280
@media (max-width: 1280px) {
  body {
    background: pink;
  }
}
 
 // 宽度为1281-1366之间
@media (min-width: 1281px) and (max-width: 1366px) {
  body {
    background: red;
  }
}
 
// 最小宽度为1441 
@media (min-width: 1441px) {
  body {
    background: green;
  }
}

6、对BFC的理解

BFC是一个完全独立的布局环境,让空间里的子元素不会影响到外面的布局。

如何触发BFC:

1、position: absolute、fixed

2、display :inline-block、flex

3、overflow :hidden、auto、scroll

BFC可以解决哪些问题:

1、使用Float脱离文档流,父元素高度塌陷

2、解决margin的重叠问题

浏览器有哪些线程

  • GUI渲染线程:负责渲染页面,解析html和CSS、构建DOM树、CSSOM树、渲染树、和绘制页面,重绘重排也是在该线程执行。在Javascript引擎运行脚本期间,GUI渲染线程都是处于挂起状态的,GUI更新会被保存在一个队列中等到引擎线程空闲时立即被执行。
  • JS引擎线程:一个tab页中只有一个JS引擎线程(单线程),负责解析和执行JS,处理页面中用户的交互,以及操作DOM树、CSS样式树。它GUI渲染线程不能同时执行,只能一个一个来,如果JS执行过长就会导致阻塞掉帧 GUI渲染线程与JS引擎线程互斥
  • 计时器线程:指 setInterval 和 setTimeout,因为JS引擎是单线程的,所以如果处于阻塞状态,那么计时器就会不准了,所以需要单独的线程来负责计时器工作
  • 异步http请求线程: XMLHttpRequest连接后浏览器开的一个线程,比如请求有回调函数,异步线程就会将回调函数加入事件队列,等待JS引擎空闲执行
  • 事件触发线程:主要用来控制事件循环,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。

7、浏览器渲染引擎的主要模块

  • 1、HTML解析器:解释HTML文档的解析器,主要作用是将HTML文本解释为DOM树
  • 2、CSS解析器:它的作用是为DOM中的各个元素对象计算出样式信息,为布局提供基础设施
  • 3、JavaScript引擎:JavaScript引擎能够解释JavaScript代码,并通过DOM接口和CSS接口来修改网页内容 和样式信息,从而改变渲染的结果
  • 4、布局(layout):在DOM创建之后,WebKit需要将其中的元素对象同样式信息结合起来,计算他们的大小位置等布局信息,形成一个能表达着所有信息的内部表示模型
  • 5、绘图模块(paint):使用图形库将布局计算后的各个网页的节点绘制成图像结果

8、渲染页面过程

  • 1、解析HTML,生成DOM树,解析CSS,生成CSSOM树
  • 2、将DOM树和CSSOM树结合,生成渲染树(Render Tree)
  • 3、Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
  • 4、Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
  • 5、将像素发送给GPU,展示在页面上

9、CSS的盒子模型

  • 标准盒子模型:宽度=内容的宽度(content)+ border + padding
  • 低版本IE盒子模型:宽度=内容宽度(content+border+padding)

10、用纯CSS创建一个三角形

 <style>
    div {
        width: 0;
        height: 0;
        border-top: 40px solid transparent;
        border-left: 40px solid transparent;
        border-right: 40px solid transparent;
        border-bottom: 40px solid #ff0000;
    }
  </style>

11、如何理解 HTML 语义化?

  • 让人更容易读懂(增加代码可读性)。
  • 让搜索引擎更容易读懂,有助于爬虫抓取更多的有效信息,爬虫依赖于标签来确定上下文和各个关键字的权重(SEO)。
  • 在没有 CSS 样式下,页面也能呈现出很好地内容结构、代码结构。

12、script 标签中 defer 和 async 的区别?

  • script :会阻碍 HTML 解析,只有下载好并执行完脚本才会继续解析 HTML。

  • async script :解析 HTML 过程中进行脚本的异步下载,下载成功立马执行,有可能会阻断 HTML 的解析。

  • defer script:完全不会阻碍 HTML 的解析,解析完成之后再按照顺序执行脚本。

image.png

13、浏览器页面的加载过程

首先,浏览器在加载页面的时候,会用到GUI渲染线程(渲染浏览器HTML元素)和Javascript引擎线程(处理Javascript脚本程序),由于Javascript在执行过程中可能会改变页面结构和样式,因此二者之间被设计为互斥的关系,也就是说当Javascript引擎执行时,GUI线程会被挂起。 具体加载过程如下:

  1. 当打开一个官网的时候,浏览器会从服务器中获取到HTML内容
  2. 浏览器从上到下解析HTML元素,标签内容会先被解析,此时浏览器还没开始渲染页面;
  3. <head>标签若包含<script>标签,script元素通过src属性指向外部资源,当浏览器解析到这里时,会暂停解析并下载Javascript脚本;
  4. Javascript脚本下载完成后,浏览器的控制权转交给Javascript引擎;
  5. 当脚本执行完成后,控制权会转交给渲染引擎;
  6. 此时元素内容开始被解析,浏览器开始渲染页面;

我们可以看到<head>标签里的script元素会阻塞页面的渲染,如果外部脚本加载时间很长,就会造成网页长时间失去响应,浏览器就会呈现“假死”状态,这也就是为什么我们通常将Javascript脚本放在的后面的原因。 当然,我们也可以使用defer/async/preload等属性来标记script标签,来控制Javascript的加载顺序。

浏览器在渲染页面的时候,会加载HTML和CSS,以获得DOM树和CSS规则树,他们结合后才渲染出页面,因此我们还常常将CSS放在<head>里, 可以用来避免浏览器渲染的重复计算。

14、css 加载会造成阻塞吗?

在 Chrome 上,css 加载不会阻塞 HTML 解析,但会阻塞 DOM 渲染。

html 和 css 是同时解析的,所以不阻塞 HTML 解析。html 生成 dom,css 生成 cssom,两者结合才能生成 render tree 渲染树,所以阻塞 DOM 渲染

css 加载会阻塞后面 js 的执行。

由于 JavaScript 是可操纵 DOM 和 css 的,如果在修改这些元素属性同时渲染界面,会造成冲突。为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。所以 css 会阻塞后面 js 的执行。

计算机网络原理

1、GET和POST区别

  • GET在浏览器回退不会再次请求,POST会再次提交请求

  • GET请求会被浏览器主动缓存,POST不会,要手动设置

  • GET请求参数会被完整保留在浏览器历史记录里,POST中的参数不会

  • GET请求在URL中传送的参数是有长度限制的,而POST没有限制

  • GET参数通过URL传递,暴露在地址栏不安全,POST放在报文内部更安全

  • GET一般用于查询信息,POST一般用于提交某种信息进行某些修改操作

  • GET产生一个TCP数据包;POST产生两个TCP数据包

    请求方式GETPOST
    参数位置参数拼接到url的后面参数在请求体中
    参数大小受限于浏览器url大小,一般不超过32K1G
    服务器数据接收接收1次根据数据大小,可分多次接收
    适用场景从服务器端获取数据向服务器提交数据
    安全性参数携带在url中,安全性低相对于GET请求,安全性更高

2、cookie,sessionStorage和localStorage

  • cookie用来保存登录信息,大小限制为4KB左右
  • localStorage是Html5新增的,用于本地数据存储,保存的数据没有过期时间,除非被清除,否则永久保存,一般浏览器大小限制在5MB
  • sessionStorage接口方法和localStorage类似,但保存的数据仅在当前会话下有效,关闭页面或浏览器后被清除。
名称生命期大小限制与服务器通信
cookie一般由服务器生成,可设置失效时间。如果在浏览器端生成Cookie,默认是关闭浏览器后失效4KB每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题
localStorage除非被清除,否则永久保存5MB仅在浏览器中保存,不与服务器通信
sessionStorage仅在当前会话下有效,关闭页面或浏览器后被清除5MB仅在浏览器中保存,不与服务器通信

localStorage和sessionStorage的使用

存储数据(同修改数据)
sessionStorage.setItem('key', 'value');
localStorage.setItem('key', 'value');
取出数据
sessionStorage.getItem('key');
localStorage.getItem('key');
删除数据
sessionStorage.removeItem('key');
localStorage.removeItem('key')

清除数据

sessionStorage.clear();
localStorage.clear();

3、HTTP状态码

1xx表示客户端应该继续发送请求

2xx表示成功的请求

  • 200表示OK,正常返回信息
  • 201表示请求成功且服务器创建了新的资源
  • 202表示服务器已经接受了请求,但还未处理

3xx表示重定向

  • 301表示永久重定向,请求的网页已经永久移动到新位置
  • 302表示临时重定向
  • 304表示自从上一次请求以来,页面的内容没有改变过

4xx表示客户端错误

  • 400表示请求参数有问题
  • 401表示未携带token或者token失效
  • 402表示请求未授权
  • 403表示禁止访问
  • 404表示请求的资源不存在,一般是路径写错了

5xx表示服务器错误

  • 500表示最常见的服务器错误
  • 503表示服务器暂时无法处理请求

4、HTTP与HTTPS的区别

  • HTTP的URL由http://起始且默认使用端口80,而HTTPS的URL由https://起始且默认使用端口443
  • HTTP是超文本传输协议,信息是明文传输,HTTPS则是具有安全性的 SSL 加密传输协议
  • http 的连接很简单,是无状态的。HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议 要比http协议安全
  • https 缓存不如 http 高效,会增加数据开销。‘

HTTP 1.0、HTTP 1.1、HTTP 2.0、HTTP 3.0的区别

链接:HTTP 灵魂之问,巩固你的 HTTP 知识体系

HTTP 1.0

  • 无状态,无连接;
  • 短连接:每次发送请求都要重新建立tcp请求,即三次握手,非常浪费性能;
  • 无host头域,也就是http请求头里的host;
  • 不允许断点续传,而且不能只传输对象的一部分,要求传输整个对象。

HTTP 1.1

  • 长连接,流水线,使用connection:keep-alive使用长连接;
  • 请求管道化;
  • 增加缓存处理(新的字段如cache-control);
  • 增加Host字段,支持断点传输等;
  • 由于长连接会给服务器造成压力。

HTTP 2.0

  • 多路复用,无需多个TCP连接,因为其允许在单一的HTTP2连接上发起多重请求,因此可以不用依赖建立多个TCP连接。
  • 二进制分帧,将所有要传输的消息采用二进制编码,并且会将信息分割为更小的消息块。
  • 头部压缩,用HPACK技术压缩头部,减小报文大小
  • 服务端推送,服务端可以在客户端发起请求前发送数据,换句话说,服务端可以对客户端的一个请求发送多个相应,并且资源可以正常缓存

HTTP 3.0

  • 基于google的QUIC协议,而quic协议是使用udp实现的;
  • 减少了tcp三次握手时间,以及tls握手时间;
  • 解决了http 2.0中前一个stream丢包导致后一个stream被阻塞的问题;
  • 优化了重传策略,重传包和原包的编号不同,降低后续重传计算的消耗;
  • 连接迁移,不再用tcp四元组确定一个连接,而是用一个64位随机数来确定这个连接;
  • 更合适的流量控制

5、HTTP缓存

1、强缓存:expries/cache-control

响应头里expires的时间为当前时间 + 30s;30s之内,资源都会走本地缓存,而不会重新请求

cache-control:设置的是毫秒数

const time = new Date(Date.now() + 30000).toUTCString()
 ctx.set('Expires', time)
2、协商缓存:
Last-Modified,If-Modified-Since:对比资源最后一次修改时间,来确定资源是否修改了
  • 第一次请求资源时,服务端会把所请求的资源的最后一次修改时间当成响应头中Last-Modified的值发到浏览器并在浏览器存起来
  • 第二次请求资源时,浏览器会把刚刚存储的时间当成请求头中If-Modified-Since的值,传到服务端,服务端拿到这个时间跟所请求的资源的最后修改时间进行比对
  • 比对结果如果两个时间相同,则说明此资源没修改过,那就是命中缓存,那就返回304,如果不相同,则说明此资源修改过了,则没命中缓存,则返回修改过后的新资源
Etag,If-None-Match:对比资源内容,来确定资源是否修改

6、浏览器输入URL发生了啥?

  • 1、浏览器的地址栏输入URL并按下回车。
  • 2、浏览器查找当前URL是否存在缓存,并比较缓存是否过期。
  • 3、DNS解析URL对应的IP:浏览器向DNS服务器发起请求,解析该URL中的域名对应的IP地址
  • 4、根据IP建立TCP连接(三次握手):解析出IP地址后,根据IP地址和默认80端口,和服务器建立TCP连接
  • 5、HTTP发起请求:浏览器发起读取文件的HTTP请求,,该请求报文作为TCP三次握手的第三次数据发送给服务器
  • 6、服务器处理请求,浏览器接收HTTP响应。
  • 7、渲染页面,构建DOM树。
  • 8、关闭TCP连接(四次挥手)。

TCP的三次握手和四次挥手

三次握手

  • 第一次握手:客户端发送一个SYN码给服务器,要求建立数据连接;
  • 第二次握手: 服务器SYN和自己处理一个SYN(标志);叫SYN+ACK(确认包);发送给客户端,可以建立连接
  • 第三次握手: 客户端再次发送ACK向服务器,服务器验证ACK没有问题,则建立起连接;

四次挥手

  • 第一次挥手: 客户端发送FIN(结束)报文,通知服务器数据已经传输完毕;
  • 第二次挥手: 服务器接收到之后,通知客户端我收到了SYN,发送ACK(确认)给客户端,数据还没有传输完成
  • 第三次挥手: 服务器已经传输完毕,再次发送FIN通知客户端,数据已经传输完毕
  • 第四次挥手: 客户端再次发送ACK,进入TIME_WAIT状态;服务器和客户端关闭连接;

7、什么是同源策略

一个域下的js脚本未经允许的情况下,不能访问另一个域下的内容。通常判断跨域的依据是协议、域名、端口号是否相同,不同则跨域。同源策略是对js脚本的一种限制,并不是对浏览器的限制,像img,script脚本请求不会有跨域限制。

前后端如何通信
Ajax : 短连接
Websocket : 长连接,双向的。
Form表单(最原始的)
跨域通信的几种方式

解决方案:

  1. jsonp(利用script标签没有跨域限制的特点实现。缺点:只支持GET请求)
  2. CORS(设置Access-Control-Allow-Origin:指定可访问资源的域名)
  3. postMessage(message, targetOrigin, [transfer])(HTML5新增API 用于多窗口消息、页面内嵌iframe消息传递),通过onmessage监听 传递过来的数据
  4. Websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。
  5. Node中间件代理
  6. Nginx反向代理
  7. 各种嵌套iframe的方式,不常用。
  8. 日常工作中用的最对的跨域方案是CORS和Nginx反向代理

nginx反向代理

image.png

8、EventLoop:事件循环,是浏览器或Node解决单线程运行时不会阻塞的一种机制。

因为 js 是单线程运行的,在代码执行时,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。在执行同步代码时,如果遇到异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当异步事件执行完毕后,再将异步事件对应的回调加入到一个任务队列中等待执行。任务队列可以分为宏任务队列和微任务队列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务队列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。当微任务队列中的任务都执行完成后再去执行宏任务队列中的任务。

Event Loop执行过程
  1. 代码开始执行,创建一个全局调用栈,script作为宏任务执行

  2. 执行过程过同步任务立即执行,异步任务根据异步任务类型分别注册到微任务队列和宏任务队列(Event Queue)

  3. 同步任务执行完毕,查看微任务队列

    • 若存在微任务,将微任务队列全部执行(包括执行微任务过程中产生的新微任务)
    • 若无微任务,查看宏任务队列,执行第一个宏任务,宏任务执行完毕,查看微任务队列,重复上述操作,直至宏任务队列为空

8、Vue性能优化

  1. v-for 正确设置key值:更快定位数据与diff
  2. 模块化,组件化:封装具有高度复用性的模块;拆分高度复用性组件;组件可配置项强
  3. 路由懒加载:componet:()=>import('./view/index.vue') 初次不加载,跳转到路由组件后才加载路由
  4. 合理使用computed和watch
  5. v-for的同时避免使用v-if
  6. 使用keep-alive 缓存不活跃组件
  7. destory时销毁事件:比如addEventListener添加的事件、setTimeout、setInterval、bus.$on绑定的监听事件等

9、性能优化

webpack打包优化

  • 路由的按需加载
  • 启用gzip压缩,打包体积更小 productionGzip:true
  • 优化打包后资源的大小
  • 第三方插件的按需引入
  • image-webpack-loader对图片进行压缩
  • Tree shaking 去除死代码(usedExports:通过标记某些函数是否被使用,之后通过Terser来进行优化; sideEffects:跳过整个模块/文件,直接查看该文件是否有副作用)
  • 使用babel-plugin-transform-runtime插件减少ES6转化ES5的冗余

网络优化

  • 利用缓存不加载重复资源,添加express
  • 减少http请求,删除不必要的代码
  • 静态资源使用CDN(内容分发网络),在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间

页面渲染优化

  • 降低css选择器的复杂性:减少嵌套:最多不要超过三层,并且后代选择器的开销较高,慎重使用
  • 字体图标代替图片图标:将图标制作成一个字体,生成文件特别小
  • 首屏加载优化:使用骨架屏或者动画优化用户体验;资源按需加载,首页不需要的资源延迟加载
  • 减少重绘/重排,js修改样式,最好不要直接写样式,而是替换class类来改变样式;如果对DOM元素执行一系列操作的操作,可以将DOM元素脱离文档流;用transfrom和opacity属性更改来实现动画,不会触发重排和重绘

JS性能优化

  • 使用事件委托:事件委托利用了事件冒泡,指定一个事件处理程序,就可以管理某一类的所有事件,可以减少内存
  • 使用防抖和节流
  • 多线程:复杂的计算开启webWorker进行计算,避免页面假死
  • 尽量不要使用JS动画:css3动画和canvas动画都比JS动画性能好
  • if else和switch 当判断数据较多时,倾向使用switch

图片优化

雪碧图

  • 借助减少http请求次数来进行优化

图片懒加载

  • 在图片即将进入可视区域的时候进行加载(判断图片进入可视区域请参考这里)

使用CSS3代替图片

  • 有很多图片使用 CSS 效果(渐变、阴影等)就能画出来,这种情况选择 CSS3 效果更好

图片压缩

  • 压缩方法有两种,一是通过在线网站进行压缩,二是通过 webpack 插件 image-webpack-loader。它是基于 imagemin 这个 Node 库来实现图片压缩的。

使用渐进式jpeg

  • 使用渐进式jpeg,会提高用户体验 参考文章

使用 webp 格式的图片

  • webp 是一种新的图片文件格式,它提供了有损压缩和无损压缩两种方式。在相同图片质量下,webp 的体积比 png 和 jpg 更小。

首页渲染优化

  1. 路由懒加载(import+箭头函数)
  2. 组件懒加载(dialog弹框代码延迟加载)
  3. 合理使用 Tree shaking
  4. 骨架屏优化白屏时长(vue-skeleton-webpack-plugin)
  5. 长列表虚拟滚动
  6. Web Worker 优化长任务

webpack篇

面试题:「吐血整理」再来一打 Webpack 面试题

1、基本配置

  1. entry(入口)

指示 Webpack 从哪个文件开始打包

  1. output(输出)

指示 Webpack 打包完的文件输出到哪里去,如何命名等

  1. loader(加载器)

webpack 本身只能处理 js、json 等资源,其他资源需要借助 loader,Webpack 才能解析

  1. plugins(插件)

扩展 Webpack 的功能,让webpack具有更多的灵活性。 在 webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 webpack 提供的 API 改变输出结果。

  1. mode(模式)

主要由两种模式:

  • 开发模式:development
  • 生产模式:production

2、使用过webpack里面哪些plugin和loader

loader

  • babel-loader: 将ES6+转移成ES5-
  • css-loader,style-loader:解析css文件,能够解释@import url()等
  • file-loader:直接输出文件,把构建后的文件路径返回,可以处理很多类型的文件
  • url-loader:与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值会交给 file-loader 处理,小于阈值时返回文件 base64 形式编码 (处理图片和字体)
// url-loader增强版的file-loader,小于limit的转为Base64,大于limit的调用file-loader
npm install url-loader -D
// 使用
module.exports = {
    module: {
        rules: [{
            test: /\.(png|jpg|gif)$/,
            use: [{
                loader: 'url-loader',
                options: {
                    outputPath: 'images/',
                    limit: 500 //小于500B的文件打包出Base64格式,写入JS
                }
            }]
        }]
    }
}

plugins

  • html-webpack-plugin: 压缩html
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
  //...
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html', // 配置输出文件名和路径
      template: './public/index.html', // 配置要被编译的html文件
      hash: true,
      // 压缩 => production 模式使用
      minify: {
        removeAttributeQuotes: true, //删除双引号
        collapseWhitespace: true //折叠 html 为一行
      }
    })
  ]
}
  • clean-webpack-plugin: 打包器清理源目录文件,在webpack打包器清理dist目录
npm install clean-webpack-plugin -D
// 修改webpack.config.js
const cleanWebpackPlugin=require('clean-webpack-plugin')
module.exports = {
    plugins: [new cleanWebpackPlugin(['dist'])]
}

Vue篇

1、MVVM是什么?和MVC有何区别呢?

MVC

  • Model(模型):负责从数据库中取数据
  • View(视图):负责展示数据的地方
  • Controller(控制器):用户交互的地方,例如点击事件等等
  • 思想:Controller将Model的数据展示在View上
VM:也就是View-Model,做了两件事达到了数据的双向绑定 一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。

思想:实现了 View 和 Model 的自动同步,也就是当 Model 的属性改变时,我们不用再自己手动操作 Dom 元素,来改变 View 的显示,而是改变属性后该属性对应 View 层显示会自动改变(对应Vue数据驱动的思想)

2、 Vue和JQuery的区别在哪?为什么放弃JQuery用Vue?

1.jQuery是直接操作DOM,Vue不直接操作DOM,Vue的数据与视图是分开的,Vue只需要操作数据即可

2.在操作DOM频繁的场景里,jQuery的操作DOM行为是频繁的,而Vue利用虚拟DOM的技术,大大提高了更新DOM时的性能

3.Vue中不倡导直接操作DOM,开发者只需要把大部分精力放在数据层面上

4.Vue集成的一些库,大大提高开发效率,比如Vuex,Router等

3、为什么data是个函数并且返回一个对象呢?

data之所以是一个函数,是因为一个组件可能会多处调用,而每一次调用就会执行data函数并返回新的数据对象,这样,可以避免多处调用之间的数据污染

4、组件之间的传值方式有哪些?

  • 父组件传值给子组件,子组件使用props进行接收
  • 子组件传值给父组件,子组件使用$emit+事件对父组件进行传值
  • 组件中可以使用$parent$children获取到父组件实例和子组件实例,进而获取数据
  • 使用$refs获取组件实例,进而获取数据
  • 使用Vuex进行状态管理
  • 使用eventBus进行跨组件触发事件,进而传递数据
  • 使用provideinject,官方建议我们不要用这个,我在看ElementUI源码时发现大量使用
  • 使用$attrs$listeners,在对一些组件进行二次封装时可以方便传值,例如A->B->C
  • 使用浏览器本地缓存,例如localStorage

5、v-if和v-show有何区别?

  • v-if是通过控制dom元素的删除和生成来实现显隐,每一次显隐都会使组件重新跑一遍生命周期,因为显隐决定了组件的生成和销毁
  • v-show是通过控制dom元素的css样式来实现显隐,不会销毁
  • 频繁或者大数量显隐使用v-show,否则使用v-if

6、computed和watch有何区别?

  • computed是依赖已有的变量来计算一个目标变量,大多数情况都是多个变量凑在一起计算出一个变量,并且computed具有缓存机制,依赖值不变的情况下其会直接读取缓存进行复用,computed不能进行异步操作
  • watch是监听某一个变量的变化,并执行相应的回调函数,通常是一个变量的变化决定多个变量的变化,watch可以进行异步操作

7、Vue的生命周期,讲一讲?

beforeCreate:vue实例被创建,此时data和methods中的数据都还没有初始化

created:数据已被初始化和响应式处理,在这里可以访问数据和修改数据,但未挂载

beforeMount:通过模板生成虚拟DOM,但是还没有转换成真实DOM并且替换el

mounted:真实DOM挂载完毕,可以获取DOM节点

beforeUpdate:数据更新后,新的虚拟DOM生成,但是还没有和旧的虚拟DOM做对比

updated:新旧虚拟DOM对比打补丁后,进行真实DOM的更新

beforeDestroy:实例被销毁前,此时可以手动销毁一些方法

destroyed:实例销毁后调用,该钩子被调用后,对应Vue实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁

8、Vue响应式是怎么实现的?

整体思路是数据劫持+发布者-订阅者模式模式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发响应的监听回调。

img

Vue创建实例后, 会调用_init 函数进行初始化,通过Object.defineProperty方法来对数据进行劫持,当被设置的对象被读取的时候会执行getter 函数,而在当被赋值的时候会执行 setter函数。

当render 函数执行的时候,因为会读取所需对象的值,所以会触发getter函数从而将Watcher添加到依赖中进行依赖收集。

在修改对象的值的时候,会触发对应的setter, setter通知之前依赖收集得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher就会开始调用 update 来更新视图

双向数据绑定

双向数据绑定通常是指我们使用的v-model指令的实现,是Vue的一个特性,也可以说是一个input事件和value的语法糖。 Vue通过v-model指令为组件添加上input事件处理和value属性的赋值。

<template> <input v-model='localValue'/> </template>

上述的组件就相当于如下代码

<template>
   <!-- 这里添加了input时间的监听和value的属性绑定 -->
   <input @input='onInput' :value='localValue' />
   <span>{{localValue}}</span>
</template>
<script>
  export default{
    data(){
      return {
        localValue:'',
      }
    },
    methods:{
      onInput(v){
         //在input事件的处理函数中更新value的绑定值
         this.localValue=v.target.value;
         console.log(this.localValue)
      }
    }
  }
</script>

9、nextTick的实现

nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout的原生 JavaScript 方法来模拟对应的微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列。

Vue是异步更新,数据一更新,视图却还没更新,nextTickVue提供的一个全局API,是在下次DOM更新循环结束之后执行延迟回调,在修改数据之后使用$nextTick,则可以在回调中获取更新后的DOM

10、keep-alive的实现

作用:实现组件缓存

钩子函数:

`activated `组件渲染后调用
`deactivated `组件销毁后调用

11、Vuex

State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。

Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。

Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。

Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。

Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。

12、你都做过哪些Vue的性能优化?

  • 尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher,可以利用Object.freeze()冻结不需要响应式变化的数据
  • 提前处理好数据解决v-if和v-for必须同级的问题,因为当Vue处理指令时,v-forv-if具有更高的优先级,意味着v-if 将分别重复运行于每个v-for循环中。可以在computed中提前把要v-for的数据中v-if的数据项给过滤处理了
  • 给v-for循环项加上key提高diff计算速度
  • 防抖和节流
  • 第三方模块按需导入、图片懒加载

13、图片懒加载

内容上方偏移量(scrollTop)+视口高度(innerHeight)>图片距离内容顶部的偏移量(offsetTop)

执行命令npm install vue-lazyload --save安装vue-lazyload插件。在main.js中引入配置

import VueLazyload from 'vue-lazyload';
Vue.use(VueLazyload, {
  preLoad: 1.3,//预载高度比例
  error: 'dist/error.png',//加载失败显示图片
  loading: 'dist/loading.gif',//加载过程中显示图片
  attempt: 1,//尝试次数
})
复制代码
<img v-lazy="/static/img/1.png">

14、父子组件生命周期顺序

父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted

15、对象新属性无法更新视图,删除属性无法更新视图,为什么?怎么办?

  • 原因:Object.defineProperty没有对对象的新属性进行属性劫持
  • 对象新属性无法更新视图:使用Vue.$set(obj, key, value),组件中this.$set(obj, key, value)
  • 删除属性无法更新视图:使用Vue.$delete(obj, key),组件中this.$delete(obj, key)

16、插槽

  • 默认插槽/匿名插槽
<template>  // 父组件:(引用子组件 child)
  <div class= 'app'>
     <child> {{ child }}</child>
  </div>
</template><template>  // 子组件 : (假设名为:child)
  <div class= 'child'>
       <slot></slot>
  </div>
</template>
​
​
  • 具名插槽
<template>  //父组件:(引用子组件 child)
  <div class= 'app'>
     <child> 
        <template v-slot:"one"> 这是插入到one插槽的内容 </template>
        <template v-slot:"two"> 这是插入到two插槽的内容 </template>
     </child>
  </div>
</template>
<template>  //子组件 : (假设名为:child)
  <div class= 'child'>
      <slot name='one'> 这就是默认值1</slot>
      <slot name='two'> 这就是默认值2 </slot>
  </div>
</template>
  • 作用域插槽:父组件直接使用子组件内的数据
<template>  //父组件:(引用子组件 child)
  <div class='app'>
     <child> 
        <template v-slot:one='slotone'>  
           {{ slotone.value1 }}    // 通过v-slot的语法 将子组件的value1值赋值给slotone 
        </template>
        <template v-slot:default='slotde'> 
           {{ slotde.value2 }}  // 同上,由于子组件没有给slot命名,默认值就为default
        </template>
     </child>
  </div>
</template>
<template> //子组件 : (假设名为:child)
  <div class= 'child'>
      <slot name= 'one' :value1='child1'> 这就是默认值1</slot>    //绑定child1的数据
      <slot :value2='child2'> 这就是默认值2 </slot>  //绑定child2的数据,这里我没有命名slot
  </div>           
</template>

17、 keep-alive

keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染 ,其有以下特性:

  • 一般结合路由和动态组件一起使用,用于缓存组件;
  • 提供 include 和 exclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;
  • 对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。

18、路由有哪些模式呢?又有什么不同呢?

路由器对象底层实现的三大步骤即(1)监视地址栏变化;(2)查找当前路径对应的页面组件;(3)将找到的页面组件替换到 router-vieW 的位置。

  • hash模式:通过#号后面的内容的更改,触发hashchange事件,实现路由切换;hash 虽然出现URL中,但不会被包含在HTTP请求中,对后端完全没有影响,不需要后台进行配置,因此改变hash不会重新加载页面。
  • history模式:通过pushState() 和replaceState() 方法来实现无刷新跳转的功能。history模式改变了路由地址,因为需要后台配置地址。

19、vue的优点

  • 轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十kb;
  • 简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习;
  • 双向数据绑定:保留了angular的特点,在数据操作方面更为简单;
  • 组件化:保留了react的优点,实现了html的封装和重用,在构建单页面应用方面有着独特的优势;
  • 视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作;
  • 虚拟DOM:dom操作是非常耗费性能的,不再使用原生的dom操作节点,极大解放dom操作,但具体操作的还是dom不过是换了另一种方式;
  • 运行速度更快:相比较与react而言,同样是操作虚拟dom,就性能而言,vue存在很大的优势

20、vue2与vue3区别

1、Vue3支持碎片(Fragments)

Vue2 组件中只允许有一个根节点,在vue3中可以有多个根节点。在vue3中如果组件有多个根节点,并且在父组件给该组件传人样式或者类,你需要用$attrs来指定根节点接受这个样式或者类。

2、新增的setup方法

一般的用法是在script标签上加上setup,setup是在单文件组件 (SFC)中使用组合式 API的编译时语法糖。当同时使用 SFC 与组合式 API 时该语法是默认推荐。相比于普通的

  1. 少的样板内容,更简洁的代码。
  2. 能够使用纯 TypeScript 声明 props 和自定义事件。
  3. 更好的运行时性能。
  4. 更好的 IDE 类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。

3、检测数据方法的改变

Vue2中监测数据的方法是Object.defineProperty,有着诸多限制,比如说检测对象只能检测其中的一个属性,我们每次注册对象时都要遍历这个对象才能全部检测,而且Obiect.defineProperty检测不了数组的变化,只能重写数组的方法才能达到检测数组的效果,而且新增和删除数组和对象的数据检测不了,要用this.$set和vue.delete来进行新增和删除。

Vue3检测数据的方法是es6的Proxy,可以监听整个对象,而不是对象的某个属性,也可以检测数组的改变以及数据的增删以及Map,Set,WeakMap,WeakSet

4、生命周期的改变

Vue3中的生命周期相比于vue2少了beforeCreate和created这两个生命周期,可以直接把这两个生命周期的内容写在setup中。

5、父子组件传参方式的改变

Vue3中子组件要接收父组件的传参需要用到defineProps

6、v-model的改变

在vue2中v-model绑定的是子组件的value,在vue3中改成了modeIValue,触发的方法也从input改成了update:modelValue。Vue2的.sync修饰符也被v-model:xxx给取代了。

7、Diff算法的改变

vue2的diff算法是全量diff,vue3是静态标记+不全量dif,换句话说就是把一些不会更新的节点打上静态标记,进行diff算法的时候忽略这些节点,节约性能。

8、Api模式不同

Vue2使用的是选项式api(Options Api) Vue3不仅兼容选项式api(Options Api),也支持组合式api(Composition Api)

21、Vue和React的区别

相同点:

  • 1.都使用了虚拟dom
  • 2.组件化开发
  • 3.都是单向数据流(父子组件之间,不建议子修改父传下来的数据)
  • 4.都支持服务端渲染

不同点:

  • 1.React的JSX,Vue的template
  • 2.数据变化,React手动(setState),Vue自动(初始化已响应式处理,Object.defineProperty)
  • 3.React单向绑定,Vue双向绑定
  • 4.React的Redux,Vue的Vuex

22、什么是SPA单页面,有什么优缺点?

  • SPA 单页面及应用方式:单一页面应用程序,只有一个完整的页面;它在第一次加载页面时,就将唯一完整的 html 页面和所有其余页面组件一起下载下来,这样它在切换页面时,不会加载整个页面,而是只更新某个指定的容器中内容。

  • 单页面应用(SPA)的核心之一是: 更新视图而不重新请求页面。

优点:

  1. 体验好,不刷新,减少 请求 数据ajax异步获取 页面流程;
  2. 前后端分离
  3. 减轻服务端压力
  4. 共用一套后端程序代码,适配多端

缺点:

1.首屏加载过慢;

2.SEO 不利于搜索引擎抓取

23、虚拟DOM

为什么要用虚拟DOM ?

既然真实DOM已经有了,那为什么要浪费资源来使用虚拟DOM1、呢?

确实第一次渲染DOM,虚拟DOM确实是在浪费资源,但是后续的DOM更新,如果没有虚拟DOM就会全量更新整个DOM树,然而DOM操作是非常消耗资源的,虚拟DOM就是为了解决这个问题。

那虚拟DOM是怎么解决这个问题呢?我们拿vue来举例子,vue是怎么更新DOM呢 ?

通过依赖更新触发,然后生成新的虚拟DOM,将新的虚拟DOM传入patch中,通过diff算法对比新旧两个虚拟DOM,如果虚拟DOM没有改变则不操作,改变了就操作真实DOM,减少了DOM的操作,只会操作已经改变的DOM

举个例子,没有用虚拟DOM的更新就像是没有设计图的房子,如果想修改只能全部拆掉重新做一个房子,有虚拟DOM的更新是先在设计图上改,改好后施工队根据新的图纸和已经存在的房子来进行修改能减少很多工作量。 什么虚拟DOM ?

虚拟DOM就是用js按照DOM结构来实现的一个树形结构,这个树形结构可以完全表现真实DOM的每个DOM节点以及DOM节点的属性真实DO

diff 算法

1.概念:

diff 算法是一种对比算法,通过对比旧的虚拟DOM和新的虚拟DOM,得出是哪个虚拟节点发生了改变,找出这个虚拟节点并只更新这个虚拟节点所对应的真实节点,而不用更新其他未发生改变的节点,实现精准地更新真实DOM,进而提高效率。

2.对比方式:

diff 算法的整体策略是: 深度优先,同层比较 。比较只会在同层级进行,不会跨层级比较:比较的过程中,循环从两边向中间收拢。

首先判断两个节点的 tag 是否相同,不同则删除该6节点重新创建节点进行替换。

tag 相同时,先替换属性,然后对比子元素,分为以下几种情况:

  • 新旧节点都有子元素时,采用双指针方式进行对比。新旧头尾指针进行比较,循环向中间靠拢,根据情况调用 patchVnode 进行 patch 重复流程、调用 createElem 创建一个新节点从哈希表寻找 key一致的 VNode 节点再分情况操作。

  • 新节点有子元素,旧节点没有子元素,则将子0元素虚拟节点转化成真实节点插入即可。

  • 新节点没有子元素,旧节点有子元素,则清空0子元素,并设置为新节点的文本内容新旧节点都没有子元素时,即都为文本节点0则直接对比文本内容,不同则更新。

24、pinia

  • main中注册
// 引入pinia
import { createPinia } from 'pinia'
// 创建 Pinia 实例
const pinia = createPinia()
// 挂载
app.use(pinia)
  • 新建store文件夹,在store文件夹下新建index.js
import { defineStore } from "pinia";

export const useMainStore = defineStore("main", {
  // 类似于Vue2组件中的data,用于存储全局状态数据,但有两个要求
  // 1. 必须是函数,目的是为了在服务端渲染的时候避免交叉请求导致的数据状态污染
  // 2. 必须是箭头函数,这样是为了更好的 TS 类型推导
  state: () => {
    return {
      msg: "安徽黄山期待您的到来",
      count: 0
    };
  },
  getters: {
    getCount(state) {
      return state.count + 100;
    }
  },
  actions: {
    changeState() {
      this.msg = "安徽黄山祝您工作愉快";
      this.count += 10;
    }
  }
});
  • 基本使用
<template>
  <div class="home">
    <div>{{msg}}</div>
    <el-button type="primary"
               @click="addCount">count+1</el-button>
    <div>{{count}}</div>
     <!-- getter 和 Vuex中的getter一样,在获取 State值之前做一些逻辑处理,具有缓存作用 -->
    <div>{{store.getCount}}</div>
  </div>
</template>
<script setup lang="ts">

import { useMainStore } from '@/store/index'
// 当store中的多个参数需要被使用到的时候,为了更简洁的使用这些变量,采用结构的方式一次性获取所有的变量名
// ES传统方式解构(能获取到值,但是不具有响应性)
// Pinia解构方法:storeToRefs
import { storeToRefs } from 'pinia'
const store = useMainStore()
// 解构store中的值
const { msg, count } = storeToRefs(store)

const addCount = () => {
  // 通过`store.属性名`来直接修改值
  store.count++
  // 通过store.方法名来调用action中的方法
  store.changeState()
}
</script>

手撕代码篇

防抖

function debounce(fn, wait) {
  let timer = null;
  return function() {
    let context = this,
      args = [...arguments];
    // 如果此时存在定时器的话,则取消之前的定时器重新记时
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
    // 设置定时器,使事件间隔指定事件后执行
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, wait);
  };
}

节流

// 定时器版
function throttle (fun, wait){
  let timer = null
  return function(){
    let context = this
    let args = [...arguments]
    if(!timer){
      timer = setTimeout(() => {
        fun.apply(context, args)
        timer = null 
      }, wait)
    }
  }
}

3、深拷贝

function deepClone(obj) {
    if (typeof obj !== 'object') return;
    let newObj = obj instanceof Array ? [] : {};
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
        }
    }
    return newObj;
}

4、数组扁平化

function flatten(arr) {
    let result = [];
    for (let i = 0; i < arr.length; i++) {
        if (Array.isArray(arr[i])) {
            result = result.concat(flatten(arr[i]))
        } else {
            result.push(arr[i])
        }
    }
    return result;
}

事件总线(发布订阅模式)

class EventEmitter {
    constructor() {
        this.cache = {}
    }
    on(name, fn) {
        if (this.cache[name]) {
            this.cache[name].push(fn)
        } else {
            this.cache[name] = [fn]
        }
    }
    off(name, fn) {
        let tasks = this.cache[name]
        if (tasks) {
            const index = tasks.findIndex(f => f === fn || f.callback === fn)
            if (index >= 0) {
                tasks.splice(index, 1)
            }
        }
    }
    emit(name, once = false, ...args) {
        if (this.cache[name]) {
            // 创建副本,如果回调函数内继续注册相同事件,会造成死循环
            let tasks = this.cache[name].slice()
            for (let fn of tasks) {
                fn(...args)
            }
            if (once) {
                delete this.cache[name]
            }
        }
    }
}
​
// 测试
let eventBus = new EventEmitter()
let fn1 = function(name, age) {
    console.log(`${name} ${age}`)
}
let fn2 = function(name, age) {
    console.log(`hello, ${name} ${age}`)
}
eventBus.on('aaa', fn1)
eventBus.on('aaa', fn2)
eventBus.emit('aaa', false, '布兰', 12)
// '布兰 12'
// 'hello, 布兰 12'

AJAX

const getJSON = function(url) {
    return new Promise((resolve, reject) => {
        const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
        xhr.open('GET', url, false);
        xhr.setRequestHeader('Accept', 'application/json');
        xhr.onreadystatechange = function() {
            if (xhr.readyState !== 4) return;
            if (xhr.status === 200 || xhr.status === 304) {
                resolve(xhr.responseText);
            } else {
                reject(new Error(xhr.responseText));
            }
        }
        xhr.send();
    })
}

实现 instanceof 关键字

function myInstanceOf (left, right) {
  let proto = Object.getPrototypeOf(left)
  let prototype = right.prototypewhile (true) {
    if (proto === null) return false
​
    if (prototype === proto) return true
​
    proto = Object.getPrototypeOf(proto)
  }
}

Promise.all

function promiseAll(promises) {
  if(!Array.isArray(promises)) {
        return console.log('传入参数必须是一个数组')               
    }
    return new Promise((resolve,reject) => {
        let length = promises.length // 缓存一下有多少个promise
        let count = 0 // 用于记录resolve的数量
        let results = new Array(length) // 用于存储resolve返回的值
        for(let i = 0; i < length; i++) {
            let promise = promises[i]
            // 判断数组的每一项,如果是promise,就进入then,不是就直接放进values数组中返回
            if(promise.then) {
                promise.then(res => {
                    // 记录promise完成的数量
                    count++
                    // values存储每一个promise的res
                    results[i] = res
                    // 由于异步代码在最后执行,我们需要在then里面判断promise的完成数量,全部完成就resolve
                    // 在for外面判断,是防止它全部都不是promise实例
                    if(count === length) {
                        resolve(results)
                    }
                }).catch(err =>{
                    // 当有一个promise实例reject,我们就直接reject
                    reject(err)
                })
            } else { // 针对不是promise实例
                count++
                results[i] = promise
                if (count === length) {
                  resolve(result)
                }
            }
        }
    })
}

class 类

const getData = () => {
  class Person {
    constructor(name) {
      this.name = name
    }
​
    getName () {
      return this.name
    }
  }
​
  class Child extends Person {
    constructor(name, age) {
      super(name)
      this.age = age
    }
  }
​
  let p1 = new Person()
  p1.__proto__ === Person.prototype
  p1.constructor === Personlet c1 = new Child()
  c1.__proto__.__proto__ === Person.prototype
  Child.prototype.__proto__ === Person.prototype
​
}

apply 函数

Function.prototype.myApply = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  let result = null;
  // 判断 context 是否存在,如果未传入则为 window
  context = context || window;
  // 将函数设为对象的方法
  context.fn = this;
  // 调用方法
  if (arguments[1]) {
    result = context.fn(...arguments[1]);
  } else {
    result = context.fn();
  }
  // 将属性删除
  delete context.fn;
  return result;
};

call函数

Function.prototype.myCall = function(context) {
  // 判断调用对象
  if (typeof this !== "function") {
    console.error("type error");
  }
  // 获取参数
  let args = [...arguments].slice(1),
    result = null;
  // 判断 context 是否传入,如果未传入则设置为 window
  context = context || window;
  // 将调用函数设为对象的方法
  context.fn = this;
  // 调用函数
  result = context.fn(...args);
  // 将属性删除
  delete context.fn;
  return result;
};

bind函数

Function.prototype.myBind = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  // 获取参数
  let args = [...arguments].slice(1),
    fn = this;
  return function Fn() {
    // 根据调用方式,传入不同绑定值
    return fn.apply(
      this instanceof Fn ? this : context,
      args.concat(...arguments)
    );
  };
};

项目篇

大型文件分片上传

文件资源云盘应用的功能类似于百度网盘,它提供了文件管理的能力,我们把文件类型分为视频、图片及其他三类,提供了上传、下载、生成链接、分享、删除等功能,对这些上传的文件会有一些权限处理,除了提供对文件的管理外,还有发布公告和查看监控点视频功能。

1、使用webworker多线程计算文件的MD5值,请求后端校验接口,判断文件是否上传

2、若已经全部上传,则直接返回文件的url,前端实现秒传,若部分上传,则返回以上传的文件切片索引,这个文件切片索引也可以使用loacalStorage存储在前端,MD5值作为key值,索引数组使用JSON.stringfly()转换成字符串

3、使用file.slice方法将文件分片,如果之前已经上传了部分切片,筛选掉已经上传的分片文件,之后进行上传切片操作,如果同时发送多个请求,很容易达到浏览器的瓶颈,所以需要对请求进行并发处理,使用for循环控制并发的初始并发数,如果某个切片上传失败,则重新上传,设置了三次重新上传的机会,使用axios的onUploadProgress事件来获取上传文件进度

4、文件上传时,还可以进行暂停,继续,取消上传操作,暂停是使用axios上传的时候可以new一个cancelToken的对象,返回一个取消函数,点击暂停的时候可以取消当前切片的上传,点击继续的话就把未上传的切片继续上传,和断点续传差不多,取消上传的话就是暂停当前正在上传的切片,从列表中移除上传文件

5、当切片全部上传完毕后,就需要进行文件的合并,请求后端进行文件合并并返回文件URL

Web Worker

Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。

基本用法
主线程:
// 1、新建worker对象
let worker = new Worker('worker.js', { name : 'myWorker' })
​
// 2、主线程调用worker.postMessage()方法,向 Worker 发消息
worker.postMessage({method: 'echo', args: ['Work']})
​
// 3、主线程通过worker.onmessage指定监听函数,接收子线程发回来的消息。通过 event.data 可以获取 Worker 子线程发过来的数据
​
worker.onmessage = function (event) {
  doSomething(event.data);
}
function doSomething() {
  ...
}
​
 // 4、Worker 完成任务以后,主线程就可以把它关掉。
 worker.terminate()
 
 // 5、主线程可以监听 Worker 是否发生错误。如果发生错误,Worker 会触发主线程的error事件。Worker 内部也可以监听error事件。
 worker.addEventListener('error', function (event) {
   console.log(
    'ERROR: Line ', event.lineno, ' in ', event.filename, ': ', event.message
  )
});
Worker 线程
// 1、Worker 线程内部需要有一个监听函数,监听message事件。通过 e.data 可以获取主线程发过来的数据。
self.addEventListener('message', function (e) {
  doSomething(e.data)
}, false)
function doSomething() {
  ...
}
​
// 2self.postMessage()方法用来向主线程发送消息。
self.postMessage(...)
​
// 3、Worker 也可以关闭自身
self.close()

监控点运维考核

提供了公安用户、监理用户、运营商用户三种角色的权限控制,提供对设备抓拍图片、录像的进行自动巡检,产生对应的扣分、扣款信息,监理用户、公安用户可以手动扣分、扣款,运营商用户可以对自动扣款记录发起申诉,监理、公安用户审核申诉。

30万条数据导出excel浏览器报错问题

返回的文件格式我们还是指定为 blob 的格式内容

1、通过 fileReader 对象来读取后台返回的流信息转换后的 blob 对象值,该对象读取文件信息成功之后会在 onload 方法中范围该文件的 base64 编码格式。转换成功之后再通过动态创建 a 标签,指定download属性完成下载。此方法的特点也在上文中提过,文件或者信息转换成 base64 之后占据内存空间会较大,如果数据总量不是很大的话可能还看不出来什么问题,如果数据总量较大的话就会直接导致内存溢出。

.then((response) => {
       console.log(response)
       const fileReader = new FileReader() // FileReader 对象允许Web应用程序异步读取存储在用户计算机上的文件的内容
       fileReader.readAsDataURL(BLOB)
       // 开始读取指定的Blob中的内容。一旦完成,result属性中将包含一个data: URL格式的Base64字符串以表示所读取文件的内容
       fileReader.onload = (event) => {
         // 处理load事件。该事件在读取操作完成时触发
         // 新建个下载的a标签,完成后移除。
         const a = document.createElement('a')
         // 构造下载文件名称映射字典表
         const downloadMap = {
           0: '感知全量点位匹配情况表.xlsx',
           1: '感知考核点位匹配情况表.xlsx'
         }
         // 给a标签的 download 属性赋值,设置下载文件名称
         a.download = downloadMap[this.type]
         a.href = event.target.result
         document.body.appendChild(a)
         a.click()
         document.body.removeChild(a)
       }
    })

2、通过原生的 URL 对象的 createObjectURL 方法。该方法的特点是接收参数必须是 blob 或者 file 文件格式,他返回的是一个类似 URL 地址。此处是重点,这里和之前 fileReader最大的区别就是,不管你的blob对象或者file对象有多大,此处都只是生成一个指向性的地址信息而已。完美解决了动态导出数据量很大的问题。不过该方法的一个缺点就是创建后的URL地址如果不进行释放,会一直停留在内存中,所以我们再使用完成之后,需要调用revokeObjectURL方法完成资源释放。到此问题完美解决。

downloadExportFile (formData) {
  this.$message({
    type: 'info',
    message: '文件正在导出,数据量可能较大;请耐心等待..'
  })
​
  const exportUrl = process.env.NODE_ENV === 'development' ? '/devApi/camdosmatch/service/exportExcel' : '/camdosmatch/service/exportExcel'
  axios
    .post(exportUrl, formData, { responseType: 'blob' })
    .then((response) => {
       console.log(response)
       const a = document.createElement('a')
       // 构造下载文件名称映射字典表
       const downloadMap = {
         0: '感知全量点位匹配情况表.xlsx',
         1: '感知考核点位匹配情况表.xlsx'
       }
       // 给a标签的 download 属性赋值,设置下载文件名称
       a.download = downloadMap[this.type];
​
       const BLOB = response.data; // Blob 对象表示一个不可变、原始数据的类文件对象(File 接口都是基于Blob)
       a.href = window.URL.createObjectURL(BLOB);
       document.body.appendChild(a);
       a.click();
       window.URL.revokeObjectURL(a.href);
       document.body.removeChild(a);
    })
    // eslint-disable-next-line handle-callback-err
    .catch((error) => {
       console.log('文件导出失败')
       this.$message({
         type: 'error',
         message: '文件导出失败!'
       })
     })
}

虚拟列表

实现:在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。

  • 计算当前可视区域起始数据索引(startIndex)
  • 计算当前可视区域结束数据索引(endIndex)
  • 计算当前可视区域的数据,并渲染到页面中
  • 计算startIndex对应的数据在整个列表中的偏移位置startOffset并设置到列表上

列表总高度listHeight = listData.length * itemSize

可显示的列表项数visibleCount = Math.ceil(screenHeight / itemSize)

数据的起始索引startIndex = Math.floor(scrollTop / itemSize)

数据的结束索引endIndex = startIndex + visibleCount

列表显示数据为visibleData = listData.slice(startIndex,endIndex)

偏移量startOffset = scrollTop - (scrollTop % itemSize);

当滚动后,由于渲染区域相对于可视区域已经发生了偏移,此时我需要获取一个偏移量startOffset,通过样式控制将渲染区域偏移至可视区域中。

​
  
  data() {
    return {
      //可视区域高度
      screenHeight:0,
      //偏移量
      startOffset:0,
      //起始索引
      start:0,
      //结束索引
      end:null,
    };
  },
  mounted() {
    this.screenHeight = this.$el.clientHeight;
    this.start = 0;
    this.end = this.start + this.visibleCount;
  },
    computed:{
    //列表总高度
    listHeight(){
      return this.listData.length * this.itemSize;
    },
    //可显示的列表项数
    visibleCount(){
      return Math.ceil(this.screenHeight / this.itemSize)
    },
    //偏移量对应的style
    getTransform(){
      return `translate3d(0,${this.startOffset}px,0)`;
    },
    //获取真实显示列表数据
    visibleData(){
      return this.listData.slice(this.start, Math.min(this.end,this.listData.length));
    }
    },
  methods: {
    scrollEvent() {
      //当前滚动位置
      let scrollTop = this.$refs.list.scrollTop;
      //此时的开始索引
      this.start = Math.floor(scrollTop / this.itemSize);
      //此时的结束索引
      this.end = this.start + this.visibleCount;
      //此时的偏移量
      this.startOffset = scrollTop - (scrollTop % this.itemSize);
    }
  }
};

el-table自适应列宽

 产品想要的效果是:内容保持单行显示,列间距保持一致,表格超出容器允许水平滚动。

动态计算内容宽度,但是是根据实际渲染后的 DOM 元素宽度去计算。渲染后的每个单元格有个.cell类,用white-space: nowrap; overflow: auto;设置为不允许换行,内容超出后可滚动,同时设置display: inline-block;以便计算实际内容宽度。这样,最终的宽度可通过.cell元素的scrollWidth属性得到。

function adjustColumnWidth(table) {
  const colgroup = table.querySelector("colgroup");
  const colDefs = [...colgroup.querySelectorAll("col")];
  colDefs.forEach((col) => {
    const clsName = col.getAttribute("name");
    const cells = [
      ...table.querySelectorAll(`td.${clsName}`),
      ...table.querySelectorAll(`th.${clsName}`),
    ];
    // 忽略加了"leave-alone"类的列
    if (cells[0]?.classList?.contains?.("leave-alone")) {
      return;
    }
    const widthList = cells.map((el) => {
      return el.querySelector(".cell")?.scrollWidth || 0;
    });
    const max = Math.max(...widthList);
    const padding = 32;
    table.querySelectorAll(`col[name=${clsName}]`).forEach((el) => {
      el.setAttribute("width", max + padding);
    });
  });
}

在什么时候触发列宽计算呢?自然是组件渲染完成后。为了方便重用,我采用了 Vue 自定义指令的方式。

Vue.directive("fit-columns", {
  update() {},
  bind() {},
  inserted(el) {
    setTimeout(() => {
      adjustColumnWidth(el);
    }, 300);
  },
  componentUpdated(el) {
    el.classList.add("r-table");
    setTimeout(() => {
      adjustColumnWidth(el);
    }, 300);
  },
  unbind() {},
});

管理系统权限管理

1. 路由权限设置

情景:当用户没有登录,直接访问页面时,重定向到登录页登录

思路:在路由全局前置钩子中,增加鉴权功能,从localStorage中获取token,如果有则跳转到对应路由,没有的话则重定向到登陆页

import { ACCESS_TOKEN, ROLE, MENUS } from "../config/constant";

router.beforeEach((to, from, next) => {
  if (to.path === "/login") {
    //在登录页清除存储信息
    localStorage.removeItem(ACCESS_TOKEN);
    localStorage.removeItem(ROLE);
    localStorage.removeItem(MENUS);
  }
  let token = localStorage.getItem(ACCESS_TOKEN);
  //没有token,则重定向到登录页
  if (!token && to.path !== "/login") {
    next({
      path: "/login",
    });
  } else {
    next();
  }
});

2. 接口权限设置

情景:调用接口时,当token过期时,需用户重新登录

思路:在请求拦截器中,将token添加到请求头中;后端判断token是否过期,过期返回过期状态码,响应拦截器根据状态码跳转到登录页;在响应拦截器中,判断状态码决定是否跳转到登录页

const http = axios.create({
  timeout: 20000,
  withCredentials: true,
  headers: { 'X-Requested-With': 'XMLHttpRequest' },
})

// 请求拦截器
http.interceptors.request.use(
  (config) => {
    let { headers } = config;
    const tk = getToken();
    tk &&
      Object.assign(headers, {
        Authorization: tk,
      });
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 相应拦截器
http.interceptors.response.use(
  function (response) {
    let { data } = response;
    if (
      data.returncode === 106 ||
      data.returncode === 104
    ) {
      router.push("/login");
      //清除动态路由缓存
      location.reload();
    }
    return Promise.resolve({
      code: response.data.code,
      msg: response.data.msg,
      data: response.data.data,
    })
  },
  function (error) {
    return Promise.reject(error)
  }
)

登录页

// 登录
const onSubmit = async () => {
  const { data } = await API.getUserInfo(form)
  setToken(data.token)
  setRoot(data.username)
  if (data.username === 'admin') {
    router.addRoute("home", manageRoute)
  }
  router.push("/home");
}

访问http://127.0.0.1:8080/login 进入登录页,登录之后过5s在首页点击获取数据按钮(token在服务端上设置了5s的过期时间),后端判断token是否过期,过期返回过期状态码,响应拦截器根据状态码跳转到登录页

3. 菜单栏权限设置

情景:不同级别用户看到不同菜单栏

思路:前端通过返回的菜单栏列表,去封装一个新的菜单栏数组

4. 动态路由设置

情景:管理员能访问管理页面路由,非管理员不能访问该路由

思路:通过router.addRoute添加动态路由

动态路由

//动态路由
const manage = {
  path: 'manage',
  name: 'manage',
  component: () => import('@/pages/manage/Index.vue'),
}

export default manage

添加动态路由

import manageRoute from '../router/manageRoute'
//如果是管理员,添加管理员页面路由
if (res.data.role === "admin") {
  router.addRoute("Layout", manageRoute);
}

解决刷新页面,动态路由丢失问题

原因:刷新页面,路由初始化,动态路由会丢失

思路:通过监听路由的变化,当刷新时,添加动态路由并定位到管理页面

import manage from "@/router/dynamicRoute"

export default {
  watch: {
    $route: {
      async handler (newVal) {
        const root = getRoot()
        if (root && root === "admin") {
          /* 在4.x版本中需手动调用router.replace方法重定向,
          因为动态路由页面刷新时,matched的值为空;
          在3.x版本中,刷新页面添加异步路由,matched有值,不需要再重定向 */
          this.$router.addRoute("home", manage)
          /* 在动态路由页面刷新时,matched数组为空 */
          if (!newVal.matched.length && newVal.fullPath === "/home/manage") {
            await this.$router.replace("/home/manage")
          }
        }
      },
    },
  },
};

5. 按钮权限设置

情景:根据不同的用户,一些页面功能进行显示或者隐藏

思路:在路由元信息上定义权限信息,通过自定义指令删除一些DOM节点

  1. 定义路由元信息(上面routes.js中已经添加了)
{
  path: "/about",
  name: "About",
  component: About,
  meta: {
      btnPermissions: ['admin']
  },
},
复制代码
  1. 增加判断方法

新建src/utils/index.js

import { ROLE } from "../config/constant";

// 权限检查方法
export function has(value) {
  let isExist = false;
  // 获取用户按钮权限
  let btnPermissionsStr = localStorage.getItem(ROLE);

  if (btnPermissionsStr == undefined || btnPermissionsStr == null) {
    return false;
  }

  if (value.indexOf(btnPermissionsStr) > -1) {
    isExist = true;
  }
  return isExist;
}
复制代码
  1. 新建自定义指令

修改src/main.js

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";

import { has } from "./utils";

const app = createApp(App);

app.directive("has", {
  mounted(el) {
    // 获取页面按钮权限
    const btnPermissionsArr = router.currentRoute._value.meta.btnPermissions;
    if (!has(btnPermissionsArr)) {
      if (el.parentNode) {
        el.parentNode.removeChild(el);
      }
    }
  },
});

app.use(router).mount("#app");
复制代码
  1. 在about页面使用v-has指令

修改src/views/About.vue

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <button type="button" v-has>管理员按钮</button>
  </div>
</template>

websocket

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。 websocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

  handleStart(id) {
    // 判断浏览器是否支持websocket
    var CreateWebSocket = (function () {
      return function (urlValue) {
        if (window.WebSocket) return new WebSocket(urlValue);
        if (window.MozWebSocket) return new MozWebSocket(urlValue);
        return false;
      }
    })()
    // 创建一个websocket
    var webSocket = CreateWebSocket("ws://127.0.0.1:3000/websocket/" + id);
    // 与服务端建立连接触发
    webSocket.onopen = function (evt) {
        // 主动向后台发送数据
          webSocket.send("前端向后端发送第一条数据")
    }
    // 监听websocket通讯,获取服务端返回的数据
    webSocket.onmessage = function (evt) {
        // 这是服务端返回的数据
        let res = JSON.parse(evt.data)
        if(res.n > 0.99) {
          // 前端主动关闭连接
          webSocket.close()
        }
    }
    // 向服务端发送数据
    webSocket.send(JSON.stringify(json))
    // 监听连接关闭
    webSocket.onclose = function (evt) {
        console.log("Connection closed.")
    }
  }

实时聊天过程: 1、websocket连接后,长时间远端和客户端不发消息,服务端会把websocket给断开; 2、所以就需要一种机制来检测客户端和服务端是否处于正常的链接状态。 3、因此就有了websocket的‘心跳监测’。 4、还有心跳,说明还活着,没有心跳说明已经断开了。

心跳机制是每隔一段时间会向服务器发送一个数据包,告诉服务器自己还活着,同时客户端会确认服务器端是否还活着,如果还活着的话,就会回传一个数据包给客户端来确定服务器端也还活着,否则的话,有可能是网络断开连接了。需要重连~

心跳的实现也很简单,只需在第一次连接时用回调函数做延时处理,此时还需要设置一个心跳超时时间,若某时间段内客户端发送了消息,而服务端未返回,则认定为断线。

postmessage

postMessage是html5引入的API,postMessage()方法允许来自不同源的脚本采用异步方式进行有效的通信,可以实现跨文本文档,多窗口,跨域消息传递.多用于窗口间数据通信,这也使它成为跨域通信的一种有效的解决方案.

使用postmessage进行跨域通信

父窗体创建跨域iframe并发送信息

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <title>跨域POST消息发送</title>
        <script type="text/JavaScript">    
            // sendPost 通过postMessage实现跨域通信将表单信息发送到 moweide.gitcafe.io上,
            // 并取得返回的数据    
            function sendPost() {        
                // 获取id为otherPage的iframe窗口对象        
                var iframeWin = document.getElementById("otherPage").contentWindow;        
                // 向该窗口发送消息        
                iframeWin.postMessage(document.getElementById("message").value, 
                    'http://moweide.gitcafe.io');    
            }    
            // 监听跨域请求的返回    
            window.addEventListener("message", function(event) {        
                console.log(event, event.data);    
            }, false);
        </script>
    </head>
    <body> 
        <textarea id="message"></textarea> 
        <input type="button" value="发送" onclick="sendPost()"> 
        <iframe
            src="http://moweide.gitcafe.io/other-domain.html" id="otherPage"
            style="display:none"></iframe>
    </body>

</html>
复制代码

子窗体接收信息并处理

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <title>POST Handler</title>
        <script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
        <script type="text/JavaScript">
            window.addEventListener("message", function( event ) {
                // 监听父窗口发送过来的数据向服务器发送post请求
                var data = event.data;
                $.ajax({
                    // 注意这里的url只是一个示例.实际练习的时候你需要自己想办法提供一个后台接口
                    type: 'POST', 
                    url: 'http://moweide.gitcafe.io/getData',
                    data: "info=" + data,
                    dataType: "json"
                }).done(function(res){        
                    //将请求成功返回的数据通过postMessage发送给父窗口        
                    window.parent.postMessage(res, "*");    
                }).fail(function(res){        
                    //将请求失败返回的数据通过postMessage发送给父窗口        
                    window.parent.postMessage(res, "*");    
                });
            }, false);
        </script>
    </head>

    <body></body>
</html>