Vue的核心作用

  • 声明式渲染:Vue 基于标准 HTML 拓展了一套模板语法,使得我们可以声明式地描述最终输出的 HTML 和 JavaScript 状态之间的关系。 - 命令式 :告诉程序"如何做",需要手动操作DOM - 声明式 :告诉程序"要什么结果",由框架负责DOM操作
  • 响应性:Vue 会自动跟踪 JavaScript 状态并在其发生变化时响应式地更新 DOM。

基于 Proxy 的响应式代理

在 Vue 3 中,响应式系统的核心是使用 ES6 的 Proxy 来替代 Vue 2 里的 Object.defineProperty 方法,以此实现更加全面和强大的响应式追踪功能。下面我们来详细剖析这个过程。

响应式代码实现

  • Reflect.set(target, key, value, receiver):使用 Reflect.set 方法设置目标对象的属性值。
  • trigger(target, key):调用 trigger 函数触发更新。当属性值发生变化时,需要通知所有依赖该属性的副作用函数重新执行。

相比 Object.defineProperty 的优势

  • 支持数组索引修改和 length 变化检测:在 Vue 2 中,使用 Object.defineProperty 来实现响应式时,对于数组的一些操作(如通过索引修改元素、修改 length 属性)无法直接检测到变化。而在 Vue 3 中,使用 Proxy 可以轻松地拦截这些操作,从而实现对数组的全面响应式追踪。
  • 自动追踪新增对象属性(无需 Vue.set):在 Vue 2 中,如果要给响应式对象新增一个属性,需要使用 Vue.set 方法才能让新增的属性也具有响应式特性。而在 Vue 3 中,由于使用了 Proxy,可以自动追踪对象新增的属性,无需额外的操作。
  • 深度监听嵌套对象(Lazy 模式,按需激活):Vue 3 的响应式系统会对嵌套对象进行深度监听,但采用的是 Lazy 模式,即只有在访问嵌套对象的属性时才会激活对该嵌套对象的响应式追踪,这样可以提高性能。

依赖管理机制

Vue 3 的响应式系统采用了三层依赖管理体系,通过 WeakMap、Map 和 Set 来高效地管理依赖关系。

三层依赖管理体系结构

  • TargetMap:是一个 WeakMap,用于存储目标对象到键的映射。WeakMap 的键必须是对象,并且这些对象是弱引用,即如果对象没有其他引用指向它,它可以被垃圾回收,这样可以避免内存泄漏。
  • DepsMap:是一个 Map,用于存储键到依赖集合的映射。每个键对应一个依赖集合,该集合存储了所有依赖于该键的副作用函数。
  • Dep:是一个 Set,用于存储具体的副作用函数。Set 是一种无序且唯一的数据结构,确保每个副作用函数只被存储一次。

依赖管理的代码实现

代码详细解释

  • 类型定义:
  • Dep:定义为 Set,表示一个存储副作用函数的集合。
  • KeyToDepMap:定义为 Map,表示一个存储键到依赖集合的映射。

2. targetMap:是一个全局的 WeakMap,用于存储所有目标对象的依赖信息。

3. track 函数:用于进行依赖收集。

  • if (!activeEffect) return;:如果当前没有活跃的副作用函数,则直接返回,不进行依赖收集。
  • let depsMap = targetMap.get(target);:从 targetMap 中获取目标对象对应的 DepsMap。
  • if (!depsMap) { targetMap.set(target, (depsMap = new Map())); }:如果 DepsMap 不存在,则创建一个新的 Map 并存储到 targetMap 中。
  • let dep = depsMap.get(key);:从 DepsMap 中获取键对应的依赖集合。
  • if (!dep) { depsMap.set(key, (dep = new Set())); }:如果依赖集合不存在,则创建一个新的 Set 并存储到 DepsMap 中。
  • dep.add(activeEffect);:将当前活跃的副作用函数添加到依赖集合中。

副作用调度

Vue 3 的响应式系统基于调度器实现了异步更新队列,以提高性能和避免不必要的重复更新。

异步更新队列的实现代码

代码详细解释

  • queue:是一个 Set,用于存储需要执行的副作用函数。使用 Set 可以确保每个副作用函数只被存储一次,避免重复执行。
  • isFlushing:是一个布尔值,用于标记当前是否正在执行更新队列中的副作用函数。
  • queueJob 函数:用于将副作用函数添加到更新队列中。
  • isFlushing = true;:标记为正在执行更新。
  • Promise.resolve().then(() => { ... }):使用 Promise 实现异步执行。在微任务队列中执行更新队列中的副作用函数。
  • try { queue.forEach(job => job()); } finally { queue.clear(); isFlushing = false; }:遍历更新队列,执行每个副作用函数。执行完毕后,清空队列并将 isFlushing 标记为 false,表示更新完成。
  • queue.add(job):将副作用函数添加到 queue 中。
  • if (!isFlushing) { ... }:如果当前没有正在执行更新队列中的副作用函数,则启动异步更新。

通过这种异步更新队列的方式,Vue 3 可以将多次属性变化引起的副作用函数执行合并为一次,从而提高性能。例如,在短时间内多次修改响应式对象的属性,只会触发一次副作用函数的执行。

综上所述,Vue 3 的响应式系统通过基于 Proxy 的响应式代理、三层依赖管理机制和副作用调度,实现了高效、全面的响应式追踪和更新功能。

coder why 老师讲解笔记(2025-07-18之前)

什么是响应式

下响应式意味着什么?先看一段代码:

  • m有一个初始化的值,有一段代码使用了这个值;
  • 那么在m有一个新的值时,这段代码可以自动重新执行;

let m = 20;

console.log(m); console.log(m * 2); console.log(m ** 2); m = 40;依赖收集这样一种可以自动响应数据变量的代码机制,我们就称之为是响应式的。同理,当一个对象的属性发生改变时自动执行的相应操作是对象的响应式。 上述代码中当执行的操作过多时不便于管理,可以把要执行的代码放到函数中。

const obj = {
  name: "sun",
  age: 20,
};
// obj变化时的响应函数
function foo() {
  console.log(obj.name);
  console.log(obj.age);
}这样做,当开发中需要有多个不一定时需要响应的函数时不便于区分哪个需要响应,哪个不需要响应,所有就可以定义一个数组将这些需要响应的函数收集起来。

代码实现

  • 问题一:如果函数中有用到两次key,比如name,那么这个函数会被收集两次;

问题:我们并不希望将添加reactiveFn放到get中,以为它是属于Dep的行为;

所以我们需要对Depend类进行重构:

  • 解决问题一的方法:不使用数组,而是使用Set;
  • 解决问题二的方法:添加一个新的方法,用于收集依赖;