前端学习&复习-重点篇

260 阅读27分钟

引言


整理一些前端常见且较为重要的知识点。

一、js

1、 闭包

在前端JavaScript编程中,闭包(Closure)是一种非常重要的概念,它涉及到函数作用域、变量访问以及内存管理等多个方面。以下是对前端闭包的详细说明:

① 定义与构成

闭包是指有权访问另一个函数(通常称为外部函数)作用域中变量的函数(通常称为内部函数)。它由两部分构成:

  • 内部函数:定义在外部函数内部的函数。这个内部函数可以访问外部函数的作用域,包括其参数、局部变量和声明的任何对象。
  • 外部函数的作用域(或称词法环境):当外部函数被执行时创建的一个作用域,其中包含了该函数执行期间可见的所有变量和参数。即使外部函数已经执行完毕(即返回了),这个作用域并不立即销毁,而是被内部函数所保留,使得内部函数可以继续访问这些变量。
②闭包的创建与示例

闭包通常通过以下方式创建:

function outerFunction(arg) {
  var outerVar = arg; // 外部函数的局部变量

  function innerFunction() {
    console.log(outerVar); // 内部函数访问外部函数的局部变量
  }

  return innerFunction; // 返回内部函数,形成闭包
}

var closure = outerFunction("Hello, Closure!"); // 调用外部函数并保存返回的内部函数
closure(); // 输出: "Hello, Closure!"

在这个例子中,outerFunction 是外部函数,它接受一个参数 arg 并定义了一个局部变量 outerVar。内部函数 innerFunction 可以访问这些外部变量。当 outerFunction 执行完毕并返回 innerFunction 时,形成了一个闭包。调用返回的 closure 函数时,尽管 outerFunction 已经执行结束,但因为闭包的存在,innerFunction 仍能访问并打印 outerVar 的值。

③闭包的作用
  • 保护函数的私有变量不受外部的干扰。形成不销毁的栈内存保护函数的私有变量不受外部的干扰。形成不销毁的栈内存。
  • 保存,把一些函数内的值保存下来。闭包可以实现方法和属性的私有化。
④闭包的缺陷

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

2、 原型、原型链

①原型(Prototype)

原型是一个JavaScript对象,它与其它对象之间存在隐含的关联关系。在JavaScript中,每当创建一个新对象(无论是通过new关键字构造函数创建,还是通过字面量直接创建),这个对象都会有一个特殊的内部属性[[Prototype]](在代码层面可通过Object.getPrototypeOf()方法或非标准但广泛支持的__proto__属性访问),该属性指向该对象的原型对象。

原型对象本身也是一个普通的JavaScript对象,它可以包含属性(包括方法)供与之关联的对象共享。这意味着,如果一个对象无法在其自身属性中找到某个属性或方法,JavaScript引擎会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的末端(即null)。

②原型链(Prototype Chain)

原型链是通过对象的[[Prototype]]属性链接起来的一系列原型对象,它定义了对象之间的继承关系。当试图访问一个对象的属性或方法时,如果该对象本身没有定义该属性或方法,JavaScript引擎会自动到其原型对象中去寻找。如果原型对象也没有,就会继续向上查找其原型的原型,依此类推,直至找到该属性或方法,或者到达原型链的顶端(即null),此时视为未找到该属性。

示例:

// 创建一个构造函数
function Person(name) {
  this.name = name;
}

// 给Person的原型添加一个方法
Person.prototype.sayName = function() {
  console.log(this.name);
}

// 使用构造函数创建新对象
var person1 = new Person("Alice");
var person2 = new Person("Bob");

// 访问对象的方法
person1.sayName(); // 输出: Alice
person2.sayName(); // 输出: Bob

// 查看对象的原型链
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype) === null); // true

在这个示例中:

  • Person是一个构造函数,它有一个原型对象Person.prototype
  • sayName方法被添加到Person.prototype上,使得所有通过Person构造函数创建的对象(如person1person2)都能共享这个方法。
  • person1person2[[Prototype]]属性都指向Person.prototype
  • Person.prototype[[Prototype]]又指向Object.prototype,因为所有的原型对象(除了null)最终都会继承自Object.prototype
  • Object.prototype[[Prototype]]null,标志着原型链的终点。
③原型链的应用与意义

原型链机制为JavaScript提供了以下功能和便利:

  • 继承:通过原型链,对象可以继承其原型对象的属性和方法,实现代码复用和对象间的层次结构。
  • 属性查找:JavaScript引擎按照原型链顺序查找对象的属性,实现动态绑定和多态。
  • 方法共享:多个对象可以通过原型链共享同一组方法,节省内存。
  • 原型对象的修改影响所有实例:对原型对象的属性或方法进行增删改,会影响所有基于该原型创建的对象。

理解原型和原型链是深入掌握JavaScript面向对象特性和行为的关键,有助于编写高效、可维护的代码,并能更好地利用诸如原型继承、混入(mixin)、代理(proxy)等高级特性。

3、 继承

JavaScript 中的继承是实现对象间属性和方法共享的一种机制,使其能够模拟面向对象编程中的类继承概念。由于 JavaScript 是一门基于原型的语言,它提供了多种实现继承的方式,这些方式主要围绕着原型链、构造函数、以及 ES6 引入的类(class)展开。以下是JavaScript中常见的几种继承方法:

①原型链继承

原理:通过将子类的原型(prototype)设置为父类的一个实例,使得子类实例可以沿着原型链向上查找父类的属性和方法。 示例

function Parent(name) {
  this.name = name;
}

Parent.prototype.greet = function() {
  console.log(`Hello, I am ${this.name}`);
}

function Child(name, age) {
  Parent.call(this, name); // 调用父类构造函数初始化子类实例
  this.age = age;
}

// 子类继承父类:将子类原型指向父类的一个实例
Child.prototype = new Parent();

Child.prototype.describe = function() {
  console.log(`I am ${this.age} years old.`);
}

const child = new Child("Alice", 25);
child.greet(); // 输出: Hello, I am Alice
child.describe(); // 输出: I am 25 years old.
②构造函数继承(借用构造函数/经典继承)

原理:在子类构造函数内部调用父类构造函数,直接复制父类的属性到子类实例上。

示例

function Parent(name) {
  this.name = name;
}

function Child(name, age) {
  Parent.call(this, name); // 调用父类构造函数初始化子类实例
  this.age = age;
}

const child = new Child("Alice", 25);
child.name; // 输出: "Alice"
child.age; // 输出: 25
③组合继承(原型链+构造函数继承)

原理:结合原型链继承和构造函数继承,既通过原型链实现方法的继承,又通过构造函数调用实现属性的继承,避免了原型链继承中子类实例共享父类实例属性的问题。

示例

function Parent(name) {
  this.name = name;
}

Parent.prototype.greet = function() {
  console.log(`Hello, I am ${this.name}`);
}

function Child(name, age) {
  Parent.call(this, name); // 调用父类构造函数初始化子类实例属性
  this.age = age;
}

// 子类继承父类原型上的方法,而不是父类实例
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child; // 修复构造函数引用

Child.prototype.describe = function() {
  console.log(`I am ${this.age} years old.`);
}

const child = new Child("Alice", 25);
child.greet(); // 输出: Hello, I am Alice
child.describe(); // 输出: I am 25 years old.
④原型式继承

原理:通过Object.create()或借助函数返回一个新对象,该对象的原型([[Prototype]])被设置为指定的对象,实现基于现有对象的轻量级继承。

示例

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

const parent = {
  name: "Parent",
  greet: function() {
    console.log(`Hello, I am ${this.name}`);
  }
};

const child = object(parent);
child.name = "Child";
child.greet(); // 输出: Hello, I am Child
⑤ES6 类(Class)继承

原理:ES6 引入了更接近传统面向对象语言的class语法糖,其底层仍然是基于原型链的继承,但提供了更清晰、更易于理解的继承表达形式。

示例

class Parent {
  constructor(name) {
    this.name = name;
  }

  greet() {
    console.log(`Hello, I am ${this.name}`);
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name); // 调用父类构造函数
    this.age = age;
  }

  describe() {
    console.log(`I am ${this.age} years old.`);
  }
}

const child = new Child("Alice", 25);
child.greet(); // 输出: Hello, I am Alice
child.describe(); // 输出: I am 25 years old.

总结来说,JavaScript 提供了多种实现继承的机制,我们可以根据具体需求选择合适的方式来实现对象间的继承关系。随着 ES6 类的引入,推荐使用class及其extends关键字来编写更加清晰、易于维护的继承代码。然而,理解底层的原型链原理对于深入理解JavaScript对象模型和解决复杂继承问题仍然非常重要。

4、 类

在JavaScript中,类(Class) 是一种面向对象编程的概念,用于定义对象的模板或蓝图,规定了对象的属性和方法。虽然JavaScript本质上是一门基于原型的语言,但为了更好地适应传统的面向对象编程范式,从ES6(ECMAScript 6)开始,JavaScript引入了正式的class语法糖,使得定义类及其继承关系变得更加清晰和直观。以下是关于JavaScript类的详细说明:

①定义类

使用class关键字定义一个类,后面紧跟类名。类体内部可以包含以下组成部分:

  • 构造函数(Constructor) :使用constructor方法定义,用于初始化新创建的对象实例。构造函数通常包含this关键字来设置实例属性。
class MyClass {
  constructor(param1, param2) {
    this.property1 = param1;
    this.property2 = param2;
  }
}
  • 方法(Methods) :在类体内定义的函数,成为类的方法。方法与普通函数的区别在于它们绑定在类的原型上,可供所有实例共享。
class MyClass {
  method1() {
    console.log("Method 1 called.");
  }

  method2() {
    console.log("Method 2 called.");
  }
}
  • 静态方法(Static methods) :使用static关键字修饰的方法,属于类本身而非实例,可以直接通过类来调用,不需实例化。
class MyClass {
  static staticMethod() {
    console.log("Static method called.");
  }
}

MyClass.staticMethod(); // 输出: Static method called.
②实例化与访问

要创建一个类的实例,使用new关键字:

const instance = new MyClass(value1, value2);

实例化后,可以通过.运算符访问实例的属性和方法:

instance.property1; // 访问实例属性
instance.method1(); // 调用实例方法
③继承

使用extends关键字实现类的继承。子类可以继承父类的属性和方法,并可以通过super关键字调用父类的构造函数和方法:

class ParentClass {
  constructor(param) {
    this.parentProperty = param;
  }

  parentMethod() {
    console.log("Parent method called.");
  }
}

class ChildClass extends ParentClass {
  constructor(param1, param2) {
    super(param1); // 调用父类构造函数
    this.childProperty = param2;
  }

  childMethod() {
    console.log("Child method called.");
  }
}

const childInstance = new ChildClass(value1, value2);
childInstance.parentProperty; // 访问继承的父类属性
childInstance.parentMethod(); // 调用继承的父类方法
childInstance.childProperty; // 访问子类属性
childInstance.childMethod(); // 调用子类方法
④静态属性与实例属性

在类的定义中,可以使用static关键字定义静态属性,它们属于类本身,所有实例共享同一份静态属性:

class MyClass {
  static staticProperty = "Static property value";

  constructor() {
    this.instanceProperty = "Instance property value";
  }
}

MyClass.staticProperty; // 输出: "Static property value"
const instance = new MyClass();
instance.instanceProperty; // 输出: "Instance property value"
⑤私有方法与私有属性

JavaScript并未直接提供类的私有方法与私有属性的语法。但可以通过一些技巧实现类似效果,如:

  • 闭包:在类的构造函数内部定义私有方法或变量,通过返回一个对象来暴露必要的接口。
  • Symbol:使用Symbol创建唯一标识符作为私有属性的键,外部难以直接访问。
  • WeakMap:配合闭包或Symbol,将私有属性存储在 WeakMap 中,关联到实例上。
  • ES2022 私有类成员(#语法):最新版本的JavaScript提供了原生的私有类成员支持,使用#符号表示私有属性或方法。
class MyClass {
  #privateProperty;

  constructor(value) {
    this.#privateProperty = value;
  }

  getPrivateProperty() {
    return this.#privateProperty;
  }
}

const instance = new MyClass("secret");
instance.getPrivateProperty(); // 输出: "secret"
instance.#privateProperty; // 报错: SyntaxError: Private field '#privateProperty' must be declared in an enclosing class

总的来说,JavaScript的class语法为开发者提供了更符合传统面向对象习惯的类定义方式,简化了构造函数、原型链等底层细节,便于理解和维护面向对象代码。同时,类还支持继承、静态方法、私有属性等特性,进一步丰富了JavaScript的面向对象编程能力。

5、 promise

Promise 是 JavaScript 中一种用于处理异步操作的对象,它提供了一种统一、简洁的异步编程模式,解决了回调函数嵌套过深(“回调地狱”)的问题,并提供了链式调用、错误处理、并发控制等功能。Promise 是 ES6 标准的一部分,现在已经成为现代 JavaScript 开发中的基础工具之一。以下是对 Promise 的详细介绍:

①定义与基本用法

Promise 是一个构造函数,用于创建 Promise 实例。Promise 实例代表一个异步操作,有三种状态:

  • Pending(待定) :初始状态,既不是成功也不是失败。
  • Fulfilled(已成功) :操作成功完成,此时 Promise 对象有一个不可变的值。
  • Rejected(已失败) :操作失败,此时 Promise 对象有一个不可变的原因(通常是一个错误对象)。

创建 Promise 实例时,传入一个函数(executor),该函数接受两个参数:resolve 和 reject,分别用于将 Promise 状态变为 fulfilled 或 rejected:

const promise = new Promise((resolve, reject) => {
  if (/* 异步操作成功 */) {
    resolve(result); // 参数为成功的结果
  } else {
    reject(error); // 参数为失败的原因(通常是一个 Error 对象)
  }
});
②then 方法

Promise 实例的主要交互方式是通过 then 方法,它接受两个可选的回调函数作为参数:

  • onFulfilled:当 Promise 状态变为 fulfilled 时调用,接收到 fulfilled 时传递的值。
  • onRejected:当 Promise 状态变为 rejected 时调用,接收到 rejected 时传递的原因。
promise.then(
  result => {
    // 处理成功结果
  },
  error => {
    // 处理失败原因
  }
);

then 方法返回一个新的 Promise,这样可以方便地进行链式调用:

promise
  .then(result1 => {
    // 处理 result1
    return anotherAsyncOperation(result1); // 返回一个新的 Promise
  })
  .then(result2 => {
    // 处理 result2
  })
  .catch(error => {
    // 处理任何前面 Promise 链中出现的错误
  });
③catch 方法

catch 方法是 then(null, onRejected) 的简写,用于处理 Promise 链中出现的任何错误。通常放在 then 链的末尾,捕获前面所有 then 中未处理的 rejection:

promise
  .then(result => {
    // ...
  })
  .catch(error => {
    // 处理任何前面 Promise 链中出现的错误
  });
④finally 方法

finally 方法无论 Promise 最终状态如何,都会执行提供的回调函数。常用于清理资源、统一关闭加载提示等:

promise
  .then(result => {
    // ...
  })
  .catch(error => {
    // ...
  })
  .finally(() => {
    // 无论成功或失败,都会执行这里的代码
  });
⑤async/await

ES8 引入了 async 函数和 await 关键字,它们基于 Promise 实现,提供了更接近同步代码的异步编程体验。async 函数返回一个 Promise,await 可以用于等待 Promise 解决:

async function asyncFunction() {
  try {
    const result = await somePromise();
    // 使用 result
  } catch (error) {
    // 处理 error
  }
}
⑥其他 Promise 方法
  • Promise.resolve(value) :创建一个已 resolved 的 Promise,直接返回给定的值。
  • Promise.reject(reason) :创建一个已 rejected 的 Promise,给出失败的原因。
  • Promise.all(iterable) :接收一个 Promise 对象的数组(或其他可迭代对象)作为参数,只有当所有 Promise 都 fulfilled 时,返回的 Promise 才 fulfilled,此时返回一个包含所有结果的数组;只要有一个 Promise rejected,返回的 Promise 就 rejected,此时返回第一个 rejected 的原因。
  • Promise.race(iterable) :接收一个 Promise 对象的数组(或其他可迭代对象)作为参数,只要有一个 Promise fulfilled 或 rejected,返回的 Promise 就相应地 fulfilled 或 rejected,返回值或原因取自率先改变状态的那个 Promise。

总之,Promise 为 JavaScript 异步编程带来了巨大的改进,通过统一的 API 和链式调用,极大地简化了异步操作的管理和错误处理,配合 async/await 语法,使得异步代码更加简洁易读。熟练掌握 Promise 是编写现代 JavaScript 应用程序的重要基础。

6、 事件循环原理

事件循环(Event Loop) 是JavaScript运行时环境(如浏览器环境或Node.js环境)中处理异步任务的核心机制。它确保了JavaScript的单线程非阻塞执行模型能够有效地处理异步事件,如网络请求、定时器、用户交互等。事件循环的原理主要包括以下几个关键概念和步骤:

①单线程执行

JavaScript是单线程语言,意味着在同一时间只能执行一个任务。这种设计避免了多线程环境下的竞态条件和同步问题,但同时也要求JavaScript必须采用异步处理机制来处理耗时操作,以避免阻塞主线程导致用户界面无响应(UI冻结)。

②任务队列(Task Queue)与微任务队列(Microtask Queue)

事件循环管理着两种类型的任务队列:

  • 任务队列(Task Queue) :也称为宏任务队列,存放着各种异步任务,如用户事件回调(如点击事件、键盘事件)、setTimout回调、setInterval回调、网络请求回调、I/O操作回调等。
  • 微任务队列(Microtask Queue) :存放着微任务,这类任务优先级高于宏任务,通常包括Promise回调(.then.catch.finally)、MutationObserver回调、process.nextTick(Node.js环境中)等。
③事件循环流程

事件循环遵循以下基本流程:

  1. 主线程执行全局脚本:JavaScript引擎首先执行主程序中的同步代码。
  2. 检查微任务队列:在当前执行上下文结束前(即同步代码执行完毕后),事件循环会检查微任务队列。如果有微任务,就取出第一个微任务执行,然后再次检查微任务队列,直到微任务队列为空。
  3. 渲染(浏览器环境):在所有微任务执行完毕后,浏览器可能在此阶段进行页面渲染(不是所有情况下都会发生,视具体浏览器实现和任务类型而定)。
  4. 执行下一个宏任务:从任务队列中取出下一个宏任务放到主线程上执行。这可能是之前注册的回调函数,如定时器触发的函数、用户交互事件的回调等。
  5. 重复上述过程:执行完宏任务后,再次回到步骤2,检查微任务队列,执行微任务,如此反复循环。
示例说明

下面是一个简单的示例,帮助理解事件循环的工作原理:

console.log('1');

setTimeout(function() {
  console.log('2');
}, 0);

Promise.resolve().then(function() {
  console.log('3');
});

console.log('4');

输出顺序将是:

1
4
3
2

解释如下:

  1. 同步代码首先打印出14
  2. setTimeout回调被安排到宏任务队列,Promise.resolve().then回调被安排到微任务队列。
  3. 当同步代码执行完毕,事件循环检查微任务队列,发现并执行Promise回调,打印出3
  4. 微任务队列清空后,事件循环从宏任务队列取出第一个任务(setTimeout回调)执行,打印出2
总结

事件循环机制确保了JavaScript单线程环境下异步任务的有序、高效执行。它通过任务队列(包括宏任务队列和微任务队列)管理异步任务,并按照特定的优先级顺序(先微任务后宏任务)执行这些任务。理解事件循环对于编写高效的异步JavaScript代码,尤其是正确处理异步操作的执行顺序和依赖关系至关重要。

7、 变量回收机制

变量回收机制,也称为内存管理或垃圾回收机制,是指编程语言运行时系统自动识别并释放不再使用的内存区域的过程。在JavaScript中,变量回收机制是自动进行的,无需开发者手动干预。JavaScript的垃圾回收机制主要基于两种策略:标记清除引用计数。不过,现代浏览器和JavaScript引擎普遍使用标记清除算法,而引用计数算法由于其局限性,已较少被使用。

①标记清除(Mark-and-Sweep)

原理:标记清除算法分为两个阶段:

  • 标记阶段:垃圾回收器遍历所有可达(reachable)的变量和对象,将它们标记为活跃(alive)。可达对象指的是从根对象(如全局对象、当前执行环境的变量、闭包等)出发,通过引用关系能够直接或间接访问到的对象。
  • 清除阶段:垃圾回收器再次遍历内存空间,清除所有未被标记的变量和对象,释放它们所占用的内存。这些未被标记的对象被视为不可达(unreachable),即不再有任何有效引用指向它们,因此可以安全地回收。

优点:能够处理循环引用的问题,不易造成内存泄漏。

②引用计数(Reference Counting)

原理:每个对象都维护一个引用计数器,每当有一个新的引用指向该对象时,计数器加一;当引用失效(如赋值为null或引用变量离开作用域)时,计数器减一。当一个对象的引用计数器变为零时,表明没有任何引用指向它,垃圾回收器可以立即回收该对象。

缺点:无法处理循环引用的情况,可能导致内存泄漏。例如,两个对象互相引用但不再被其他对象引用时,由于它们的引用计数都不为零,垃圾回收器不会回收它们,尽管它们在逻辑上已经是不可达的。

③现代JavaScript中的变量回收

现代浏览器和JavaScript引擎(如V8,用于Chrome和Node.js)普遍采用增量标记(Incremental Marking)并行标记(Parallel Marking) 、**增量压缩(Incremental Compaction)**等优化技术,提升垃圾回收的效率和性能。这些技术旨在减少垃圾回收对程序运行的影响,尤其是在复杂且内存密集型的应用中。

注意事项

  • 闭包:闭包可能导致变量即使在函数执行完毕后仍被保留,因为闭包内的函数可以访问并保持对外部变量的引用。如果不再需要这些变量,应确保解除闭包对其的引用。
  • 全局变量:全局变量在整个程序生命周期内都是可达的,除非显式地将其赋值为null来断开引用。
  • 定时器和回调:定时器回调、事件监听器回调、异步操作回调等如果引用了局部变量,这些变量会在回调执行前保持存活。确保在不再需要时清理相关定时器和解除事件监听器,以释放相关变量。
  • DOM元素引用:JavaScript代码中对DOM元素的引用也会阻止这些元素及相关的JavaScript对象被垃圾回收。在移除DOM节点后,记得删除所有对这些节点的引用。

总结来说,JavaScript的变量回收机制主要是通过标记清除算法自动进行的,开发者无需直接管理内存。理解垃圾回收的基本原理有助于识别并避免潜在的内存泄漏问题,特别是在处理闭包、全局变量、定时器、回调函数和DOM元素引用等情况时。

二、vue

1. vue2响应式原理

Vue 2 的响应式原理是其核心功能之一,实现了数据与视图之间的自动同步。该原理主要包括以下几个关键步骤:

数据劫持(Data Hijacking):
  • 当创建一个 Vue 实例时,Vue 会对传入的 data 对象进行递归遍历,对其中的每个属性使用 Object.defineProperty()  方法进行处理。
  • Object.defineProperty() 是 ES5 提供的一个方法,允许直接在对象上定义一个新属性或修改现有属性,并可以精确控制该属性的 getter、setter、枚举性(enumerable)、可配置性(configurable)等特性。
  • 在 Vue 中,对每个属性应用 Object.defineProperty() 时,会为其创建 getter 和 setter。getter 用于读取属性值时触发依赖收集,setter 用于在属性值被修改时触发依赖通知。
依赖收集(Dependency Collection):
  • 当 Vue 模板编译成渲染函数后,执行渲染时会访问到 data 对象中的各个属性。每当模板中某个表达式的值需要被求解时,会触发相应属性的 getter。
  • 这些 getter 被设计为在被调用时将当前活跃的 Watcher(观察者)对象添加到一个称为 Dep(依赖)的对象中。Dep 负责管理所有依赖它的 Watcher。
  • Watcher 代表的是对数据的“观察”,通常关联到组件的某个计算属性、模板表达式或用户自定义的 watch 函数。当一个 Watcher 被创建并开始计算时,它会进入“活跃”状态,此时访问的数据属性就会将其自身(Watcher)注册到对应的 Dep 中。
派发更新(Notification Dispatch):
  • 当数据属性的值发生变化时,会触发对应的 setter。setter 中的逻辑会遍历该属性所关联的 Dep 中收集的所有 Watcher,并调用它们的 update() 方法。
  • update() 方法会将 Watcher 标记为待重新计算,并将其推入一个队列中等待异步处理。Vue 使用异步队列来批量更新 Watcher,以提高性能,避免过度频繁地触发 DOM 更新。
  • 最终,这些待更新的 Watcher 会重新计算其关联的值(可能是计算属性的结果,也可能是视图的一部分),如果计算结果与之前不同,就会触发相应的视图更新操作(如使用 Virtual DOM 技术对比差异并更新实际 DOM)。

总结来说,Vue 2 的响应式原理通过数据劫持实现对数据属性的拦截,利用 getter/setter 收集和触发依赖关系,确保当数据变化时,相关的 Watcher 能够得到通知并执行更新逻辑,进而驱动视图的同步更新。这一机制使得开发者只需关注数据层的操作,无需手动管理视图与数据之间的同步关系,极大地提升了开发效率和代码的可维护性。

2. vue3响应式原理

Vue 3 对其响应式系统进行了重大的重构,引入了基于 ES6 新特性 Proxy 的实现方式,以应对 Vue 2 中的一些限制并提升性能和易用性。以下是 Vue 3 响应式原理的关键要点:

代理对象(Proxied Object):
  • Vue 3 使用 Proxy 对象代替了 Vue 2 中的 Object.defineProperty(),以实现对整个数据对象的透明代理。 - Proxy 是 ES6 引入的一种新特性,它可以创建一个目标对象的代理,对外提供与目标对象相同的接口,但在访问或修改属性时可以插入自定义的拦截逻辑。
  • 在 Vue 3 中,当使用 reactive() 函数创建响应式对象时,它会返回一个由 Proxy 包装的目标对象。这样,对代理对象的所有操作(包括访问、设置、删除属性,以及遍历、冻结等)都可以通过 Proxy 的 handler(处理器)进行拦截。
响应式转换(Reactive Transformation):
  • 与 Vue 2 类似,Vue 3 也会递归地将对象的属性转化为响应式。但区别在于,Vue 3 不仅对对象本身的属性进行响应式处理,还会对嵌套对象和数组的属性进行递归转换。
  • 数组也有专门的处理逻辑,通过 Reflect.apply() 和 Array.prototype 方法的代理来实现数组索引和数组长度变更的响应式。
依赖收集与通知(Dependency Tracking and Notification):
  • Vue 3 中的依赖收集与通知机制同样基于依赖跟踪系统,但实现细节有所不同。
  • 当访问代理对象的属性时,会触发 get 拦截器,该拦截器会将当前的副作用(如计算属性、组件渲染函数等)注册到对应的 Dep(依赖收集器)中,类似于 Vue 2 中的依赖收集过程。
  • 当修改代理对象的属性时,会触发 set 拦截器,它会触发依赖收集器的 notify() 方法,通知所有依赖于该属性的副作用(即 Watcher)进行更新。与 Vue 2 类似,Vue 3 也采用了异步队列来批量处理这些更新。
优化改进
  • 深度监听:由于 Proxy 可以对整个对象进行代理,Vue 3 的响应式系统能够自动处理新增或删除的属性,无需像 Vue 2 那样使用 Vue.set() 或 Vue.delete() 特殊方法来保证响应式。
  • 性能提升Proxy 的拦截机制通常比 Object.defineProperty() 更高效,尤其是在大量属性和深层嵌套对象的情况下。此外,Vue 3 还引入了 track() 和 trigger() 函数来进一步优化依赖收集和更新通知的过程。
  • 更细粒度的反应:Vue 3 可以精确地追踪到对象属性级别的依赖,从而实现更细粒度的更新。这意味着只有当真正影响到视图的属性发生变化时,才会触发对应的视图更新,提高了更新效率。

综上所述,Vue 3 的响应式原理利用 Proxy 提供的全面拦截能力,实现了对数据对象的深度响应式化,并对依赖收集和通知机制进行了优化,提供了更好的性能、易用性和灵活性。这些改进使得 Vue 3 能够更好地适应现代前端应用的需求,特别是对于大型、复杂的应用场景。

3. 什么是mvvm

MVVM(Model-View-ViewModel)是一种软件架构模式,特别适用于构建用户界面(UI)应用程序,尤其是那些采用声明式编程技术和数据绑定机制的框架,如WPF、Silverlight、Angular、Vue.js等。MVVM模式旨在将应用程序的业务逻辑、数据状态管理和用户界面展示清晰地分离,以提高代码的可维护性、可测试性和可重用性,并促进开发人员与UI设计师之间的协作。

MVVM模式包含三个核心组成部分:

Model(模型)
  • 模型代表着应用程序的数据和业务逻辑。它封装了应用程序的核心数据结构和业务规则,负责数据的获取、存储、处理、验证以及与后端服务(如数据库或API)的交互。
  • 模型通常包含数据实体(如数据库表的映射对象)和业务逻辑方法,这些方法可能涉及数据查询、过滤、排序、校验以及与外部系统的通信。
View(视图)
  • 视图是用户与应用程序交互的可视化界面,它呈现模型中的数据,并允许用户通过各种控件(如按钮、文本框、列表等)与应用程序进行交互。
  • 视图通常是由HTML、XML、XAML等标记语言以及CSS样式定义的,或者是由图形界面工具生成的。在MVVM模式下,视图应尽可能地“薄”,即只负责展示数据和接收用户输入,而不包含复杂的逻辑处理。
ViewModel(视图模型)
  • 视图模型是连接模型与视图的桥梁,它包含了视图所需要的数据和行为(如命令),并将这些数据和行为以适合视图展示和交互的方式包装起来。
  • 视图模型对模型数据进行抽象和转换,提供视图所需的数据形态和格式,同时暴露属性和方法供视图绑定使用。
  • 视图模型还负责处理视图发出的用户操作,如按钮点击、表单提交等,并将这些操作转化为对模型的适当更新。此外,视图模型可能还包括状态管理、验证逻辑以及任何与视图交互密切相关的逻辑。
数据绑定与通知机制

MVVM模式的核心特性之一是双向数据绑定。视图模型与视图之间通过数据绑定技术自动保持同步:

  • 从模型到视图:当模型数据发生变化时,视图模型会接收到通知,并更新其内部的状态。由于视图与视图模型之间存在绑定关系,视图模型状态的改变会自动反映到视图上,更新用户界面。
  • 从视图到模型:用户在视图上的交互(如填写表单、选择选项等)会触发视图模型中的相关属性或命令。视图模型接收到这些变动后,会更新模型数据,同时触发数据验证和任何必要的业务逻辑。

这种自动化的数据同步机制减少了手动编写大量事件处理和DOM操作代码的需要,使得开发者可以专注于业务逻辑的实现,同时保持代码的清晰和简洁。

优势

MVVM模式的优势包括:

  • 分离关注点:通过明确划分模型、视图和视图模型的职责,使得各部分可以独立开发、测试和维护。
  • 易于测试:由于业务逻辑集中在模型和视图模型中,可以方便地进行单元测试和集成测试,而无需实际的用户界面。
  • 代码复用:视图模型可以独立于特定的视图技术,有利于跨平台或跨项目复用业务逻辑。
  • 设计与开发协作:设计师可以专注于视图的设计,开发者专注于视图模型的实现,两者之间的交互主要通过数据绑定和约定的接口进行,减少了沟通成本。

总之,MVVM模式通过视图模型这一中间层,有效地解耦了模型(数据与业务逻辑)与视图(用户界面),实现了数据状态与界面展示的自动同步,提升了现代UI应用程序的开发效率和质量。

4. keep-alive是什么,有什么作用

keep-alive 是一个在某些前端框架(如 Vue.js)中用于组件缓存的内置组件。它有两个主要作用:

  1. 保留组件状态: 当 keep-alive 包裹动态组件时,当用户离开该组件的路由(例如导航到其他页面,再返回),被包裹的组件实例并不会被销毁,而是被保存在内存中。这意味着组件的状态(如输入框的文本、滚动位置、内部变量等)会被保留,当用户再次访问该组件时,可以从缓存中直接取出已存在的实例,而不是重新创建一个新的实例。这样可以避免组件状态的丢失,提供更加流畅的用户体验,特别是在具有复杂状态或用户交互频繁的组件中。
  2. 避免重新渲染: 由于组件实例没有被销毁,只是从活跃状态变为非活跃状态(inactive),当它再次被激活时,不需要重新执行组件的 beforeCreatecreatedbeforeMountmounted 等生命周期钩子函数,而是直接进入 activated 生命周期钩子。这不仅节省了初始化和渲染的时间,还降低了因重新渲染可能导致的性能开销,特别是对于大数据列表、图表或计算密集型组件来说,能显著提高页面切换速度。

应用场景

  • 列表与详情页切换: 在常见的列表与详情页交互场景中,用户可能会频繁地在列表页面和详情页面之间来回切换。使用 keep-alive 包裹详情页组件,可以确保用户返回时详情内容仍保持上次查看的状态,无需重新加载。
  • 多标签页界面: 在支持多标签页浏览的应用中,即使用户暂时切换到其他标签页,原始标签页的内容也能保持原状,当用户重新选择该标签时,可以直接显示之前的状态,无需重新加载数据或重新渲染页面。
  • 频繁交互的表单: 对于包含复杂表单或用户正在编辑但尚未提交的数据的组件,使用 keep-alive 可以防止用户意外离开页面后返回时丢失已填写的信息。
  • 性能优化: 对于渲染成本较高、数据加载耗时或需要维持复杂内部状态的组件,通过缓存可以减少不必要的重复工作,提升整体应用性能。

生命周期钩子: keep-alive 组件为被包裹的组件提供了额外的生命周期钩子:

  • activated:当组件被激活(从缓存中取出并重新显示)时调用。
  • deactivated:当组件被停用(离开当前路由,但未被销毁,仍保留在缓存中)时调用。

这些钩子函数允许开发者在组件状态变化时执行特定的清理或恢复操作,如暂停定时器、取消网络请求、释放资源或重新订阅数据源等。

总结来说,keep-alive 组件通过缓存组件实例,实现了组件状态的持久化和避免不必要的重新渲染,为用户提供更流畅的页面切换体验,同时也作为一项重要的性能优化手段,常用于需要保持用户交互状态或减少渲染开销的场景。

5. diff算法

Diff算法是一种用于比较两个数据序列(如文本文件、文本字符串、二进制文件、XML文档、JSON数据等)并找出它们之间差异的技术。Diff算法在版本控制系统(如Git)、文件比较工具、文本编辑器的合并冲突解决、在线协作平台、自动化测试的断言检查、虚拟DOM的更新策略等领域有广泛应用。其基本思想是高效地识别出两个序列中哪些部分相同、哪些部分不同,以及如何将一个序列转换为另一个序列。

以下是Diff算法的主要特点和工作原理:

主要特点:
  1. 最小化差异:Diff算法力求找到最精简的差异描述,通常表现为最小数量的插入、删除、替换操作序列,使得经过这些操作后,一个序列可以变成另一个序列。
  2. 效率考虑:高效的Diff算法应具有较低的时间复杂度,以便在处理大规模数据时仍能快速得出结果。理想情况下,复杂度应与实际差异的数量(而非原始序列的长度)成正比。
  3. 通用性:Diff算法应能处理不同类型和结构的数据,如文本、二进制数据、树状结构(如XML、JSON、HTML)等。针对不同数据类型,可能存在特定的优化算法或变种。
工作原理:

Diff算法的基本流程通常包括以下步骤:

  1. 预处理

    • 对文本数据,可能进行字符规范化(如忽略空格、大小写)、行分割等预处理,以便简化后续比较。
    • 对结构化数据,可能需要先将其转换为线性表示(如XML树的深度优先遍历序列),或者使用专门的树形Diff算法。
  2. 序列匹配

    • 找出两个序列中尽可能长的相同(或相似)的子序列,这些子序列被称为最长公共子序列(Longest Common Subsequence, LCS)最长递增子序列(Longest Common Increasing Subsequence, LCIS)
    • 对于文本数据,可以使用动态规划算法(如Myers算法、Hunt-McIlroy算法)快速计算LCS。
    • 对于树形结构,可以使用启发式算法(如Levenshtein距离、Kumar-Rangan算法)或专门的树 Diff 算法(如Weiss-Schulz算法、Ryder算法)。
  3. 生成差异

    • 基于LCS(或相似概念),确定两个序列中哪些部分是相同的、哪些部分发生了变化。

    • 生成差异描述(如补丁文件、差异列表),通常包括以下操作类型:

      • 插入(Addition):在序列A中不存在但在序列B中存在的部分。
      • 删除(Deletion):在序列A中存在但在序列B中不存在的部分。
      • 更改(Change)或替换(Replace):在两个序列中都存在但内容不同的部分。
  4. 应用差异

    • 根据差异描述,可以将差异应用到一个序列以生成另一个序列,或反之。
    • 在版本控制系统中,这一步骤对应于将本地修改合并到远程分支,或反之。
示例应用:
  • 版本控制:Git等版本控制系统使用Diff算法来计算每次提交之间的差异,并生成.patch文件,用于记录历史变更、合并分支或回滚操作。
  • 文本编辑器:比较文件功能使用Diff算法来高亮显示两个文件之间的差异,帮助用户识别和合并改动。
  • 在线协作平台:如Google Docs等实时协作工具使用Diff算法实时检测和同步多个用户对同一文档的修改。
  • 虚拟DOM:前端框架(如React、Vue)在更新UI时,利用Diff算法比较新旧虚拟DOM树的差异,只对实际发生变化的部分进行最小化的DOM操作,提高UI渲染性能。

总的来说,Diff算法是识别和描述两个数据序列间差异的核心技术,它在众多领域中发挥着重要作用,确保数据变更的准确跟踪、高效传播和可视化展示。

6. vuex

Vuex 是一款专为 Vue.js 应用程序开发的状态管理模式。它提供了一种集中式存储管理应用级状态(数据)的方法,并通过定义明确的规则保证状态以一种可预测的方式发生变化。Vuex 适用于管理中大型单页应用(SPA)中多个组件共享和依赖的状态,有助于解决组件间状态传递的复杂性,提高代码的可维护性和可扩展性。

Vuex 的核心概念和组成部分如下:

核心概念

State(状态):

  • Vuex 的核心是store(仓库),其中包含应用的所有共享状态。这些状态以单一对象的形式存储在 store 的 state 属性中,所有的组件都可以通过 this.$store.state 访问到这些状态。

Getters(获取器):

  • Getters 是从 store 的状态派生出的计算属性。它们用于定义一些复杂的状态计算逻辑,或者对状态进行筛选、格式化等操作。组件可以通过 this.$store.getters 访问 getters,或者使用 mapGetters 辅助函数将其映射为局部计算属性。

Mutations(突变):

  • Mutations 是唯一更改 store 状态的方法。每个 mutation 都是一个包含状态变更逻辑的纯函数,接收 state 作为第一个参数,并在函数体内修改 state。为了确保状态变更的可追踪性,mutation 必须是同步的。触发 mutation 通常通过 store.commit('mutationName', payload) 完成。

Actions(动作):

  • Actions 是处理异步操作或包含多个 mutation 的事务的地方。它们可以包含任意异步操作(如 API 调用、计时器等),并在完成这些操作后通过 commit 方法触发 mutation 更新状态。组件通过 store.dispatch('actionName', payload) 触发 actions。

Modules(模块):

  • 当应用变得庞大时,store 可以通过 modules 进行模块化拆分。每个模块拥有自己的 state、mutations、actions、getters,且可以嵌套。模块间的状态和逻辑得以更好地组织和隔离。
使用场景与优势
  • 统一状态管理:所有组件共享的全局状态集中存放于 store 中,避免了组件间状态传递的混乱和难以维护的问题。
  • 状态变更的可预测性:通过 mutations 规范化状态变更方式,确保所有的状态变更都是通过明确的类型和payload进行的,易于调试、追踪和回滚。
  • 组件的解耦:组件不再直接依赖彼此的状态,而是通过 store 作为中介进行通信。这使得组件更易于复用和测试,且降低了耦合度。
  • 状态的实时响应:借助 Vue.js 的响应式系统,当 store 中的状态发生变化时,依赖这些状态的组件会自动更新视图。
  • 集中式状态监控:Vuex 提供了插件系统和 devtools 插件,可以方便地对 store 的状态变化、mutation 执行和 action 调用进行监控和调试。
使用步骤
  1. 安装:通过 npm 或 yarn 安装 Vuex。

  2. 创建 Store:定义 store 的状态、mutations、actions、getters,并创建 store 实例。

  3. 注入组件:在根 Vue 实例中通过 store 选项注入 store。

  4. 在组件中使用

    • 直接通过 this.$store 访问状态和进行状态变更。
    • 使用 mapStatemapGettersmapMutationsmapActions 辅助函数将 store 的方法映射为局部计算属性或方法。

总结而言,Vuex 作为 Vue.js 的官方状态管理库,通过提供集中式状态存储、严格的mutation操作、actions处理异步逻辑以及模块化组织方式,有效解决了复杂单页应用中状态管理的挑战,提高了代码的组织性和可维护性。

7. vue3的特性

Vue 3 引入了许多重要特性和改进,旨在提升性能、易用性和可维护性。以下是一些关键的 Vue 3 特性:

① 响应式系统重构
  • 基于 Proxy 的响应式:Vue 3 使用 ES6 的 Proxy 对象替代了 Vue 2 中的 Object.defineProperty,实现更高效、更完整的数据对象代理。Proxy 提供了对对象操作的全方位拦截,包括属性访问、修改、添加和删除,使得响应式系统能够更轻松地处理深层嵌套的对象和数组,且无需手动使用 Vue.set 或 Vue.delete
  • 更细粒度的依赖追踪:Vue 3 改进了依赖收集和通知机制,能够更精确地追踪到对象属性级别的依赖,从而实现更精细的组件更新。这有助于减少不必要的组件重渲染,提高性能。
② Composition API
  • 引入 <script setup> :这是一种简化的组件定义语法,允许在单个 <script> 标签内同时定义组件的逻辑和模板使用的变量,无需显式导出。它通过编译时优化,减少运行时开销,提高代码可读性。
  • Composition API(组合式 API) :替代了 Vue 2 的 Options API,提供了一种更灵活、更模块化的代码组织方式。开发者可以使用 setup() 函数集中定义组件的响应式状态、计算属性、watcher、生命周期钩子等,便于逻辑复用和测试。
③ 组件与优化
  • Teleport:允许将组件的 DOM 内容渲染到 DOM 树的其他位置,解决了portal-like(传送门)需求,如将模态框、提示信息等渲染到 body 结尾以避免定位问题或样式污染。
  • Fragments:组件现在可以返回多个顶级元素,无需包裹在一个无意义的父元素中,使模板更简洁、语义更清晰。
  • Suspense:用于异步内容加载的组件,可以优雅地处理加载状态和错误状态,配合 async setup() 提供更好的异步数据加载体验。
  • ** <script setup> 中的 ref 和 reactive 简写**:在 <script setup> 中,可以直接使用 ref 和 reactive 创建响应式变量,无需导入。
④ Tree-shaking & Bundle Size Reduction
  • 更好的 Tree-shaking 支持:通过使用 ES 模块,Vue 3 可以更好地进行 Tree-shaking,仅打包应用实际使用的代码,进一步减小生产环境下的 bundle 大小。
  • 优化的打包工具:Vue 3 对打包工具进行了改进,生成更小、更高效的代码,并移除了一些不常用的 API,减少了库的大小。
⑤ 其他改进与新特性
  • 自定义渲染器:Vue 3 的核心库与平台无关,更容易创建自定义渲染器,如 Weex、服务器端渲染(SSR)等。
  • TypeScript 集成:Vue 3 的源码使用 TypeScript 编写,对 TypeScript 支持更友好,提供了更完善的类型定义,提高了大型项目的可维护性。
  • 更好的错误提示与调试:Vue 3 提供了更详细的错误信息和调试工具支持,如时间旅行调试、组件快照等,增强了开发体验。
  • Composition API 支持插槽:Composition API 现在可以直接操作插槽,提供了 slots 和 slotProps 对象,使得在 setup 函数中使用插槽更加直观。

综上所述,Vue 3 通过一系列重大改进和新特性,提升了性能、简化了开发流程、增强了类型安全性和可维护性,为构建现代 Web 应用提供了更为强大和灵活的工具集。

8. vue2和vue3的区别

Vue 2 和 Vue 3 是 Vue.js 框架的两个主要版本,它们之间存在一些显著的区别。以下是 Vue 2 和 Vue 3 之间的一些关键区别:

① 响应式系统
  • Vue 2:使用 Object.defineProperty 对数据对象进行观测,只能对已有属性进行响应式处理,对新增或删除的属性需要使用 Vue.set 和 Vue.delete 方法。不支持对数组索引的直接响应式处理,需要使用 Vue.set 或数组变异方法(如 pushsplice 等)。
  • Vue 3:基于 ES6 Proxy 实现响应式系统,能够自动跟踪任何层次的属性访问和修改,包括新增、删除和数组索引。无需手动调用特殊方法,响应式能力更为全面且易于使用。
② Composition API(组合式 API)
  • Vue 2:采用 Options API(选项式 API),组件逻辑分散在 datacomputedmethodswatch 等不同选项中,随着组件规模增大,代码组织和复用可能变得困难。
  • Vue 3:引入 Composition API,提供 setup() 函数用于集中定义组件的响应式状态、计算属性、方法、副作用等逻辑。支持使用 refreactivecomputedwatchEffectwatch 等函数创建和管理响应式状态。Composition API 具有更好的代码组织性和复用性,尤其适用于大型和复杂组件。
③ 模板语法与指令
  • Vue 2:使用 v-bindv-ifv-forv-on 等指令。
  • Vue 3:保留了原有的指令,但对部分指令进行了优化。例如,v-model 支持 .value 后缀以适应 Composition API 中的 refv-if 和 v-for 不再在同一元素上竞争,v-bind 简化为 : 或双括号 {{ }}
④ 组件特性
  • Vue 2:组件必须有一个根元素,不能直接返回多个顶级元素。
  • Vue 3:引入 Fragment 功能,允许组件模板不包含一个明确的根元素,而是直接返回多个顶级元素。
⑤ 新特性与优化
  • Vue 2:不支持 <script setup>TeleportSuspense 等新特性。
  • Vue 3:新增 <script setup> 语法糖,简化组件定义;引入 Teleport 用于将组件内容渲染到指定的 DOM 节点;提供 Suspense 组件用于异步内容加载的管理。
⑥ 类型系统与 TypeScript 支持
  • Vue 2:对 TypeScript 的支持有限,需要额外安装和配置类型声明文件。
  • Vue 3:核心库完全使用 TypeScript 编写,自带丰富的类型定义,对 TypeScript 的支持更为原生和深入,开发体验更好。
⑦ 性能与体积
  • Vue 2:通过 Object.defineProperty 实现响应式,性能略逊于 Vue 3,且库体积相对较大。
  • Vue 3:基于 Proxy 的响应式系统性能更优,通过 Tree-shaking 技术优化了打包后的代码体积,整体性能和包大小有所改善。
⑧ 兼容性
  • Vue 2:对老版本浏览器(如 IE11)有较好的兼容性。
  • Vue 3:放弃了对 IE11 的官方支持,更适合现代浏览器环境。若需兼容 IE11,可以使用官方提供的兼容性构建或第三方 polyfill 解决方案。

总结来说,Vue 3 在响应式系统、API 设计、组件功能、性能优化、TypeScript 支持等方面进行了显著升级,提供了更强大、更灵活、更现代的开发体验,但同时也带来了一些向后不兼容的变化。迁移项目时需要考虑这些差异并进行相应的适配。

三、react

1. 虚拟dom

React 虚拟 DOM 是 React 框架中实现高效 UI 更新的核心技术之一。虚拟 DOM 是一种轻量级的内存数据结构,是对真实 DOM 的抽象表示。在 React 中,每当组件状态或属性发生变化时,React 会重新计算组件树并生成新的虚拟 DOM。接下来,通过对新旧虚拟 DOM 的差异进行计算(即使用 Diff 算法),React 只更新实际 DOM 中发生改变的部分,而非整体刷新整个页面。这种策略极大地提高了 UI 更新的效率,降低了与浏览器 DOM 交互的性能开销。以下是 React 虚拟 DOM 的几个关键点:

① 虚拟 DOM 的概念
  • 轻量级对象:虚拟 DOM 是一组 JavaScript 对象,它们描述了真实 DOM 元素及其属性、事件监听器、子节点等信息。这些对象比真实的 DOM 元素更轻量,因为它们不需要涉及浏览器的 DOM API 调用和相关的性能开销。
  • 组件树表示:在 React 应用中,虚拟 DOM 形成了一个与组件层次结构相匹配的树状结构。每个 React 组件实例对应虚拟 DOM 树中的一个节点,其子节点则对应组件的子组件或 JSX 元素。
② 为什么使用虚拟 DOM?
  • 跨平台:虚拟 DOM 提供了一个与具体平台无关的中间层,使得 React 可以在 Web、原生移动应用、桌面应用等多个环境中使用,只需适配底层平台的渲染器。
  • 批量更新:React 可以积累多个状态变更,一次性计算出最终的虚拟 DOM 树差异,然后一次性应用到真实 DOM,避免了频繁与 DOM 交互导致的性能瓶颈。
  • 高效更新:通过 Diff 算法,React 可以精准识别出哪些节点发生了变化,并针对性地更新真实 DOM 中的对应部分,而不是盲目地重新渲染整个组件树。这大大减少了不必要的 DOM 操作,提高了性能。
③ Diff 算法
  • 算法优化:React 的 Diff 算法针对虚拟 DOM 树做了大量优化,使其时间复杂度从 O(n^3) 降低到接近 O(n)。主要优化包括:

    • 树结构比较:React 只会对同层级的节点进行比较,不会尝试跨层级移动节点。
    • 组件 key:为列表项指定稳定的 key 属性可以帮助 React 更准确地识别列表中哪些项发生了变动,避免不必要的节点重排。
    • 元素类型相同假设:当元素类型相同且出现在相同位置时,React 假设它们是同一个节点,仅比较并更新属性。
  • 差异应用:计算出差异后,React 会生成一个最小化更新操作列表(称为“补丁”),这些操作包括创建、更新、删除节点等。这些补丁随后会被应用到真实 DOM 上,引发必要的浏览器重绘或重排。

④ 示例

简单的虚拟 DOM 结构的示例:

var element = {
  element: 'ul',
  props: {
    id: "ulist"
  },
  children: [
    {
      element: 'li',
      props: {
        id: "first"
      },
      children: ['这是第一个List元素']
    },
    {
      element: 'li',
      props: {
        id: "second"
      },
      children: ['这是第二个List元素']
    }
  ]
}

这个对象代表了一个 <ul> 元素,具有 id="ulist" 属性,包含两个 <li> 子元素,每个子元素有自己的 id 属性和文本内容。这就是虚拟 DOM 在内存中的一种表示形式。

总结

React 虚拟 DOM 是一种用于高效渲染 UI 的技术,通过在内存中维护轻量级的 DOM 表示,结合优化的 Diff 算法,React 可以精确识别并应用最小化更新到真实 DOM,从而实现高性能的用户界面更新。这一设计是 React 能够在大规模应用中保持良好性能的关键因素之一。

2. 函数式组件和类组件的区别

React 函数式组件与类组件是两种不同的组件编写方式,它们各自有其特点和适用场景。以下是两者的主要区别:

① 语法与定义方式
  • 函数式组件(Functional Component):使用纯函数的形式定义,接受 props 作为输入参数,返回 React 元素(通常是 JSX)。在 React 16.8 及之后版本,函数式组件可以使用 Hook(如 useStateuseEffect 等)来处理状态和副作用。
function MyFunctionalComponent(props) {
  // 使用 props 和 Hook
  const [count, setCount] = useState(0);

  useEffect(() => {
    // 副作用逻辑
  }, [count]);

  return (
    <div>
      {/* JSX */}
    </div>
  );
}
  • 类组件(Class Component):继承自 React.Component 类,包含 render() 方法(必填)、state 属性和生命周期方法。类组件内部通过 this.state 和 this.setState() 来管理状态,通过定义生命周期方法(如 componentDidMountcomponentDidUpdate 等)处理副作用。
class MyClassComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  componentDidMount() {
    // 组件挂载后的逻辑
  }

  componentDidUpdate(prevProps, prevState) {
    // 组件更新后的逻辑
  }

  render() {
    // 使用 this.props 和 this.state
    return (
      <div>
        {/* JSX */}
      </div>
    );
  }
}
② 状态管理
  • 函数式组件:通过 Hook(如 useStateuseReduceruseContext 等)来管理组件内部状态。状态是通过 Hook 返回的变量直接访问和更新的,状态更新逻辑与组件代码紧密关联,易于理解和维护。
  • 类组件:通过 this.state 和 this.setState() 方法来管理状态。状态和状态更新逻辑分布在类的不同部分,可能需要更多的上下文切换来理解。
③ 生命周期方法
  • 函数式组件:通过 Hook(如 useEffectuseLayoutEffectuseMemouseCallback 等)来替代类组件的生命周期方法。这些 Hook 允许在特定时刻执行副作用操作(如数据获取、订阅、清理等),并提供细粒度的控制,比如指定依赖项来决定何时重新执行副作用。
  • 类组件:具有丰富的生命周期方法,如 constructorcomponentDidMountcomponentDidUpdatecomponentWillUnmount 等,用于处理组件的不同阶段(如初始化、更新、卸载)的逻辑。
④ 可测试性
  • 函数式组件:由于其纯函数性质和 Hook 的明确职责划分,函数式组件通常更易于测试。可以针对独立的 Hook 或者整个组件的功能进行单元测试,而无需关心类实例的生命周期细节。
  • 类组件:测试类组件通常需要模拟生命周期方法的调用、检查实例状态的变化以及确认 setState 的调用。虽然可以测试,但可能需要更复杂的测试工具和设置。
⑤ 代码简洁性与可读性
  • 函数式组件:通常更简洁,没有类的语法开销,逻辑更倾向于自顶向下阅读。Hook 的使用让相关状态和副作用逻辑集中在一起,有助于提高代码可读性。
  • 类组件:对于复杂组件,可能需要在多个方法和生命周期阶段之间跳转来理解完整逻辑,代码组织可能稍显复杂。
⑥ 性能
  • 函数式组件:如果使用恰当的优化策略(如避免不必要的重新渲染),在现代浏览器中,函数式组件与类组件的性能差异很小。React 的 Fiber 架构和 Reconciliation 过程能够有效地处理两种类型的组件。
  • 类组件:同样,在优化得当的情况下,性能差异不大。但由于类组件可能涉及到更多生命周期方法和状态管理的间接操作,如果不注意优化,可能会导致不必要的更新。

总结起来,函数式组件与类组件的主要区别在于语法、状态管理方式、生命周期处理、可测试性、代码风格和潜在性能影响。函数式组件借助 Hook 提供了一种更简洁、易于理解的编写模式,尤其适合现代 React 开发实践。类组件虽然功能完备,但对于许多常见场景来说,其复杂性可能超出所需。随着 React Hook 的普及,函数式组件已成为推荐的编写组件方式,但对于遗留代码或特定场景,类组件仍然有其应用价值。

3. fiber

React Fiber 是 React 16 及更高版本中引入的一个新的协调(Reconciliation)算法,它是 React 能力的核心组成部分,负责管理组件树的更新过程。Fiber 的引入旨在解决以下几个关键问题:

① 更灵活的任务调度

在 React 15 及之前的版本中,React 的更新过程是同步且不可中断的。这意味着一旦开始更新组件树,直到整个过程结束,浏览器都无法处理其他任务(如用户交互、动画帧等)。这可能导致用户界面卡顿,尤其是在处理大量或复杂组件更新时。

React Fiber 引入了基于优先级的任务调度机制,允许更新过程在多个“帧”之间分片执行。它将组件树的遍历和更新分解为一个个可中断的“工作单元”(称为 Fiber 节点),React 可以根据当前系统的可用资源(如 CPU 时间、空闲时间等)灵活调度这些工作单元,确保高优先级的任务(如用户交互响应)得到及时处理,从而提高界面流畅度。

② 更精细的更新控制

Fiber 允许 React 在更新过程中暂停、恢复和重置工作进度。这使得 React 能够更精细地控制组件的更新过程,根据组件的状态、优先级等因素决定是否继续更新、跳过更新或重新开始更新。这种控制能力有助于优化性能,减少不必要的渲染和 DOM 操作。

③ 异步渲染支持

Fiber 为未来的异步渲染功能奠定了基础。通过任务分片和灵活调度,React 可以在浏览器空闲时(如 requestIdleCallback)进行非紧急的更新,进一步提高性能并减少对主线程的阻塞。

④ 更友好的服务器端渲染

Fiber 的设计使得 React 在服务器端渲染时能够更好地利用多核 CPU,通过并行渲染多个子树来加速 SSR 过程。

⑤ 更好的错误处理与恢复

Fiber 可以捕获并记录渲染过程中的错误,并尝试恢复渲染,确保即使在某些组件出错的情况下,其他组件仍能正常渲染和更新。这对于大型应用的健壮性至关重要。

⑥ 未来特性支持

Fiber 为 React 引入了更先进的特性铺平了道路,如 Suspense(用于异步数据加载和呈现)、Concurrent Mode(并发模式,进一步提升应用响应速度和用户体验)等。

工作原理简述

在 React Fiber 中,每个组件对应一个 Fiber 节点,这些节点形成一棵 Fiber 树,与组件树一一对应。Fiber 树的遍历采用深度优先或优先级驱动的方式,根据任务优先级决定下一个要处理的 Fiber 节点。每个 Fiber 节点包含更新相关的信息,如 effect list(用于记录需要执行的副作用操作,如 DOM 更新、清理任务等)、alternate(用于保存之前渲染的状态,便于对比和复用)等。

在更新过程中,React 通过遍历 Fiber 树、比较新旧 Fiber 节点、生成 effect list,最后在适当的时机(如浏览器空闲时)执行 effect list 中的副作用操作,完成实际的 DOM 更新。

总结来说,React Fiber 是 React 协调算法的一次重大革新,它通过引入任务调度、精细化更新控制、异步渲染支持等功能,显著提升了 React 应用的性能、响应速度和用户体验。尽管作为开发者,我们通常无需直接与 Fiber 接触,但理解其设计理念和工作原理有助于我们更好地优化 React 应用。

4. hook

React Hooks 是 React 16.8 版本引入的一项重大新特性,它允许在函数组件中使用状态管理和生命周期等原本只在类组件中可用的功能。Hooks 使得函数组件能够保持简洁的同时,具备复杂逻辑处理的能力,极大地改善了 React 的开发体验。以下是一些关键的 React Hooks 以及它们的作用:

① useState
const [state, setState] = useState(initialState);

useState Hook 用于在函数组件中添加状态(state)。它返回一个状态变量和一个更新状态的函数。每次调用 setState 时,React 将重新渲染该组件。

② useEffect
useEffect(() => {
  // 副作用函数
  return () => {
    // 清理函数
  };
}, [dependencyArray]);

useEffect Hook 用于处理副作用,如数据获取、订阅、手动更改 DOM、设置定时器等。它可以替代类组件中的 componentDidMountcomponentDidUpdate 和 componentWillUnmount 生命周期方法。useEffect 接收一个函数作为参数(副作用函数),该函数将在组件渲染后执行。可选的依赖数组(dependencyArray)决定了何时重新执行副作用函数。返回一个清理函数,当组件卸载或依赖数组变化时,清理函数会被调用来清除副作用。

③ useContext
const value = useContext(MyContext);

useContext Hook 用于消费 React Context,无需手动传递 Consumer 或 Provider。它接收一个 Context 对象并返回当前上下文的值。使用此 Hook 的组件会订阅该上下文的变化,当上下文值改变时,组件将重新渲染。

④ useReducer
const [state, dispatch] = useReducer(reducer, initialState);

useReducer Hook 用于管理复杂状态。它接收一个.reducer 函数(类似于 Redux 中的 reducer)和初始状态,返回当前状态以及一个 dispatch 函数,用于触发状态更新。相比于 useStateuseReducer 更适用于处理涉及多个相关状态或复杂逻辑的状态更新。

⑤ useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo Hook 用于缓存计算结果,避免在每次渲染时都进行昂贵的计算。它接收一个创建函数和一个依赖数组。只有当依赖数组中的值发生变化时,才会重新计算并返回新的结果。否则,返回先前缓存的结果。

⑥ useCallback
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);

useCallback Hook 类似于 useMemo,但它用于缓存函数。当依赖数组中的值发生变化时,返回一个新的函数。否则,返回先前缓存的函数引用。这对于避免在每次渲染时创建新的函数(可能导致子组件不必要的重新渲染)非常有用,特别是在传递回调给优化过的子组件(如 React.memo 包裹的组件)时。

⑦ useRef
const ref = useRef(initialValue);

useRef Hook 返回一个可变的引用对象,其 .current 属性被初始化为传入的 initialValueuseRef 对象在整个组件生命周期内保持不变,常用于存储 DOM 元素引用、滚动位置、计数器等需要跨渲染周期保持的值。

⑧ useImperativeHandle
useImperativeHandle(ref, createHandle, [deps]);

useImperativeHandle Hook 用于定制某个 ref 的暴露行为,使父组件可以通过 ref 访问到子组件暴露的特定方法或属性。通常与 forwardRef 配合使用,用于在函数组件中暴露子组件的实例方法给父组件调用。

⑨ useLayoutEffect
useLayoutEffect(() => {
  // 副作用函数
  return () => {
    // 清理函数
  };
}, [dependencyArray]);

useLayoutEffect 与 useEffect 类似,区别在于 useLayoutEffect 中的副作用函数会在所有 DOM 变更完成后同步执行,然后浏览器才会绘制。这适用于那些需要在浏览器布局或绘制之前完成的副作用,如调整 DOM 元素尺寸、读取布局信息等。不正确的使用可能会导致视觉闪烁或性能问题,因此应谨慎使用。

⑩ useDebugValue
useDebugValue(value);

useDebugValue Hook 用于在 React DevTools 中显示自定义 Hook 的调试标签。这对于自定义 Hook 的作者很有用,可以在 DevTools 中提供有关 Hook 状态的额外信息,帮助开发者理解其行为。

总结来说,React Hooks 提供了一种在函数组件中管理状态、处理副作用、使用上下文、优化性能的新方式,极大地提升了代码的可复用性和可维护性。通过合理使用这些内置 Hooks,开发者可以避免编写复杂的高阶组件或 Render Props,使得函数组件的逻辑更清晰、更易于理解。