沉淀

107 阅读41分钟

JavaScript

一、 JavaScript 的数据类型有哪些?

一共有 8 种,其中 7 种是基本数据类型,1 种 是引用数据类型

基本数据类型:Number、String、Boolean、Null、Undefined、Bigint、Symbol

Symbol 提供的实例是唯一、不可变的。它的用途可以确保对象属性使用唯一标识符,不会发生属性冲突的危险。 Symbol 提供 Symbol() 函数来返回 Symbol 类型的值。语法如下所示:Symbol(desc)

引用数据类型:Object

包括 Array、Date、Regep 正则等

二、 如何判断 JavaScript 的数据类型?

typeof

typeof 是一个操作符而不是函数。typeof 总是返回一个字符串,用来说明变量的数据类型。

// 数值
typeof 37 === "number";
typeof NaN === "number";
typeof 42n === "bigint";

// 字符串
typeof "1" === "string"; 

// 布尔值
typeof true === "boolean";

// Symbols
typeof Symbol("foo") === "symbol";

// Undefined
typeof undefined === "undefined";

// Null
typeof null=== "object"; // JavaScript 诞生以来便如此

// 对象
typeof { a: 1 } === "object";
typeof [1, 2, 4] === "object";
typeof new Date() === "object";
typeof /regex/ === "object";

// 函数
typeof function () {} === "function";

typeof 原理

在JavaScript中,所有数值类型在底层都是以二进制形式表示的,由一个表示类型的标签和实际数据值表示的,前三位存储其类型信息。

二进制中的“前”一般代表低位, 比如二进制00000011对应十进制数是3,它的前三位是011。

  • 000: 对象
  • 010: 浮点数
  • 100:字符串
  • 110:布尔
  • 1:整数

typeof就是通过机器码判断类型的,因为对象的二进制表达前三位都是000,所以typeof无法区分。 由于 null 代表的是空指针(大多数平台下值为 0x00),因此null 的类型标签是 0,typeof null 也因此返回 “object”。

typeof 可以对除了 null 的基本数据类型做出准确的判断外,不能判断对象(如数组,正则等)具体是哪种类型,返回值都为“object”(除了Function会被识别出来),但是这些对象可以通过Object.prototype.toString.call() 查看内部属性。

instance of

语法:object instanceof constructor

instanceof 是用来检测构造函数的 prototype 属性是否出现在某个实例对象(参数)的原型链上 通俗一些讲,instanceof 运算符用来检测一个对象是否为某一个构造函数的实例。注意,instance of只能用于对象,不适用原始类型的值。

// 定义构造函数
function C() {}
function D() {}

var o = new C();

o instanceof C; // true,因为 Object.getPrototypeOf(o) === C.prototype

o instanceof D; // false,因为 D.prototype 不在 o 的原型链上

o instanceof Object; // true,因为 Object.prototype.isPrototypeOf(o) 返回 true
C.prototype instanceof Object; // true,同上

C.prototype = {};
var o2 = new C();

o2 instanceof C; // true

o instanceof C; // false,C.prototype 指向了一个空对象,这个空对象不在 o 的原型链上。

D.prototype = new C(); // 继承
var o3 = new D();
o3 instanceof D; // true
o3 instanceof C; // true 因为 C.prototype 现在在 o3 的原型链上

但是 instanceof 也存在缺陷,就是它不能精确的判断 Object 类的具体数据类型。例如:

[] instanceof Array; // true, Object.getPrototypeOf([]) === Array.prototype
 
function A() {};
let a = new A();
a instanceof A ; // true,同上
 
[] instanceof Object; // true, 这是什么原因呢?
 
a instanceof Object; // true, 这是什么原因呢?

从上面的例子可以发现 [ ] 通过 instanceof 可以判断出 [ ] 是 Array 的实例对象,但是不能辨别 [ ] 不是 Object 的实例对象,通过构造函数也是这个效果,造成这样的原因:主要与原型链有关:[ ]. __ proto __ 指向的是 Array.prototype,而 Array.prototype.__ proto __ 指向的是 Object.prototype,Object.prototype.__ proto __ 指向了 null。同理 a.__ proto __ 指向的是Function.prototype,而 Function.prototype.__ proto __ 指向了 Object.prototype。

Object.prototype.toString.call()

这是对象的一个原生原型扩展函数,用来更精确的区分数据类型。 对于 Object.prototype.toString() 方法,调用该方法,返回统一的格式"[object xxxx]"的字符串。如果对象的 toString() 方法未被重写,就会返回如上面形式的字符串。 但是,大多数对象,toString() 方法都是重写了的,这时,需要用 call() 方法来调用。

Object.prototype.toString({});  // "[object Object]"
Object.prototype.toString.call({});  // "[object Object]"
Object.prototype.toString.call(1);  // "[object Number]"
Object.prototype.toString.call('1');  // "[object String]"
Object.prototype.toString.call(true);  // "[object Boolean]"
Object.prototype.toString.call(function(){});  // "[object Function]"
Object.prototype.toString.call(null);  // "[object Null]"
Object.prototype.toString.call(undefined);  // "[object Undefined]"
Object.prototype.toString.call(/123/g);  // "[object RegExp]"
Object.prototype.toString.call(new Date());  // "[object Date]"
Object.prototype.toString.call([]);  // "[object Array]"
Object.prototype.toString.call(document);  // "[object HTMLDocument]"
Object.prototype.toString.call(window);  // "[object Window]"

三、JS 原型链,原型链的顶端是什么?Object 的原型是什么?Object 的原型的原型又是什么

原型、原型链相等关系理解

  • 首先我们要清楚明白两个概念:
  1. js分为函数对象普通对象,每个对象都有__proto__属性,但是只有函数对象才有prototype 属性
  2. Object、Function 都是 js 内置的函数, 类似的还有我们常用到的Array、RegExp、Date、Boolean、Number、String
  • 那么__proto__和 prototype 到底是什么,两个概念理解它们:
  1. 属性__proto__是一个对象,它有两个属性,constructor和__proto__
  2. 原型对象 prototype 有一个默认的 constructor 属性,用于记录实例是由哪个构造函数创建;
function Person(name, age){ 
  this.name = name; 
  this.age = age; 
} 
Person.prototype.motherland = 'China'
let person01 = new Person('小明', 18);

js之父在设计js原型、原型链的时候遵从以下两个准则:

  1. Person.prototype.constructor == Person // 准则1:原型对象(即Person.prototype)的 constructor 指向构造函数本身
  2. person01.__ proto __ == Person.prototype // 准则2:实例(即person01)的__proto__和原型对象指向同一个地方
// 从上方 function Foo() 开始分析这一张经典之图
function Foo()
let f1 = new Foo();
let f2 = new Foo();

f1.__proto__ = Foo.prototype; // 准则2
f2.__proto__ = Foo.prototype; // 准则2
Foo.prototype.__proto__ = Object.prototype; // 准则2 (Foo.prototype本质也是普通对象,可适用准则2)
Object.prototype.__proto__ = null; // 原型链到此停止
Foo.prototype.constructor = Foo; // 准则1
Foo.__proto__ = Function.prototype; // 准则2
Function.prototype.__proto__  = Object.prototype; //  准则2 (Function.prototype本质也是普通对象,可适用准则2)
Object.prototype.__proto__ = null; // 原型链到此停止
// **此处注意Foo 和 Function的区别, Foo是 Function的实例**

// 从中间 function Object()开始分析这一张经典之图
function Object()
let o1 = new  Object();
let o2 = new  Object();

o1.__proto__ = Object.prototype; // 准则2
o2.__proto__ = Object.prototype; // 准则2
Object.prototype.__proto__ = null; // 原型链到此停止
Object.prototype.constructor = Object; // 准则1
// 所有函数的__proto__  都和 Function.prototype指向同一个地方
Object.__proto__ = Function.prototype // 准则2 (Object本质也是函数);
// 此处有点绕
Function.prototype.__proto__ =  Object.prototype; // 准则2 (Function.prototype本质也是普通对象,可适用准则2)
Object.prototype.__proto__ = null; // 原型链到此停止

// 从下方 function Function()开始分析这一张经典之图
function Function()
Function.__proto__ = Function.prototype // 准则2
Function.prototype.constructor = Function; // 准则1

由此可以得出结论: 除了 Object 的原型对象(Object.prototype)的__proto__指向null,其他内置函数对象的原型对象(例如:Array.prototype)和自定义构造函数的 __proto__都指向Object.prototype, 因为原型对象本身是普通对象。 即:

Object.prototype.__proto__ = null;
Array.prototype.__proto__ = Object.prototype;
Foo.prototype.__proto__  = Object.prototype;

总结

每一个对象从被创建开始就和另一个对象关联,从另一个对象上继承其属性,这个另一个对象就是 原型。比如说JavaScript中当使用构造函数来新建一个对象时,这个对象有一个属性 __ proto__ 指向一个对象,就是这个对象的原型,也就是构造函数的 prototype。每个对象的原型都可以通过 constructor 找到构造函数,构造函数也可以通过 prototype 找到原型

所有函数都可以通过 __ proto __ 找到 Function 对象

所有对象可以通过 __ proto __ 找到 Object 对象

对象之间通过 __ proto __ 连接起来的链条称为原型链。

当访问一个对象的属性时,先在对象的本身找,找不到就去对象的原型上找,如果还是找不到,就去对象的原型(原型也是对象,也有它自己的原型)的原型上找,如此继续,直到找到为止,或者查找到最顶层的原型对象中也没有找到,就结束查找,返回undefinedObject.prototype 是所有对象的最终原型,它位于原型链的顶端。这意味着无论你创造了多少个对象,它们的原型最终都会经过 Object.prototype,并且 Object.prototype 本身的原型是 null, 表示原型的终点。

原型存在的意义就是组成原型链:引用类型皆对象,每个对象都有原型,原型也是对象,也有它自己的原型,一层一层,组成原型链。

原型链存在的意义就是继承:访问对象属性时,在对象本身找不到,就在原型链上一层一层找。说白了就是一个对象可以访问其他对象的属性。

继承存在的意义就是属性共享:好处有二:一是代码重用,字面意思;二是可扩展,不同对象可能继承相同的属性,也可以定义只属于自己的属性。

image.png

四、怎么判断两个对象相等?如何判断空对象?

  • 判断两个对象相等
function deepEqual(obj1, obj2) {
  if (obj1 === obj2) return true; // 同一引用或都为 null/undefined
  if (obj1 == null || obj2 == null || typeof obj1 !== 'object' || typeof obj2 !== 'object') return false;
  
  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);
  if (keys1.length !== keys2.length) return false;

  for (const key of keys1) {
    if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {
      return false
    }
  }
  return true
}

// or

// 将两个对象使用 JSON.stringify() 后使用 === 号判断
  • 如何判断空对象
Object.keys().length === 0 

// or

JSON.stringify(obj) === '{}'

五、0.1 + 0.2 为什么不等于 0.3?

精度丢失的原因造成的。

第一次精度丢失

js 中的 Number 类型的数据是双精度浮点型保存在内存中的,遵循的是 ieee754 标准。由于 0.1 0.2 转化为二进制是无限循环的内存是有限的(52 位)。所以将 0.1 和 0.2 转化成二进制这里就造成了一次精度丢失(为 1 则进 1 为 0 则舍去)。 先看一下 0.1 和 0.2 转成为二进制是什么样的。小数转二进制是采用"乘2取整"的方式进行转换的,

  1. 0.1转化为二进制为0.000110011001100(无限循环)
  2. 0.2转化为二进制为0.00110011001100(无限循环)

因为转成二进制后是无限循环小数,计算机在存储的时候会存储为一个近似值,所以在转二进制这个过程中就造成了精度丢失。

第二次精度丢失

转化为二进制的数字相加; 转成二进制相加的过程,在 IEEE754 的标准中,浮点数要采用科学计数法计数,0.1的二进制采用科学计数法表示为1.10011001100....×2^(-4),0.2 的二进制采用科学计数法表示为 1.10011001100....×2^(-3),为了进行二进制加法,需要将两个数的指数部分对齐。为了使二者小数点对齐,所以 0.1 需要小数点需要向后移动一位,在这个过程中也会造成一次精度丢失,两者相加的值为 0.0100110011.....,同样是一个无限循环小数。然后再将这个二进制小数转成十进制。

如何解决

  1. 先扩大相对应倍数,得到结果后在除以对应的倍数,这样就可以得到正确的结果
  2. 使用 toFixed() 代码如下 注意使用 toFixed 是可以得到 0.3 的,但是运行结果还是 false 的原因是得到的结果是 String 类型的,而 0.3 是 Number 的,所以=== 为 false。
console.log(0.1 + 0.2 === 0.3); //false

console.log((0.1 * 10 + 0.2 * 10) / 10 === 0.3); //true

console.log((0.1 + 0.2).toFixed(1) === 0.3); //false

六、强制类型转换、隐式类型转换分别是什么,列举场景说明

强制类型转换 --原始值转原始值

通过 String()、Number()、Boolean()、parseInt()、parseFloat()

隐式类型转换 --对象转原始值

运算符比如 || != !== > < >=

算术运算
  1. 一元操作符 +

+号会触发隐式类型转换,往number

  1. 二元运算符 +

当参与运算的操作数不是同一类型时,某些值会被转换为数字。

  • 例如,'3' + 2 结果为 '32'(字符串连接)。只要左右两边有一个是字符串,结果都是字符串
  • 但 '3' - 2 结果为 1(字符串 '3' 被转换为数字 3)。
1 + '1' //'11'只要左右两边有一个是字符串,结果都是字符串

1 + [] // 1 + '' = '1' 存在字符串依旧按照字符串的拼接规则,而不转为数字 1 + 0

{} + [] // "[object,Object]" + '' = "[object,Object]"

比较运算

在比较操作中,不同类型的数据会被转换以进行比较。

  • 如 true == 1 结果为 true,因为在比较前 true 被转换为 1
  • null == undefined 结果为 true,两者在比较时被视为相等。
  • == 与 === 的区别
  1. == 会触发隐式类型转换
  2. === 不会触发隐式类型转换,判断类型和值都相等,严格等于
逻辑运算

在逻辑运算中,如 if 语句或 &&|| 运算符,表达式会被转换为布尔值。

  • 如 if ('text') 会执行代码块,因为非空字符串被视为 true

七、创建函数的几种方式

函数声明

function sum1(num1,num2) {
      return num1+num2
}

函数表达式

let sum2 = function(num1,num2) {
     return num1+num2
}

let sum3 = (num1, num2) => { // 箭头函数
    return num1+num2
}

函数对象方式

let sum3 = new Function('num1','num2') {
    return num1+num2
}

创建对象的几种方式

new Object()

var obj = new Object();

工厂模式创建

//工厂模式创建对象
function cObj(name,age) {

    var obj = new Object(); // 创建对象

    //添加属性
    obj.name = name;
    obj.age = age;

    //添加方法
    obj.eat = function () {
        console.log("我叫" + this.name + ",,," + this.age + "岁" + ",,," + "爱吃火锅");
    };
    return obj;
}

//创建人的对象
var luzp = cObj("小哥",27);
luzp.eat();

自定义构造函数创建对象

// Person () 其实就是一个函数
function Person(name,age) {
    this.name = name;
    this.age = age;
    this.eat = function () {
      console.log("我叫" + this.name + "和小哥认识的时候我" + this.age + "," + "我们都爱吃火锅");
    };
}

//创建对象
var lut = new Person("小甜",23);
console.log(lut.name);
lut.eat();

八、构造函数创建对象的过程(new 的过程)

  1. new 对象的时候,会在内存中申请一个空间,存储创建的新的对象
  2. 把 this 设置为当前的对象
  3. 设置对象的属性和方法的值
  4. 把 this 这个对象返回

九、列举宿主对象、内置对象、原生对象并说明其意义

宿主对象

宿主对象是由 JS 的宿主环境提供的对象,例如 Window、Document、XMLHttpRequest、console

内置对象

内置对象无需特定的宿主环境支持,例如 Object、Array、String、Date、Math

原生对象

包含所有内置对象和用户创建的任何对象.

十、 == 和 === 的区别

== 和 === 都是比较运算符

  • === 严格相等运算符,比较两个变量值是否完全相等,包括变量值和变量类型;
  • == 宽松相等运算符,先将变量类型进行转换在比较。比如 valueOf,toString()

十一、null 和 undefined 的区别

null

null 值表示一个空对象指针,通常用于赋值给可能返回一个对象的变量作为初始值

undefined

使用 var、let 声明了变量但没有赋值时,会返回 undefined

十二、什么情况下会返回 undefined 值

  1. 变量声明但没有赋值/初始化;
  2. 返回值未定义的函数,或没有返回值的函数;
  3. 访问对象或数组中不存在的属性或者元素时;
  4. 函数形参没有传值

十三、如何区分数组和对象

  • Array.isArray()
  • Object.prototype.toString.call()

十四、多维数组如何降维

forEach 递归

const newArr = []; 
const ergodic = (arr) => { 
arr.forEach((item) => { 
    if (Array.isArray(item)) { 
    ergodic(item); } 
    else { 
        newArr.push(item); 
       } 
     }) 
   } 

ergodic(oldArr, newArr); 
console.log(newArr);

reduce 递归

const ergodic = (arr) => arr.reduce((prev, curr, index, list) => { 
if (Array.isArray(curr)) { 
    return prev.concat(...ergodic(curr)); } 
    return prev.concat(curr); }, []); 
    const newArr = ergodic(oldArr); console.log(newArr);
}

es6 的 flat 或 flatMap

  • flat

数组嵌套有时候有很多层次的嵌套: 就是多层的, 但是 flat()默认只会“拉平”展开一层, 如果想要“拉平”多层的嵌套数组,可以通过将flat()方法的参数写成一个整数,表示想要拉平的层数,默认值为 1.值得注意的是:使用 flat() 拉平数组过程中,会移除数组的空项:

[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]]

[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]

[1, 2, , 4, 5].flat(); // [1, 2, 4, 5]

  • flatMap

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

十五、什么是类数组,如何将其转化为真实的数组

类数组的概念

类数组是类似数组的对象,它们具类似数组的索引属性和 length 属性,但并不是真正的数组。类数组可以使用数组的一些相关方法。如 for 循环、forEach 循环、reduce 方法,但一些数组特有的方法如 push、pop、slice 等则不能使用,需要先将类数组转化真实的数组使用。

常见的类数组

  • arguments 对象
  • HTMLCOllection 对象

HTMLCollection 表示一个包含了元素(元素顺序为文档流中的接口)的集合(通用集合),还提供了从该集合中选择元素的属性和方法。

document.getElementsByTagName('body') instanceof HTMLCollection // true

const htmlCollection = document.getElementsByTagName('body')
console.log(htmlCollection.item(0)) // <body>...</body>
console.log(htmlCollection.length()) // 1


  • NodeList 对象

NodeList 对象是节点的集合。可以通过以下方法获得:

  1. 一些旧版本浏览器中的方法(如 getElementsByClassName()),返回的是 NodeList 对象,而不是 HTMLCollection 对象。

  2. 所有浏览器的 Node.childNodes 属性返回的是 NodeList 对象。

  3. 大部分浏览器的 document.querySelectorAll() 返回 NodeList 对象。

如何转换成真实数组

Array.from()

十六、如何遍历对象的属性

for...in

Object.getOwnPropertyNames()

遍历对象的属性,不包括 Symbol 属性

Object.getOwnPropertySymbols()

遍历对象的所有 Symbols 属性

Object.keys()

属性数组集合

Object.values()

属性值数组集合

Object.entries()

属性和属性值集合,二维数组

[['name': 'Lc'], ['age': 29]]

十七、如何给一个按钮绑定两个点击事件

addEventListener

<body>
    <button class="btn1">按钮</button>
    <script>
        let btn1 = document.querySelector('.btn1')
        btn1.addEventListener('click', function () {
            alert('我是addEventListener语法注册的事件-1')//先弹出第一个
        });

        btn1.addEventListener('click', function () {
            alert('我是addEventListener语法注册的事件-2')//接着弹出第二个
        });
    </script>
</body>

十八、如何获取当前日期(年-月-日 时:分:秒)

const a = new Date()
let b = a.toLocaleString() 0000/00/00 00:00:00
b = b.replace(new RegExp(/\//g, '-')) // / 替换为 -

十九、变量提升是什么?与函数提升的区别

变量提升

变量提升(hoisting)是指在代码执行之前,变量和函数的声明会被提升到作用域的顶部。这意味着你可以在声明之前使用变量和函数,而不会引发错误。 变量声明(使用var关键字)会被提升到其所在作用域的顶部,并初始化为undefined

console.log(myVal) // undefined
var myVal = 'Hello'
console.log(myVal) // Hello


函数提升

函数声明会被提升到其所在作用域的顶部,并可以在声明之前调用。


console.log(myFunc()) // hello

function myFunc() {
    return 'hello'
}

区别

变量提升只提升声明,函数声明创建的函数会提升整个函数整体。函数提升优先级高于变量提升,且不会被同名变量声明时覆盖,但是会被同名变量赋值后覆盖

console.log(a) // ƒ a(){} 变量a赋值前打印的都会是函数a  
var a=1;  
function a(){}  
console.log(a) // 1 变量a赋值后打印的都会是变量a的值

首先变量和函数声明都提升,但函数提升优先级高于变量,都提升后变量只是定义没有赋值,所以输出的是函数a。详细过程如下:

function a(){} // 函数声明提升 a-> f a (){}  
var a; // 变量提升  
console.log(a) // 此时变量a只是声明没有赋值所以不会覆盖函数a --> 输出函数a f a (){}  
a=1; //变量赋值  
console.log(a) // 此时变量a赋值了 --> 输出变量a的值 1

总结:由于函数声明和变量都会提升,如果函数与变量同名,那么只要在变量赋值前打印的都会是函数,在变量赋值之后打印都是变量的值。

a(); // 2  
var a = function(){ // 看成是一个函数赋值给变量a(函数表达式不会被函数提升)  
    console.log(1)  
}  
a(); // 1  
function a(){  
    console.log(2)  
}  
a(); // 1

只有函数声明才会提升函数表达式不会提升,所以在函数表达式后面的代码会输出1,因为变量a赋值后把提升的函数a覆盖了。详细过程如下:

function a(){ // 函数提升  
    console.log(2)  
}  
var a; // 变量提升  
a(); // 2  
a = function(){ // 变量a赋值后覆盖上面的函数a  
    console.log(1)  
}  
a(); // 1  
a(); // 1

二十、作用域链的概念?如何延长?

概念

当我们使用一个变量时,JS 解释器会优先在当前作用域中寻找变量,如果找到了则直接使用,如果没找到,则去上一层作用域中寻找,以此类推,如果一直到全局作用域也没找到则报错 xxx is not defined。在这过程中,作用域形成了一条链式结构,这就是作用域链,闭包、eval 函数 width 语句可以延长作用域链。

如何延长

  • with 语句

with语句为了简化多次编写访问同一对象的工作,将一个特定的变量对象存储到作用域链的最上层。下面来看一个 JavaScript 高级程序设计上面的一个例子:

function buildUrl(){
    var qs = "?debug=true";
    with(location){
        var url = href + qs;
    }
    return url;
}
console.log(buildUrl());//file:///C:/Users/Administrator/Desktop/%E7%8E%8B%E8%A7%82%E5%B9%B3/cityPicker/demo1.html?debug=true
  • try catch

try-catch 语句在 JavaScript中 用来处理异常,在 catch(e){} 中的错误对象组成了一个新的变量对象然后被加到了作用域的最前端。try-catch 是个非常有用的语句,但是在使用前我们应当了解可能出现的错误。同时,我们可以简化代码来使 catch 子句对性能的影响最小化,我们可以使用一个函数来处理错误,如下面代码所示


try{
    fn();
}catch(ex){
    handleError(ex);
}

二十一、闭包

概念

闭包允许函数访问并操作函数外部的变量。红宝书上对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数。 MDN 对闭包的定义为:闭包是指那些能够访问自由变量的函数。这里的自由变量是外部函数作用域中的变量。

原因

内部的函数存在外部作用域的引用就会导致闭包。 return f就是一个表现形式。

使用场景

  1. 函数作为参数
var a = '林一一'
function foo(){
   var a = 'foo'
   function fo(){
       console.log(a)
   }
   return fo
}

function f(p){
   var a = 'f'
   p()
}
f(foo())
/* 输出 foo/ 
  1. setTimeout 传递参数
//原生的setTimeout传递的第一个函数不能带参数
setTimeout(function(param){
    alert(param)
},1000)
function a(param) {
    return function() {
        console.log(param)
    }
}
var b = a(true)
setTimeout(() => {
    b()
}, 1000)
  1. 防抖节流

// 节流
function throttle(fn, timeout) {
    let timer = null
    return function (...arg) {
        if(timer) return
        timer = setTimeout(() => {
            fn.apply(this, arg)
            timer = null
        }, timeout)
    }
}

// 防抖
function debounce(fn, timeout){
    let timer = null
    return function(...arg){
        clearTimeout(timer)
        timer = setTimeout(() => {
            fn.apply(this, arg)
        }, timeout)
    }
}

  1. 循环赋值
for(var i = 0; i < 10; i++){
  (function(j){ // 自执行
       setTimeout(function(){
        console.log(j)
    }, 1000) 
  })(i)
}

因为存在闭包的原因上面能依次输出1~10,闭包形成了10个互不干扰的私有作用域。将外层的自执行函数去掉后就不存在外部作用域的引用了,输出的结果就是连续的 10。为什么会连续输出10,因为 JS 是单线程的遇到异步的代码不会先执行(会入栈),等到同步的代码执行完 i++ 到 10时,异步代码才开始执行此时的 i=10 输出的都是 10。

二十二、事件循环

js 的事件循环就是浏览器渲染主线程的过程。

JavaScript是一种单线程语言,所有任务都在一个线程上完成。一旦遇到大量任务或者遇到一个耗时的任务,比如加载一个高清图片,网页就会出现"假死",因为JavaScript停不下来,也就无法响应用户的行为。为了防止主线程的阻塞,JavaScript 有了 同步 和 异步 的概念。所以 JavaScript 便使用一套机制去处理同步和异步操作,那就是事件循环 (Event Loop)。

  • 所有同步任务都在主线程上依次执行,形成一个执行栈(调用栈),异步任务则放入一个任务队列
  • 当执行栈中任务执行完,再去检查微任务队列里的微任务是否为空,有就执行,如果执行微任务过程中又遇到微任务,就添加到微任务队列末尾继续执行,把微任务全部执行完
  • 微任务执行完后,再到任务队列检查宏任务是否为空,有就取出最先进入队列的宏任务压入执行栈中执行其同步代码
  • 然后回到第2步执行该宏任务中的微任务,如此反复,直到宏任务也执行完,如此循环

在 js 中,任务队列中的任务又可以被分为 2 种类型:宏任务(macrotask)与微任务(microtask

1、js宏任务有:<script> 整体代码、setTimeout、setInterval、setImmediate、Ajax、DOM事件

2、js微任务有:process.nextTick、MutationObserver、Promise.then catch finally

微任务优先级比宏任务优先级高。

二十三、深拷贝浅拷贝

浅拷贝

浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本属性的值,如果属性是引用类型,拷贝的就是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一个

深拷贝

深拷贝会在堆内存创建一个完全独立的新对象,并递归复制对象内所有子对象,这样,即使原始对象中的数据发生了变化,也不会影响到深拷贝后到对象。

内存泄露

概念

内存泄漏是指程序在动态分配内存后,无法释放或回收不再需要的内存空间的现象

常见情况

未正确释放事件监听器

如果一个对象被注册为某个事件的监听器,但在不需要他时未手动移除,这将导致该对象无法被垃圾回收机制回收。例如,当使用 addEventListener 注册事件监听器时,需要使用 removeEventLister 移除对应监听器

闭包中的变量引用

闭包是指函数可以访问并持有其语法环境中的变量。如果在闭包中引用了一些不再使用的变量,这些变量将无法被及时释放。解决方法是确保不再需要的变量解除引用将其设置为 null

定时器未清理

setTimeoutsetTimeInterval 创建的定时器,如果不及时清理,会导致函数或对象在定时器仍在运行时不能被垃圾回收机制回收。确保在不需要定时器时使用 clearTimeoutclearInterval 清除

循环引用

循环引用是指两个或多个对象相互引用,形成一个闭环,导致垃圾回收器无法判断哪个对象可以被释放。解决方法是在不再需要引用时手动断开循环引用,例如将对象的引用设置为 null。

二十四、异步加载 JS 的方法

defer

script 标签添加 defer 属性,但要等 dom 文档全部解析完(dom 树生成)后才会被执行

async

script 标签添加 async 属性

script 加载不阻塞 html 解析,但一旦下载完就回立即执行,阻塞 html 解析

二十五、require 和 import 的区别

  1. require/exports 是运行时动态加载,import/export 是静态编译
  2. require 是 commondJS 语法,import 是 ES6 语法
  3. require/exports 是运行时可以根据判断条件动态加载,import/export 是静态编译
  4. require/exports 输出的是一个值的拷贝,import/export 模块输出的是值的引用
  5. import/export 只能在模块顶层使用,不能在函数、判断语句等代码块之中引用;require/exports 可以。
  6. require是同步加载模块的,当模块较多或者模块较大时,会导致应用程序的性能下降。import 是ES6 模块系统的导入方式,支持异步加载模块,可以提高应用程序的性能。

Vue

一、描述下 Vue 的生命周期有哪些?分别做了什么事情?

  • beforeCreate

在实例初始化之后,数据观察和事件监听前调用。此时组件实例(data、methods 等)还不能使用。

  • created

组件实例挂载完成,可以获得到 data 数据和 methods 里的事件。

  • beforeMount

对 dom 进行预处理,此时模板已经被编译成渲染函数,但还没有挂载到 dom 上,此时还无法访问 dom。

  • mounted

dom 挂载完成。组件也在页面呈现,可以访问和操作 dom 元素。

  • beforeUpdate

当组件数据发生变化,导致视图需要更新之前调用。在此阶段,你可以获取更新前到 Dom 状态并可以对新数据进行预处理。

  • updated

在组件重新渲染并应用更新后立即调用。此时,Dom 已经完成更新。

  • beforeDestroy

在组件实例销毁之前调用。此阶段,组件实例仍然完全正常,可以做一些副作用的清除(如定时器)。

  • destroyed

在组件实例完全销毁后调用。此阶段,组件所有的绑定事件被移除,子组件销毁,组件完全卸载。

二、什么是虚拟 Dom

虚拟 Dom 本质是个 JavaScript 对象,通过对象的形式来表示 Dom 结构。通过把页面结构抽象成 js 的形式,配合不同的渲染工具,就可以实现跨平台。通过事务处理机制,将多次修改 Dom 的结果一次性渲染到真实页面,从而减少页面渲染次数,减少修改 Dom 重排重绘的次数,提升渲染效率。

三、v-for 没有 key 会发生什么问题

效率问题

Vue 会用一种算法来尽可能地高效更新 Dom,以反映数据的变化。如果没有提供 key,Vue 无法追踪每个元素的身份,只能使用一种最简单的更新策略 —— 替换整个 Dom 节点。这意味着每次数据发生变化时,所有的 Dom 元素都会重新渲染。即使实际上只有少数元素需要更新。这样会导致性能下降。

组件状态丢失

如果 v-for 用于渲染的是组件,并且没有提供 key,那么每次重新渲染时,Vue 会销毁旧的组件实例,并创建新的组件实例。这将导致组件的状态丢失。

四、Vue 的 $nextTick 的作用?底层原理如何实现?

作用

nextTick 的作用是保证在组件数据更新后,确保能访问或操作更新后的 Dom元素。nextTick 方法是 Vue 提供的一个实用工具,它能够将回调函数延迟到下一个 DOM 更新循环之后执行。也就是说,通过 nextTick 方法,我们可以确保在 DOM 更新完成后执行某些操作。

原理

nextTick 方法是在 Vue.js 中常见的一种异步更新 DOM 的机制。它的原理是利用 JavaScript 的事件循环机制以及浏览器的渲染流程来实现延迟执行 DOM 更新操作。是对 JavaScript 事件循环的应用。核心是利用 Promise、MutationObserver、settimeOut、setImmediate 来模拟微/宏任务的实现,利用这些异步队列来实现 Vue 中的异步任务队列。

实现简易版 nextTick

const myNextTick = () => {
  return new Promise(resolve => {
    if (typeof MutationObserver !== 'undefined') {
      const textNode = document.createTextNode('1')
      const observer = new MutationObserver(resolve)
      observer.observe(textNode, {
        characterData: true
      })
      textNode.data = 2
    } else {
      setTimeout(resolve, 0)
    }
  })
}

五、Vue 的双向绑定原理

数据劫持

Vue 通过 Object.defineProperty() 方法(Vue2.x)或 Proxy 对象(Vue3.x)来劫持数据的访问器(getter 和 setter)。这样,每当数据被读取或者修改时,Vue 都能够捕获到这些操作(观察者 Observer)。通过发布订阅者模式,通知给所有订阅了该数据的观察者(Watcher)执行相应的回调函数更新视图。

发布订阅者模式

在这种模式下,数据的变化会触发通知给所有订阅了该数据的观察者(Watcher)。观察者接收到通知后,会执行相应的更新函数来修改视图。

观察者(Watcher)

连接 Observer 和 Compile 的桥梁,在数据变动时接收通知并执行相应的回调函数来更新视图。

模板解析(Compile)

解析模板指令,将模板中的变量替换成数据,并绑定相应的更新函数。

六、v-model 是什么?有什么作用?

v-model 是 Vue 的 Vue 的一个指令,是个语法糖,在 Vue2.x 中是 v-bind:value 和 @input 组合的简写。在 Vue3.0 中是 :modalValue@update:modelValue 的简写,一般用于表单元素和数据之间建立双向绑定关系。

七、为什么 data 属性是一个函数而不是一个对象

JS 中的对象是引用类型等数据,当多个实例引用同一个对象时,只要有一个实例对这个对象进行操作,其他实例中的数据也会发生变化。要想每个组件都有自己的数据不互相干扰,需要将数据以返回值的形式定义,这样每次复用组件的时候,就会返回一个新的 data,也就是说每个组件都有自己的私有数据空间。

八、keep-alive

  1. 把不活动的组件实例保存在内存中,而不是直接将其销毁
  2. 是一个抽象组件,不会被渲染到真实 Dom 中
  3. 两个生命周期
  • activated 组件激活触发
  • deactived 组件失活触发

九、Vue2 初始化过程

  1. 创建一个新的 Vue 实例,并初始化数据和选项
  2. 解析模板,将模板编译为虚拟 Dom
  3. 将虚拟 Dom 和真实 Dom 进行对比
  4. 响应式更新,根据数据变化更新视图
  5. 注册全局组件和全局混入
  6. 执行钩子函数
  7. 执行插件和指令等注册方法

十、v-if 和 v-for 为什么不能同时使用

  • Vue3

v-if 的优先级比 v-for 高。这意味着 v-if 将无法访问到 v-for 作用域定义的变量别名。v-if 无法访问到 todo,会导致报错

解决方案:

在外先包装一层 <template> 再在其上使用 v-for 可以解决这个问题 (这也更加明显易读):


<!--
 这会抛出一个错误,因为属性 todo 此时
 没有在该实例上定义
-->
<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo.name }}
</li>

// 正确做法
<template v-for="todo in todos">
  <li v-if="!todo.isComplete">
    {{ todo.name }}
  </li>
</template>

  • Vue2

v-for 的优先级比 v-if 高,也就是说在 v-if 中可以访问到 v-for 作用域内定义的变量别名,因此不会跟 Vue3 一样报错,但并不推荐这么做,原因如下:

解决方法:

可以和 Vue3 一样使用 template 包裹(for 在外 if 在内),但不推荐,因为仍然有性能问题。推荐使用 computed 对 todos 数组过滤后,v-for 遍历过滤后的数组。

  1. 性能问题

v-for 和 v-if 放在同一个元素上会导致性能下降。Vue 必须为每一个 v-for 项目都检查 v-if 的条件,这会增加不必要的计算量。特别是当 todos 数据量很大时,这种性能下降会更加明显。

v-for 优先于 v-if:首先,v-for 会为 todos 数组中的每个项目创建 DOM 节点。这意味着,它会为数组中的每个 todo 项目都创建一个 item 元素。

v-if 随后应用:然后,Vue 会检查每个由 v-for 创建的 item 元素上的 v-if 条件。如果 v-if 的条件不满足(即 todo.isComplete 为 true),则该节点不会被渲染到 DOM 中。但是,这并不意味着节点被销毁,而是它们只是简单地不被添加到 DOM 树中。

节点的销毁与复用:如果 todos 数组发生变化(例如,项目被添加、删除或更改),Vue 会重新计算 v-for 和 v-if 的结果,并相应地更新 DOM。这意味着,即使 v-if 条件不满足,由 v-for 创建的节点可能仍然存在在 Vue 的虚拟 DOM 中,只是它们不会出现在实际的 DOM 树中。这些节点可以被 Vue 复用,如果它们在未来再次满足 v-if 的条件。

性能影响:即使某些节点因为 v-if 条件不满足而不会被渲染到 DOM 中,它们仍然会被 Vue 创建和追踪,这可能会对性能产生影响,尤其是当 todos 数组很大时。

最佳实践:为了获得最佳性能,你应该避免在同一元素上同时使用 v-for 和 v-if。如果可能,应该使用计算属性或方法来预先过滤数据,然后只对过滤后的结果进行 v-for 渲染。这样做可以减少不必要的节点创建和销毁,提高应用程序的性能。

十一、Vue 的路由模式

hash的路由地址上有 #,history模式没有; 在做回车刷新的时候,hash模式会加载对应页面,history模式不会。

hash模式支持低版本浏览器,history不支持,因为是history是h5新增Api;

hash不会重新加载页面,单页面应用必备;

history是有历史记录的,h5 也新增了 history.pushState 和 replaceState 方法用于对历史记录进行修改的功能。history模式重写后 URL路径 并不包含原有路径文件的访问地址,比如进入/admin的一个子路由/admin/xxx,刷新后地址只有/admin,所以刷新会404。在生产环境需要配合服务器的转发规则重写,将admin后的路径进行重写。用以支持history模式路由的加载。

Vue3.x

一、Vue3.x 为什么要用 Proxy Api 替代 Object.defineProperty Api?

在 Vue2.0 中,使用 Object.defineProperty 是为了实现响应系统,能够监听对象的属性变化并作出相应的响应。但是 Object.defineProperty 存在一些限制,例如它不能监听到数组或者对象属性的添加和删除,而且也不支持枚举属性。相比之下,Proxy 提供了更强大的功能。Proxy 几乎可以拦截所有操作,包括属性的读取、写入、枚举、删除等。这意味着,使用 Proxy 可以实现更精细的属性控制,例如只对某些属性进行响应式处理、对属性的读取和写入进行额外的处理。此外,Proxy 还提供更多的拦截器,例如 getsethasdelete 等,可以更加灵活地控制对象的操作。

Object.defineProperty(object, propertyname, descriptor)
  • object: 被修改属性的对象

  • propertyname: 需要被修改的属性

  • descriptor: 属性修改后的特性/值

descriptor:

{
    value:'',//值
    writable:'',//boolean  是否可写,true不可修改值,只读。默认值true
    enumerable:'',//是否可枚举。默认值true
    configurable:'',//是否可配置,true表示无法删除目标属性或者修改writable, configurable, enumerable属性。默认值true
    
    get:()=>{},//读取变量值
    set:()=>{},//设置变量值
    
}

var proxy = new Proxy(target, handler)

拦截器列举:

  • get(target, property, receiver):拦截对象属性的读取操作。

  • set(target, property, value, receiver):拦截对象属性的设置操作。

  • has(target, property):拦截in操作符。判断对象是否存在某属性。

  • deleteProperty(target, property):拦截delete操作符。

  • apply(target, thisArg, argumentsList):拦截函数的调用。

  • construct(target, argumentsList, newTarget):拦截new操作符。

二、Vue3 性能提升主要体现在哪些方面

响应系统重写

Vue3 使用了 Proxy 代理对象来实现响应系统,相比 Vue2 的 Object.defineProperty 可以更好的处理动态添加和删除属性的情况,同时也更加高效。

编译器优化

Vue3 的编译器可以更好的利用静态分析技术,减少运行时的代码量,提高渲染性能。编译器优化包括提升编译速度(pathFlag 等)、减少冗余代码生成、优化代码结构等。

组件实例缓存

Vue3 在组件实例的创建和销毁上做了优化,可以更好的利益哦那个缓存机制,减少不必要的实例创建和销毁,提高性能。通过使用组件实例缓存,可以避免重复创建和销毁实例,从而减少不必要的计算和渲染。

改进了 diff 算法

tree-shaking

三、Vue3 diff 算法优化在哪些方面

Vue2 diff 算法

  1. 首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
  2. 如果为相同节点,进行 patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
  3. 如果都有子节点,则进行 updateChildren,判断如何对这些新老节点的子节点进行操作(diff核心)。

在对比其子节点数组时,vue 对每个子节点数组使用了两个指针,分别指向头尾,然后不断向中间靠拢来进行对比,这样做的目的是尽量复用真实 dom,尽量少的销毁和创建真实 dom。如果发现相同,则进入和根节点一样的对比流程,如果发现不同,则移动真实 dom 到合适的位置。 这样一直递归的遍历下去,直到整棵树完成对比。

  1. 匹配时,找到相同的子节点,递归比较子节点。尽可能的复用重复出现的节点,把旧的当中没有在新的里出现的节点移除,把出现在新的节点中而旧的节点中没有的新增。
  2. key 是为 Vue 中 vnode 的唯一标记,通过这个 key,diff 操作可以更准确、更快速
  • 将元素调换顺序,实际的 diff算法 是怎样的

Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。 调换顺序时,如果使用 index 作为 key,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2...这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。

静态提升

Vue3 在编译阶段会对模板进行静态分析,将静态的节点提升为常量,避免在运行时进行不必要的比较和更新。这大大减少了Diff过程中的计算量。

静态标记

Vue3 引入了静态标记的概念,用于区分静态节点和动态节点。在 Diff 过程中,Vue 3可以跳过静态节点的比较和更新,进一步减少不必要的操作。

PatchFlag

Vue3 引入了 Patch Flag 的概念,用于标记组件在更新过程中的一些特殊情况,如 props 的变化或需要强制更新等。这可以在 diff 算法中更快速地定位需要更新的组件,减少比较的工作量。

动态属性的快速路径

对于动态属性,Vue3 使用更快速的路径进行处理,减少了比较的开销,提高了 Diff 算法的性能。

Fragments 优化

Vue3 支持碎片化,即允许组件有多个根节点,这在 Vue2 中是只允许一个根节点。在 Vue2 中,Fragments(片段) 会引入额外的虚拟 DOM 层级,导致 Diff 算法需要进行更多的比较操作。而在 Vue3 中,对 Fragments 进行了优化,可以直接将其内部的内容合并到父级中,减少了虚拟 DOM 层级,提高了 Diff 算法的效率。

双端比较优化

Vue3 继续使用了双端比较算法,但是采用的是Map 数据结构在细节上进行了优化,比如对于相同节点的处理更加高效。

区别

举例
// 假设旧节点
const oldVnode = h('div', null, [
  h('ul', null, [
    h('li', { key: 'a' }, 'Item A'),
    h('li', { key: 'b' }, 'Item B'),
    h('li', { key: 'c' }, 'Item C'),
  ]),
  h('MyComponent', { key: 'comp' }, [
    h('p', null, 'Hello'),
    h('span', null, 'World')
  ])
]);

// 假设新节点
const newVnode = h('div', null, [
  h('ul', null, [
    h('li', { key: 'a' }, 'Item A'),
    h('li', { key: 'c' }, 'Updated Item C'),
    h('li', { key: 'd' }, 'Item D'),
  ]),
  h('MyComponent', { key: 'comp' }, [
    h('p', null, 'Hello'),
    h('span', null, 'Vue')
  ])
]);

// 执行双端比较更新
patch(oldVnode, newVnode);

在这个例子中,Vue2 的 diff 算法会进行以下步骤:

  1. 比较根节点 ul:发现类型相同,继续比较其属性和子节点。
  2. 列表更新:在 ul 列表中,Vue2 会从两端开始比较旧节点和新节点的差异。例如,旧节点的 Item B 没有对应的新节点,需要删除;新节点的 Item D 是新增的,需要插入。
  3. 组件更新:对于自定义组件 MyComponent,Vue2 同样从两端开始比较。这里的 span 内容由 World 更新为 Vue,需要进行更新操作。
  4. 性能影响:虽然 Vue2 使用了双端比较策略优化了部分比较过程,但对于复杂的结构和频繁的数据更新,仍可能引起一些不必要的性能损耗,特别是在列表较长或嵌套层级深的情况下。。

四、Vue3 初始化的大概流程

  1. 创建实例,使用 createApp() 函数创建 Vue3 实例
  2. 组件注册
  3. 使用 app.mount 将根组件挂载到页面上
  4. 模板编译:Vue3 使用基于编译的虚拟 Dom,模板会在编译时转换为渲染函数
  5. 响应式数据初始化
  6. 组件实例化
  7. 虚拟 Dom 到创建和挂载
  8. 钩子执行
  9. 渲染

Vuex

一、Vuex 的原理

Vuex 是一种状态管理机制,将全局组件的共享状态抽取出来为一个 store,以一个单例模式存在,任何一个组件中都可以使用。

Vuex 是集中于 MVC 模式中的 Model 层,规定所有的数据操作都必须通过 action - mutation - state 的流程来进行,再结合 Vue 的数据视图 v-model 的双向绑定特性来实现页面的展示更新。

Vue 组件接收交互行为,调用 dispatch 方法触发 action 相关处理,若页面状态需要改变,则调用 commit 方法提交 mutation 修改 state, 通过 getters 获取到 state 新值,响应数据或状态 Vue 组件,页面随之更新。

二、module 的 namespaced 的作用

用于组织和隔离模块的状态、突变(mutations)、动作(actions)和 getter。使用 namespace 可以避免模块之间的命名冲突,并使得状态管理更加清晰和可维护。

当使用了命名空间后,访问状态、突变和动作时,需要指定模块的名称。例如:

methods: { 
    increment() { 
    this.$store.dispatch('exampleModule/increment'); // 通过命名空间访问动作 
   } 
 }

三、实现原理

实现 state


//定义一个Vue,让全局都可以使用这个Vue
let Vue;

class Store{
    //当new的时候,给Vuex.js中传入了一堆的东西,在这里接收需要用constructor
    constructor(options){
        // console.log(options);   //打印出{state: {…}, getters: {…}, mutations: {…}, actions: {…}},就可以拿到里面的数据了
        
/*-------------------------------state原理-------------------------------------------------------------*/
        //给每个组件的$store上挂一个state,让每个组件都可以用  this.$store.state
        this.state = options.state

        //在state上面传入一个name:'Fan'打印一下
        // console.log(this.state);    //打印结果  {name: "Fan"}
/*-------------------------------------------------------------------------------------------------*/

    }
}

//install本质上就是一个函数
const install = (_Vue)=>{
    // console.log('......');  //测试能不能调到这个方法,经测试可以调到
    //把构造器赋给全局Vue
    Vue = _Vue;

    //混入
    Vue.mixin({
        beforeCreate() {    //表示在组件创建之前自动调用,每个组件都有这个钩子
            // console.log(this.$options.name) //this表示每个组件,测试,可以打印出mian.js和App.vue中的name main和app
            
            //保证每一个组件都能得到仓库
            //判断如果是main.js的话,就把$store挂到上面
            if(this.$options && this.$options.store){
                this.$store = this.$options.store
            }else{
                //如果不是根组件的话,也把$store挂到上面,因为是树状组件,所以用这种方式
                this.$store = this.$parent && this.$parent.$store

                //在App.vue上的mounted({console.log(this.$store)})钩子中测试,可以得到store ---> Store {}
            }
        },
    })
}

//导出
export default {
    install,
    Store
}




实现 getters


        //得到仓库中的getters,如果人家不写getters的话,就默认为空
        let getters = options.getters || {}
        // console.log(getters);   //打印出一个对象,对象中是一个方法  {myName: ƒ}

        //给仓库上面挂载一个getters,这个getters和上面的那一个getters不一样,一个是得到,一个是挂载
        this.getters = {}

        //不好理解,因为人家会给你传多个方法,所以使用这个api处理得到的getters,得到一个数组
        //把store.js中的getters中再写一个方法myAge,用来测试
        // console.log(Object.keys(getters));  //打印出  ["myName", "myAge"]

        //遍历这个数组,得到每一个方法名
        Object.keys(getters).forEach((getter)=>{
            // console.log(getter);    //打印出  myName   myAge
            Object.defineProperty(this.getters,getter,{
                //当你要获取getter的时候,会自动调用get这个方法
                //一定要用箭头函数,要不然this指向会出现问题
                get:()=>{
                    // console.log(this);
                    return getters[getter](this.state)
                }
            })
        })

实现 mutations


/* ---------------------------------------mutatios原理----------------------------------------------------------- */
        //和getters思路差不多

        //得到mutations
        let mutations = options.mutations || {}
        // console.log(mutations);     //{add: ƒ}

        //挂载mutations
        this.mutations = {}

        //拿到对象中的一堆方法
        Object.keys(mutations).forEach((mutation)=>{
            // console.log(mutation);  //add sub
            this.mutations[mutation] = (payload)=>{
                mutations[mutation](this.state,payload)
            }
        })

        //打印看一下,正确
        // console.log(mutations);     //{add: ƒ, sub: ƒ}
        
        //但是他比较恶心,需要实现commit,在下面实现
/* -------------------------------------------------------------------------------------------------- */

    } 

    //给store上挂一个commit,接收两个参数,一个是类型,一个是数据
    commit(type,payload){
        //{add: ƒ, sub: ƒ}
        //把方法名和参数传给mutations
        this.mutations[type](payload)
    }

实现 actions


/* ---------------------------------------------actions原理----------------------------------------------------- */
        //和上面两种大同小异,不多注释了
        let actions = options.actions || {}
        this.actions = {};
        forEach(actions, (action, value) => {
            this.actions[action] = (payload) => {
                value(this, payload)
            }
        })
/* -------------------------------------------------------------------------------------------------- */

    }
    // type是actions的类型  
    dispatch = (type, payload) => {
        this.actions[type](payload)
    }

四、刷新页面数据丢失处理方案

Vite

一、为什么 Vite 比 Webpack 快

1、开发模式的差异

在开发环境中,Webpack 是先打包再启动开发服务器,而 Vite 则是直接启动,然后再按需编译依赖文件。(大家可以启动项目后检查源码 Sources 那里看到)

这意味着,当使用 Webpack 时,所有的模块都需要在开发前进行打包,这会增加启动时间和构建时间。

Vite 则采用了不同的策略,它会在请求模块时再进行实时编译,这种按需动态编译的模式极大地缩短了编译时间,特别是在大型项目中,文件数量众多,Vite 的优势更为明显。

Webpack启动

Vite启动

2、对ES Modules的支持

现代浏览器本身就支持 ES Modules,会主动发起请求去获取所需文件。Vite充分利用了这一点,将开发环境下的模块文件直接作为浏览器要执行的文件,而不是像 Webpack 那样先打包,再交给浏览器执行。这种方式减少了中间环节,提高了效率。

什么是ES Modules?

通过使用 exportimport 语句,ES Modules 允许在浏览器端导入和导出模块。

当使用 ES Modules 进行开发时,开发者实际上是在构建一个依赖关系图,不同依赖项之间通过导入语句进行关联。

主流浏览器(除IE外)均支持ES Modules,并且可以通过在 script 标签中设置 type="module"来加载模块。默认情况下,模块会延迟加载,执行时机在文档解析之后,触发DOMContentLoaded事件前。

3、底层语言的差异

Webpack 是基于 Node.js 构建的,而 Vite 则是基于 esbuild 进行预构建依赖。esbuild 是采用 Go 语言编写的,Go 语言是纳秒级别的,而 Node.js 是毫秒级别的。因此,Vite 在打包速度上相比Webpack 有 10-100 倍的提升。

什么是预构建依赖?

预构建依赖通常指的是在项目启动或构建之前,对项目中所需的依赖项进行预先的处理或构建。这样做的好处在于,当项目实际运行时,可以直接使用这些已经预构建好的依赖,而无需再进行实时的编译或构建,从而提高了应用程序的运行速度和效率。

4、热更新的处理

在 Webpack 中,当一个模块或其依赖的模块内容改变时,需要重新编译这些模块。

而在 Vite 中,当某个模块内容改变时,只需要让浏览器重新请求该模块即可,这大大减少了热更新的时间。

总结

总的来说,Vite 之所以比 Webpack 快,主要是因为它采用了不同的开发模式充分利用了现代浏览器的 ES Modules 支持使用了更高效的底层语言并优化了热更新的处理。这些特点使得 Vite在大型项目中具有显著的优势,能够快速启动和构建,提高开发效率。

Webpack

一、常用的一些 loader

vue-loader

用于加载和解析Vue单文件组件。它允许你使用 Vue 的单文件组件语法,并将其转换为JavaScript模块。

使用vue-loader可以实现以下几个用途:

  1. 解析Vue单文件组件:vue-loader能够解析包含模板、脚本和样式的.vue文件,并将其转换为JavaScript模块,以便在应用程序中使用。
  2. 预处理器支持:vue-loader支持使用各种预处理器来编写Vue组件,例如:ES2015、TypeScript、SCSS、Less等。你可以通过 webpack 的配置来配置相应的预处理器。
  3. 模块化组件开发:使用vue-loader可以将组件的模板、脚本和样式封装在一个文件中,提供了更好的组织和可维护性。它还支持组件间的嵌套和组合,使得组件的开发更为高效和灵活。
  4. 动态加载组件:vue-loader支持动态导入组件,可以根据需要异步加载组件,提高应用程序的性能和加载速度。
loader
css-loader + style-loader

css-loader 主要是帮我们解析 css 文件内的 css 代码,将 CSS 转化成 CommonJS 模块,而style-loader则帮我们将 css-loader 解析后的内容挂载到 html 页面中。

file-loader

解析项目中的文件

sass-loader、less-loader

将 scss/less 编译成 css 代码

babel-loader

Babel是一个工具链,主要用于在旧的浏览器或环境中将 ECMAScript 2015+ 代码转换为向后兼容版本的 JavaScript 代码。简单来说就是把高阶语法转换为浏览器支持的低阶语法。

awesome-typescript-loader

将 ts 转化为 js

eslint-loader

打包时通过 ESLint 检查 JavaScript 代码

plugin
html-webpack-plugin

生成html文件,自动引入所依赖的打包好的的 js 代码

clean-webpack-plugin

自动清理 dist

uglyfyjs-webpack-plugin

压缩文件

DDLPlugin

加快打包速度,只编译核心代码,第三方库不编译直接使用

mini-css-extract-plugin

打包出css文件,可配合uglifyjs-webpack-plugin 进行压缩优化

ignonre-plugin

指定不打包目录

浏览器

一、跨域和解决方案

原因

跨域的主要原因是受同源策略的限制。

同源策略是指 协议 域名 端口 三者均相同,即便两个不同的域名指向同一个ip地址,也非同源。 同源策略限制的内容有:

CookieslocalStorage、indexDB等存储性内容
DOM节点
Ajax 请求发送后被拦截了

不受同源策略影响的标签:img、script、link 、iframe

解决方案

JSONP(只能解决get请求不能解决post请求)

jsonp 的原理就是利用 <script> 标签没有跨域限制,通过 <script> 标签src属性,发送带有 callback 参数的 GET 请求,服务端将接口返回数据拼凑到 callback 函数中,返回给浏览器,浏览器解析执行,从而前端拿到 callback 函数返回的数据。

原生 js 实现
 <script>
    var script = document.createElement('script');
    script.type = 'text/javascript';

    // 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
    script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
    document.head.appendChild(script);

    // 回调执行函数
    function handleCallback(res) {
        alert(JSON.stringify(res));
    }
 </script>

服务端返回如下

handleCallback({"success": true, "user": "admin"})
jquery Ajax 实现
$.ajax({
    url: 'http://www.domain2.com:8080/login',
    type: 'get',
    dataType: 'jsonp',  // 请求方式为jsonp
    jsonpCallback: "handleCallback",  // 自定义回调函数名
    data: {}
});
Vue axios 实现
this.$http = axios;
this.$http.jsonp('http://www.domain2.com:8080/login', {
    params: {},
    jsonp: 'handleCallback'
}).then((res) => {
    console.log(res); 
})
服务端 nodejs 处理
var querystring = require('querystring');
var http = require('http');
var server = http.createServer();

server.on('request', function(req, res) {
    var params = querystring.parse(req.url.split('?')[1]);
    var fn = params.callback;

    // jsonp返回设置
    res.writeHead(200, { 'Content-Type': 'text/javascript' });
    res.write(fn + '(' + JSON.stringify(params) + ')');

    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');
Cors
概述

CORS是基于http1.1的一种跨域解决方案,它的全称是Cross-Origin Resource Sharing,跨域资源共享。

Cors 需要浏览器和后端同时支持.实现 Cors 通信后端是关键。只要后端实现了 Cors 就实现了跨域。服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。它的总体思路是:如果浏览器要跨域访问服务器的资源,需要获得服务器的允许

image.png

而要知道,一个请求可以附带很多信息,从而会对服务器造成不同程度的影响

比如有的请求只是获取一些新闻,有的请求会改动服务器的数据

针对不同的请求,CORS 规定了三种不同的交互模式,分别是:

  • 简单请求
  • 需要预检的请求
  • 附带身份凭证的请求

这三种模式从上到下层层递进,请求可以做的事越来越多,要求也越来越严格。

当浏览器端运行了一段 ajax 代码(无论是使用 XMLHttpRequest 还是 fetch api),浏览器会首先判断它属于哪一种请求模式。下面分别说明三种请求模式的具体规范。

简单请求
简单请求的判定

当请求同时满足以下条件时,浏览器会认为它是一个简单请求:

  1. 请求方法属于下面的一种:

    • get
    • post
    • head
  2. 请求头仅包含安全的字段,常见的安全字段如下:

    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  3. 请求头如果包含Content-Type,仅限下面的值之一:

    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded

如果以上三个条件同时满足,浏览器判定为简单请求。

举例:

// 简单请求
fetch('http://crossdomain.com/api/news');

// 请求方法不满足要求,不是简单请求
fetch('http://crossdomain.com/api/news', {
  method: 'PUT',
});

// 加入了额外的请求头,不是简单请求
fetch('http://crossdomain.com/api/news', {
  headers: {
    a: 1,
  },
});
// 简单请求
fetch('http://crossdomain.com/api/news', {
  method: 'post',
});

// content-type不满足要求,不是简单请求
fetch('http://crossdomain.com/api/news', {
  method: 'post',
  headers: {
    'content-type': 'application/json',
  },
});
const express = require('express')
const cors = require("cors")
//2、调用express()得到一个app
//    类似于 http.createServer()
const app = express()
app.get('/login', (req, res) => {
    console.log('req =>', req)
    res.header("Access-Control-Allow-Origin", "*")
 })
简单请求的交互规范

当浏览器判定某个 ajax 跨域请求是简单请求时,会发生以下事情:

  1. 请求头中会自动添加 Origin 字段

比如,在页面http://my.com/index.html中有以下代码造成了跨域

// 简单请求
fetch('http://crossdomain.com/api/news');

请求发出后,请求头会是下面的格式:

GET /api/news/ HTTP/1.1
Host: crossdomain.com
Connection: keep-alive
...
Referer: http://my.com/index.html
Origin: http://my.com // 关键

最后一行,Origin 字段会告诉服务器,是哪个源地址在跨域请求

  1. 服务器响应头中应包含 Access-Control-Allow-Origin

当服务器收到请求后,如果允许请求跨域访问,需要在响应头中添加Access-Control-Allow-Origin字段

该字段的值可以是:

  • *: 表示我很开放,什么人我都允许访问
  • 具体的源:比如 my.com 。表示我就允许你访问

注:

实际上,这两个值对于客户端http://my.com而言,都一样,因为客户端才不会管其他源服务器允不允许,就关心自己是否被允许

当然,服务器也可以维护一个可被允许的源列表,如果请求的Origin命中该列表,才响应*或具体的源

为了避免后续的麻烦,强烈推荐响应具体的源

当浏览器看到服务器允许自己访问后,把响应顺利的交给 js,以完成后续的操作

下图简述了整个交互过程

image.png

需要预检的请求

简单的请求对服务器的威胁不大,所以允许使用上述的简单交互即可完成。

但是,如果浏览器不认为这是一种简单请求,就会按照下面的流程进行:

  1. 浏览器发送预检请求,询问服务器是否允许
  2. 服务器允许
  3. 浏览器发送真实请求
  4. 服务器完成真实的响应

比如,在页面http://my.com/index.html中有以下代码造成了跨域

// 需要预检的请求
fetch('http://crossdomain.com/api/user', {
  method: 'POST', // post 请求
  headers: {
    // 设置请求头
    a: 1, // 由于是额外请求头所以不是简单请求
    b: 2,
    'content-type': 'application/json',
  },
  body: JSON.stringify({ name: '袁小进', age: 18 }), // 设置请求体
});

浏览器发现它不是一个简单请求,则会按照下面的流程与服务器交互:

  1. 浏览器发送预检请求,询问服务器是否允许
OPTIONS /api/user HTTP/1.1
Host: crossdomain.com
...
Origin: http://my.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: a, b, content-type

以看出,这并非我们想要发出的真实请求,请求中不包含我们的请求头,也没有消息体。

这是一个预检请求,它的目的是询问服务器,是否允许后续的真实请求。

预检请求没有请求体,它包含了后续真实请求要做的事情

预检请求有以下特征:

  • 请求方法为OPTIONS

  • 没有请求体

  • 请求头中包含

    • Origin:请求的源,和简单请求的含义一致
    • Access-Control-Request-Method:后续的真实请求将使用的请求方法
    • Access-Control-Request-Headers:后续的真实请求会改动的请求头
  1. 服务器允许

服务器收到预检请求后,可以检查预检请求中包含的信息,如果允许这样的请求,需要响应下面的消息格式

HTTP/1.1 200 OK
Date: Tue, 21 Apr 2020 08:03:35 GMT
...
Access-Control-Allow-Origin: http://my.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: a, b, content-type
Access-Control-Max-Age: 86400
...

对于预检请求,不需要响应任何的消息体,只需要在响应头中添加:

  • Access-Control-Allow-Origin:和简单请求一样,表示允许的源
  • Access-Control-Allow-Methods:表示允许的后续真实的请求方法
  • Access-Control-Allow-Headers:表示允许改动的请求头
  • Access-Control-Max-Age:告诉浏览器,多少秒内,对于同样的请求源、方法、头,都不需要再发送预检请求了
  1. 浏览器发送真实请求
POST /api/user HTTP/1.1
Host: crossdomain.com
Connection: keep-alive
...
Referer: http://my.com/index.html
Origin: http://my.com

{"name": "xiaoming", "age": 18 }
  1. 服务器响应真实请求
HTTP/1.1 200 OK
Date: Tue, 21 Apr 2020 08:03:35 GMT
...
Access-Control-Allow-Origin: http://my.com
...
添加用户成功

可以看出,当完成预检之后,后续的处理与简单请求相同

整个交互过程

image.png

附带身份凭证的请求

默认情况下,ajax 的跨域请求并不会附带 cookie,这样一来,某些需要权限的操作就无法进行

不过可以通过简单的配置就可以实现附带 cookie


// xhr
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

// fetch api
fetch(url, {
  credentials: 'include',
});


这样一来,该跨域的 ajax 请求就是一个附带身份凭证的请求

当一个请求需要附带 cookie 时,无论它是简单请求,还是预检请求,都会在请求头中添加cookie字段

而服务器响应时,需要明确告知客户端:服务器允许这样的凭据

告知的方式也非常的简单,只需要在响应头中添加:Access-Control-Allow-Credentials: true即可

对于一个附带身份凭证的请求,若服务器没有明确告知,浏览器仍然视为跨域被拒绝。

另外要特别注意的是:对于附带身份凭证的请求,服务器不得设置 Access-Control-Allow-Origin 的值为* 。这就是为什么不推荐使用*的原因

Websocket

Websocket 是 HTML5 的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket和HTTP都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据

// socket.html
let socket = new WebSocket('ws://localhost:3000'); 
socket.onopen = function () { socket.send('我爱你');//向服务器发送数据 } socket.onmessage = function (e) { console.log(e.data);//接收服务器返回的数据 } 
// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws');//记得安装ws
let wss = new WebSocket.Server({port:3000});
wss.on('connection',function(ws) {
  ws.on('message', function (data) {
    console.log(data);
    ws.send('我不爱你')
  });
})
Proxy代理

使用的场景是:生产环境不产生跨域,但开发环境发生跨域

最常用的方式。通俗点说就是客户端浏览器发起一个请求会存在跨域问题,但是服务端向另一个服务端发起请求并无跨域,因为跨域问题归根结底源于同源策略,而同源策略只存在于浏览器。那么就可以通过Nginx配置一个代理服务器,反向代理访问跨域的接口,并且还可以修改 Cookie 中 domain 信息,方便当前域 Cookie 写入

跨域请求如何携带 Cookie

在前端请求的时候设置 request对象 的属性 withCredentials 为 true

CSS

一、重排和重绘

重排

浏览器重新计算元素的几何属性(例如大小和位置)

  • 更改了元素的尺寸
  • 更改了显示状态 display
  • margin padding border
  • 更改了元素内容
  • 布局、定位属性

重绘

重新绘制元素的外观(例如颜色和背景)

  • 背景色
  • 文字颜色
  • 透明度

重排一定会触发重绘,重绘不会导致重排

盒子模型

标准盒模型

标准盒模型也是谷歌的模型,如果我们没有去定义是什么类型的话,默认就是标准盒模型。(box-sizing: content-box)

在标准盒模型中,元素的width属性只包括内容的宽度,不包括内边距和边框,总宽度计算方式如下: 总宽度 = content(width) + padding + border + margin

这里我们讲解一种比较易错的点。在标准盒模型中,当容器的宽度固定时,设置内边距会增加元素的实际宽度,因为内边距和边框会被加在内容宽度上。当我们定死了元素的 width 后,再加 paddingmargin 整个元素占的面积会被撑大,就像建房子时,墙壁往外建的效果一样。

ie 盒模型

要将布局调整为 IE盒模型的话,那就要 (box-sizing: border-box) ,如果不切换成 IE盒模型,那默认就是标准盒模型。在IE盒模型中,元素的width属性包含了内容、内边距和边框的总和。总宽度计算方式如下: 总宽度 = width (content + padding + border) + margin

性能优化

一、# 大数据量场景

常见场景

场景说明
长列表以列表或网格形式规律展示并且列表项较多的场景,一般通过 v-for 渲染展示
表格表格项较多的场景,一般基于 Table 组件实现,同时还有可能结合表单类组件进行编辑
选择器选择器或级联选择器存在较多选项的场景,一般基于 Select 等组件实现
树形控件树形控件存在较多节点的场景,一般基于 Tree 组件实现
图表图表数据量较大的场景,一般基于 Echarts 或 AntV 等库实现

性能瓶颈

在大数据量内容需要展示的场景下,我们遇到页面卡顿响应慢等问题的时候,首先要做的就是找到产生问题的原因,只有知道原因才能对症下药。在大数据量场景下我们常见的性能原因主要如下:

  • JS 线程长时间占用导致渲染不及时 例如进行复杂计算而耗费大量时间等
  • 内存占用过高,例如 DOM 节点数量巨大或者在大量 DOM 上绑定了事件等
  • 页面存在大量回流或重绘,例如元素需要实时计算 scrollTop 等维度属性来执行动画等

解决方案

减少 DOM 数量

💡 推荐:分页或搜索的方式 > 虚拟列表 > 时间分片或触底加载 (具体情况具体分析)

分页或搜索展示
分页展示

在表格场景下,如果后端能够支持分页返回数据的话,优先通过后端分页返回数据前端分页展示数据的方式进行处理。

搜索展示

在选择器场景下,如果后端能够支持关键词搜索内容的话,前端可以仅展示部分选项,其余选项可以通过关键词搜索请求后端获取展示,避免了大量的 option 一下子渲染导致页面卡死

前端模拟

当后端接口不支持分页或搜索而是全量返回所有数据的时候,前端可以全量缓存并模拟分页和搜索,这种方式也一定程度上避免了大量 DOM 的绘制。但是,在这场景下我们需评估直接缓存全量数据进行模拟分页和搜索是否存在 JS 事件执行大量耗时的情况,例如通过响应式缓存数据在编辑时可能也会出现卡顿,此时我们往往需要结合其他方法进行组合解决。

分片加载

当有大数据量内容需要展示的时候,如果我们直接渲染全量数据,可能会出现较长时间白屏或较长时间用户才能操作。例如我们当前有 1000000 条数据需要展示,当我们直接全量渲染的时候,进入页面能够明显感到卡顿通过 performance 分析 FCP 在5.5s 以上,也就意味着用户需要等待这么久的时间才能初步看到内容,这无疑是极差的体验。此时,根据分片加载的思想,原本需要渲染的大量内容,可以先渲染一小部分,让用户感知到内容,然后再将剩余部分逐步渲染展示。


<template>
  <div class="list-wrapper">
    <template v-for="item in list" :key="item.index">
      <div class="list-item">{{ item.index }}-{{ item.value.message }}</div>
    </template>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { getData } from '../data';

const list = ref<any[]>([]);
const count = ref(0);

const total = 100000;
const once = 50;
const loop = () => {
  if (count.value > total) {
    return;
  }
  list.value = list.value.concat(getData(once));
  count.value += once;
  requestAnimationFrame(() => {
    loop();
  });
};

onMounted(() => {
  loop();
});
</script>

<style lang="less" scoped>
.list-wrapper {
  width: 800px;
  margin: 0px auto;
  .list-item {
    margin: 10px 0px;
  }
}
</style>


这里使用了 requestAnimationFrame 代替 setTimeOut。采用 setTimeout 定时器的方式后,通过 performance 分析 FCP 下降到了 1.5s 左右。但是,当我们采用定时器方式的时候,快速滚动页面会发现页面出现闪屏或白屏的现象,这是由于 setTimeout 的执行步调和屏幕的刷新步调不一致,画面出现明显的丢帧现象,此时我们可以借助 requestAnimationFrame 去改善这个问题。

触底加载

在一些特殊的大数据量内容展示场景中,例如微博信息流、朋友圈内容等,特点是不能分页且只要用户愿意就能不断随意上下滚动直到达到上下边界。如果一下子加载所有内容,数量过大会导致页面卡死或白屏较长时间。此时可以先初始加载少部分内容,通过监听滚动事件不断加载新内容来解决,可以参考 ElementUi InfiniteScroll 组件。但是,与分片加载一样,触底加载会不断地加载,依旧会使页面内容加载到一定量级,仍旧存在大量 DOM 元素而占用大量内存导致页面卡顿的问题,从而带来糟糕的用户体验。此时可以使用虚拟列表来解决。

虚拟列表
基础原理

image.png

虚拟列表简单来说就是按需渲染,只对用户可见区域进行渲染,对用户不可见区域中的数据不渲染或者部分渲染,从而模拟出一种完整渲染的效果。如上图所示,虚拟列表将原本完整渲染的列表分为三个区域:可视区域 + 预渲染区 + 未渲染区,其中可视区域为我们屏幕看到的实际内容,可视区域+预渲染区为实际渲染的内容。当用户滚动页面时,根据滚动的位置,计算出实际渲染区域第一个元素的索引 startIndex 和 最后一个元素的索引 endIndex,根据两个索引渲染相应的内容,同时为了保证实际渲染列表元素一直存在可视区中,设置相应平移的数值。通过虚拟列表技术,列表实际渲染的 DOM 节点数量都会稳定在一个范围内,不会随着列表数据的增多而不断增加。

减少 Vue 响应式数据

对于使用 Vue 库进行开发的应用,在开发过程中,我们通常会将数据通过 ref 或 reactive 直接设置为响应式的对象,所谓响应式就是当我们修改数据后,能够自动触发组件的重新渲染。为了满足响应式,vue 底层会进行一系列的处理(详见《深入响应式系统》),同时 Vue 的响应性系统默认是深度的,每个属性访问都将触发代理的依赖追踪,这会产生非常大的开销,尤其当我们得到数据源不会发生变化或者只会在部分内容上发生变化时,这部分的开销无疑是多余的。针对这种情况,我们需要减少 Vue 响应式数据:

  • 数据源全量均不会发生变化:直接赋值
  • 数据源单个数据不会发生变化只会发生全量变化:shallowRefshalloReactive(详见《减少大型不可变数据的响应性开销》
  • 数据源仅其中部分数据发生变化:仅将会变化部分的数据设为响应式,例如表格场景中仅将当前页的数据设置为响应式,其他全量数据直接赋值缓存。

shallowRef:

和 ref() 不同,浅层 ref 的内部值将会原样存储和暴露,并且不会被深层递归地转为响应式。只有对 .value 的访问是响应式的。

shallowRef() 常常用于对大型数据结构的性能优化或是与外部的状态管理系统集成

性能优化

前后端交互层面

减少网络请求

代码方面

  • 避免内存泄露、回调地狱
  • 封装组件和方法提高复用性、可维护性
  • 判断条件过多,可使用枚举查找表
  • 虚拟滚动加载长列表
  • 防抖节流

首屏优化/用户体验

  • 使用 CDN 加速资源加载静态资源
  • 减少 HTTP 请求数
  • 使用浏览器缓存缓存页面中的资源
  • 按需导入组件,懒加载没有执行的代码,预加载需要的资源(以时间维度,均摊请求量)

1.预加载

原理:提前下载将来需要的图片资源,让浏览器持有图片资源的缓存,将来需要使用对应图片资源时直接从缓存中获取即可

使用 window.onloadrequestIdleCallback ,将图片的预加载操作推迟到页面及所有依赖资源加载完成或浏览器空闲时间时进行。这样可以确保页面的关键内容(首屏内容)优先渲染,避免阻塞用户的首屏体验。


function autoPreloadImages() {  
  // 使用 require.context 动态获取指定文件夹及其子文件夹下的所有图片  
  // require.context(目录, 是否递归, 匹配文件的正则表达式)  
  const files = require.context('../../assets/img'true/^./.*pre-.*.(png|jpe?g|gif|webp)$/i);  
  
  // 调用 files.keys() 获取匹配的文件路径数组,并通过 files(key) 获取每个文件的实际 URL  
  const urls = files.keys().map(key => files(key));  
  
  // 检查浏览器是否支持 requestIdleCallback 方法  
  if ('requestIdleCallback' in window) {  
    requestIdleCallback(() => {  
      preloadImages(urls);  
    });  
  } else {  
    // 如果浏览器不支持 requestIdleCallback,则使用 window.onload 事件作为后备方案  
    window.addEventListener('load'event => {  
      preloadImages(urls);  
    });  
  }  
  
  // 定义图片预加载函数  
  function preloadImages(urls) {  
    urls.forEach(url => {  
      const img = new Image();  
      img.src = url;  
    });  
  }  
}

2.懒加载

原理:懒加载其实也叫做延迟加载、按需加载,在比较长的网页或应用中,如果图片有很多,一下子之间把所有的图片都加载出来的话,耗费很多性能,而且用户不一定会把图片全部看完。只有当图片出现在浏览器的可视区域内时,让图片显示出来,这就是图片懒加载。


 <div class="img-box">
      <div class="img-container">
        <img
          src="https://gitee.com/z1725163126/cloundImg/raw/master/loading.gif"
          alt=""
          data-src="../../static/1.jpg"
          class="lazyload"
        />
      </div>
      <div class="img-container">
        <img
          src="https://gitee.com/z1725163126/cloundImg/raw/master/loading.gif"
          alt=""
          data-src="../../static/2.jpg"
          class="lazyload"
        />
      </div>
      <div class="img-container">
        <img
          src="https://gitee.com/z1725163126/cloundImg/raw/master/loading.gif"
          alt=""
          data-src="../../static/3.jpg"
          class="lazyload"
        />
      </div>
    </div>
  <script>
      const getImage = [...document.querySelectorAll('.lazyload')]
      function lazyLoad() {
        getImage.forEach(item => {
          // 判断是否在可视区域内
          if (isInVisible(item)) {
          // 如果在可视区域内,就设置src的路径
            item.src = item.dataset.src
          }
        })
      }

      function isInVisible(image) {
        const rect = image.getBoundingClientRect()
        return (
          rect.bottom > 0 &&
          rect.top < window.innerHeight &&
          rect.right > 0 &&
          rect.left < window.innerWidth
        )
      }
      lazyLoad()
     // window.addEventListener('scroll', lazyLoad)
      
      /*
        优化:
        节流 :事件处理函数执行一次后,在某个时间期限内不再工作
        fun:传入一个函数
        miliseconds:间隔时间
      */

      function throttle(fun, time = 250) {
        let lastTime = 0 //最后一次执行的时间
        return function (...args) {
          const now = new Date().getTime()
          if (now - lastTime >= time) {
            fun()
            lastTime = now
          }
        }
      }
      window.addEventListener('scroll', throttle(lazyLoad, 1000), false)
    </script>


  • 减少重绘重排
  • 使用 一些loader 比如 url-loader 转为base64 图片
  • 使用一些压缩 loader 比如 image-webpack-loader 将图片压缩,在 num run build 时将图片压缩。
  chainWebpack: config => {
    config.module
      .rule('images')
      .test(/.(png|jpe?g|gif|svg)(?.*)?$/)
      .use('image-webpack-loader')
        .loader('image-webpack-loader')
        .options({
          bypassOnDebug: true })
        .end()
  }

打包优化

  • 开启 gzip 配置,减少文件体积
  • elementUiui 库按需加载
  • 配置 externals

大屏

大屏的适配方案

原始解决方案

起初比较流行的三大解决方式:

rem + fontSize 方案

动态设置 HTML 根节点字体大小,配合 百分比或者vw/vh, 实现容器 宽高字体大小的动态调整

合理设置 rem 大小值

function setRem() {
  const baseSize = 16; // 基准字体大小
  const designWidth = 750; // 设计稿宽度
  const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
  const fontSize = viewportWidth / designWidth * baseSize;
  document.documentElement.style.fontSize = fontSize + 'px';
}

setRem();
window.onresize = setRem;

或者使用 lib-flexible.js插件

原理是 将设计稿的宽度(1920px)平均分成24等份,每一份为80px。将这个值设置为html字体大小,既1rem = 80px; 24rem = 1920px。在移动端通常会分成10份(375/10),PC端分成24(1080/24)份

vw/vh 方案

直接使用vw单位,屏幕宽度默认为100vw,那么100vw = 1920px;1vw = 19.2px。直接将body的宽高(1920px * 1080px),将px转成vw单位。

scale 方案
  • 根据宽高比例进行动态缩放(宽度比率=网页当前宽度/设计稿宽度),代码简洁,几行代码即可解决,但是遇到一些地图或者 Canvas 中的点击事件,可能会存在错位问题,需要做针对性的调整适配
<script>
    // 设计稿:1920 * 1080
    // 1.设计稿尺寸
    let targetWidth = 1920;
    // 2.拿到当前设备(浏览器)的宽度
    // document.documentElement  获取html的宽度
    let currentWidth =
      document.documentElement.clientWidth || document.body.clientWidth;
    // 3.计算缩放比率(屏幕过宽,根据高度计算缩放比例)
    let scaleRatio = currentWidth / targetWidth; 
    // 4.开始缩放网页
    document.body.style = `transform: scale(${scaleRatio})`;
  </script>

针对1920 * 1080,3840 * 2160(4k)是没有问题的,但是在超宽屏的情况下还是存在只显示一半的问题。

分析原因:

1920 * 1080 => 要适配 (1920*2=3840, 1080*2=2160, 4k屏) 3840 * 2160
也要适配=> ( 1920*4 = 7680 : 1080 * 2 = 2160) 7680 * 2160 
 
我们当前是根据宽度比率进行缩放的:
 
先设配3840 * 2160
 
scaleRatio = 3840 / 1920  = 2
 
根据这个缩放比率
 
我们的设计稿宽高都会被缩放两倍
 
1920 * 2 = 3840
 
1080 * 2 = 2160
 
 
 
设配7680 * 2160
 
scaleRatio = 7680 / 1920  =  4
 
根据这个宽度比例我们的设置稿宽高都会被缩放41920 * 4 = 7680
 
1080 * 4  = 4240 
这个原先的比例是 4 : 2,现在变成了 44 ,这也是为什么我们只看到一半高度的原因。 
  • 动态计算

动态计算网页宽高比,决定是按照宽度的比例还是高度的比例进行缩放。

  <script>
    // 设计稿:1920 * 1080
    // 1.设计稿尺寸
    let targetWidth = 1920;
    let targetHeight = 1080;
 
    let targetRatio = 16 / 9; // 宽高比率 (宽 / 高)
 
    // 2.拿到当前设备(浏览器)的宽度和高度
    let currentWidth =
      document.documentElement.clientWidth || document.body.clientWidth;
 
    let currentHeight =
      document.documentElement.clientHeight || document.body.clientHeight;
 
    // 3.计算缩放比率(屏幕过宽,根据高度计算缩放比例)
		// 若currentWidth是4k屏宽度 3840 除于 我们设计稿的宽度 1920  3840/1920 = 2
		// 这样页面就行进行2倍缩放
    let scaleRatio = currentWidth / targetWidth; // 参照宽度进行缩放(默认情况下)
		
    // 当前页面宽高比例,当页面越宽currentRatio值就越大
    let currentRatio = currentWidth / currentHeight;
		
		// 判断是根据宽度进行缩放,还是根据高度进行缩放
    if (currentRatio > targetRatio) {
      // 根据高度进行网页的缩放
      scaleRatio = currentHeight / targetHeight; // 参照高度进行缩放(屏幕很宽的情况下)
      document.body.style = `transform: scale(${scaleRatio}) translateX(-50%)`;
    } else {
      // 根据宽度进行网页的缩放
      document.body.style = `transform: scale(${scaleRatio})`;
    }
  </script>

但是在开发过程中需要对每个字体容器去做相应的计算调整,相对来说较为繁琐,而且在团队协作过程中也容易出现问题。

那么有没有一种方式,只需要简单的一些配置,就能完全搞定大屏在不同尺寸的屏幕上都能实现良好的适配

以下给大家推荐三个方案,只需要简单的几行代码配置,可以完全解决大屏开发中的适配问题,让你效率翻倍!!!

autofit.js

autofit.js 基于比例缩放原理,通过动态调整容器的宽度和高度来实现全屏填充,避免元素的挤压或拉伸。

autofit.js 提供了一种简单而有效的方法来实现网页的自适应设计,尤其适合需要在不同分辨率屏幕尺寸下保持布局一致性的应用场景。

原理

utofit.js**的原理‌是基于比例缩放,通过动态调整容器的宽度和高度来实现全屏填充,同时保持元素原有的比例,避免挤压或拉伸元素。这种方法确保了设计的每一部分都能按照预期展示,不会出现视觉上的失真‌12。

autofit.js的工作机制

  1. 比例缩放‌:autofit.js通过CSStransformscale属性来实现比例缩放。这种方法不会改变元素在文档中的实际占位,只是视觉上的缩放,从而避免了留白和元素挤在一起的问题‌。

  2. 全屏填充‌:autofit.js通过设置容器的宽度和高度来确保元素充满整个屏幕,同时保持元素的原始比例,不会进行挤压或拉伸‌

配置
import autofit from 'autofit.js';
onMounted(() => {
    autofit.init({
        el: '#page',
        dw: 375,
        dh: 667
    })
})
   * - 传入对象,对象中的属性如下:
   * - el(可选):渲染的元素,默认是 "body"
   * - dw(可选):设计稿的宽度,默认是 1920
   * - dh(可选):设计稿的高度,默认是 1080
   * - resize(可选):是否监听resize事件,默认是 true
   * - ignore(可选):忽略缩放的元素(该元素将反向缩放),参数见readme.md
   * - transition(可选):过渡时间,默认是 0
   * - delay(可选):延迟,默认是 0

v-scale-screen

大屏自适应容器组件,可用于大屏项目开发,实现屏幕自适应,可根据宽度自适应高度自适应,和宽高等比例自适应全屏自适应(会存在拉伸问题),如果是 React 开发者,可以使用 r-scale-screen

demo

<template>
  <v-scale-screen width="1920" height="1080">
    <div>
      <v-chart>....</v-chart>
      <v-chart>....</v-chart>
      <v-chart>....</v-chart>
      <v-chart>....</v-chart>
      <v-chart>....</v-chart>
    </div>
  </v-scale-screen>
</template>

<script>
import { defineComponent } from 'vue'
import VScaleScreen from 'v-scale-screen'

export default defineComponent({
  name: 'Test',
  components: {
    VScaleScreen
  }
})
</script>

ie 浏览器兼容

HTML 兼容

一、超链接访问后样式就不出现问题

问题说明

被点击访问过的超链接样式不再具有hover和active样式了

解决方案

是改变CSS属性的排列顺序: L-V-H-A

a:link {}
a:visited {}
a:hover {}
a:active {}

二、 IE10 版本以上浏览器 input 标签后面自带一个 X 问题

image.png

解决方案

input::-ms-clear{display:none;}

三、IE 不支持 html5 标签

解决方案

使用 html5shiv.js 库

CSS 兼容

一、IE 不支持 CSS3 新特性

解决方案

在属性前加浏览器前缀兼容早期浏览器

-moz- :火狐浏览器 
-webkit- : Safari, 谷歌浏览器等使用Webkit引擎的浏览器  
-o- Opera:浏览器(早期) 
-ms- :IE// 实例

-webkit-box-shadow: #000 0px 1px 2px;
-moz-box-shadow: #000 0px 1px 2px;
-ms-box-shadow: #000 0px 1px 2px;
-o-box-shadow: #000 0px 1px 2px;
box-shadow: #000 0px 1px 2px;

二、不同浏览器的标签默认的 margin 和 padding 不同

解决方案

使用通配符

 {margin: 0;padding: 0}

三、ie 浏览器最小高度和最小宽度不生效问题

IE不认得 min- 这个定义,但实际上它把正常的 width 和 height 当作有min的情况来使。这样问题就大了,如果只用宽度和高度,正常的浏览器里这两个值就不会变,如果只用 min-width 和 min-height 的话,IE下面根本等于没有设置宽度和高度。比如要设置背景图片,这个最小宽度是比较重要的。

解决方案
#box {
   width: 80px;
   height: 35px;
}
html>body #box {
    width: auto;
    height: auto;
    min-width: 80px;
    min-height: 35px;
}

四、IE8 不支持媒体查询

在页面中所有css文件的引用位置之后引用Respond.js。而且Respond.js的引用得越早,用户看到页面闪烁的机会越小

<script src="https://cdn.bootcss.com/respond.js/1.1.0/respond.min.js"></script>

五、图片默认有间距

问题说明

几个 img 标签放在一起的时候,有些浏览器会有默认的间距

解决方案

使用 float 属性为 img 布局(所有图片左浮)

JS 兼容

一、事件绑定

其他浏览器

dom.addEventListener('click',function(event){},false);

ie7 ie8

dom.attachEvent();

二、阻止事件冒泡传播、

 // js阻止事件传播,这里使用click事件为例
document.onclick = function (e) {
    var e = e || Window.event;
    if (e.stopPropagation) {
         // W3C标准
        e.stopPropagation(); 
    } else {
         // IE... .
        e.cancelBubble = true; 
    }
}

三、阻止事件默认行为

// js阻止默认事件,一般阻止a链接href, form表单submit提交
document.onclick = function (e) {
    var e = e || Window.event;
    if (e.preventDefault) {
        // W3C标准
        e.preventDefault();
    } else {
         // IE...  .
        e.returnValue = false; 
    }
}