响应式实现原理
Published on Feb 24, 2023, with 26 view(s) and 0 comment(s)
Ai 摘要:Vue 3的响应式系统基于Proxy实现,相比Vue 2的Object.defineProperty具有更全面的数据追踪能力。其核心机制包括:通过Proxy拦截对象操作,在get时收集依赖(track),set时触发更新(trigger);采用WeakMap、Map和Set构建三层依赖管理体系,高效管理对象-属性-副作用函数的映射关系;通过异步更新队列优化性能,合并多次变更触发单次更新。系统还实现了自动追踪新增属性、深度监听嵌套对象等特性,整体架构更高效灵活。

Vue原理梳理.png

Vue的核心作用

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

基于 Proxy 的响应式代理

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

响应式代码实现

const reactive = (target) => {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key) // 依赖收集
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      Reflect.set(target, key, value, receiver)
      trigger(target, key) // 触发更新
      return true
    }
  })
}

 

代码详细解释

  1. reactive 函数:该函数接收一个目标对象 target 作为参数,返回一个使用 Proxy 代理后的新对象。Proxy 是 ES6 提供的一个强大特性,它可以拦截并重新定义对象的基本操作,比如属性的读取和设置。
  2. get 拦截器:当访问代理对象的属性时,会触发 get 拦截器。
  • track(target, key):调用 track 函数进行依赖收集。依赖收集的目的是记录哪些副作用函数依赖于当前访问的属性,以便在属性值发生变化时能够通知这些副作用函数进行更新。
  • Reflect.get(target, key, receiver):使用 Reflect.get 方法获取目标对象的属性值。Reflect 是 ES6 新增的一个内置对象,它提供了一系列用于操作对象的方法,与 Proxy 配合使用可以更方便地实现对象的拦截操作。

    3. set 拦截器:当设置代理对象的属性时,会触发 set 拦截器。

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

相比 Object.defineProperty 的优势

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

 

依赖管理机制

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

三层依赖管理体系结构
 

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

依赖管理的代码实现

type Dep = Set<ReactiveEffect>; type KeyToDepMap = Map<any, Dep>; const targetMap = new WeakMap<any, KeyToDepMap>(); function track(target: object, key: unknown) {   if (!activeEffect) return;   let depsMap = targetMap.get(target);   if (!depsMap) {     targetMap.set(target, (depsMap = new Map()));   }   let dep = depsMap.get(key);   if (!dep) {     depsMap.set(key, (dep = new Set()));   }   dep.add(activeEffect); }

 

代码详细解释

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

    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 的响应式系统基于调度器实现了异步更新队列,以提高性能和避免不必要的重复更新。

异步更新队列的实现代码

const queue = new Set(); let isFlushing = false; function queueJob(job) {   queue.add(job);   if (!isFlushing) {     isFlushing = true;     Promise.resolve().then(() => {       try {         queue.forEach(job => job());       } finally {         queue.clear();         isFlushing = false;       }     });   } }

代码详细解释

  1. queue:是一个 Set,用于存储需要执行的副作用函数。使用 Set 可以确保每个副作用函数只被存储一次,避免重复执行。
  2. isFlushing:是一个布尔值,用于标记当前是否正在执行更新队列中的副作用函数。
  3. 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);
}

这样做,当开发中需要有多个不一定时需要响应的函数时不便于区分哪个需要响应,哪个不需要响应,所有就可以定义一个数组将这些需要响应的函数收集起来。

const obj = {
  name: "sun",
};
// obj变化时的响应
function foo1() {
  console.log("执行foo1");
}
function foo2() {
  console.log("执行foo2");
}
// 定义一个数组收集一个对象的依赖
const reactiveFns = [];
// 将依赖函数传入数组
function watchFn(fn) {
  reactiveFns.push(fn);
}
watchFn(foo1);
watchFn(foo2);

// 先手动执行依赖
obj.name = "lebron";
reactiveFns.forEach((item) => {
  item();
});
//执行foo1
//执行foo2

但是当需要监测的对象比较多时,不可能一个对象定义一个数组来维护相关依赖。所以可以定义一个类,通过这个类来管理某一对象的某个属性的所有响应式函数。

const obj = {
  name: "sun",
  age: 20,
};
// obj变化时的响应
function foo1() {
  console.log("执行foo1");
}
function foo2() {
  console.log("执行foo2");
}

class Depend {
  constructor() {
    this.reactiveFns = [];
  }
  addDepend(fn) {
    this.reactiveFns.push(fn);
  }
  notify() {
    this.reactiveFns.forEach((item) => {
      item();
    });
  }
}
const depend = new Depend();
function watchFn(fn) {
  depend.addDepend(fn);
}
watchFn(foo1);
watchFn(foo2);
// 先手动执行依赖
depend.notify();
// 执行foo1
// 执行foo2

监听对象属性变化

const objProxy = new Proxy(obj, {
  get(target, key, receiver) {
    console.log("检测到获取属性");
    depend.notify();
    return Reflect.get(target, key, receiver);
  },
  set(target, key, newValue, receiver) {
    console.log("检测到设置属性");
    depend.notify();
    return Reflect.set(target, key, newValue, receiver);
  },
});

依赖收集的管理上述代码中将监听到的name属性和age属性的依赖放入了一个对象当中,当监听到对象的变化时,无论时name还是age变化都将执行所有的依赖,我们如何可以使用一种数据结构来管理不同对象的不同依赖关系呢?也就是使一个属性对应一个depend对象。使用weakMap。

Description

代码实现

const targetMap = new WeakMap();
const getDepend = function (obj, key) {
  // 根据对象获取对应的map对象
  const objMap = targetMap.get(obj);
  if (!objMap) {
    objMap = new Map();
    targetMap.set(obj, objMap);
  }

  // 根据key获取对应的depend对象
  const depend = objMap.get(key);
  if (!depend) {
    depend = new Depend();
    objMap.set(key, depend);
  }

  return depend;
};

// 检测对象的变化
const objProxy = new Proxy(obj, {
  get(target, key, receiver) {
    console.log("检测到获取属性");
    const dep = getDepend(target, key);
    dep.notify();
    return Reflect.get(target, key, receiver);
  },
  set(target, key, newValue, receiver) {
    console.log("检测到设置属性");
    depend.notify();
    return Reflect.set(target, key, newValue, receiver);
  },
});

正确收集依赖之前收集依赖的地方是在 watchFn 中:但是这种收集依赖的方式根本不知道是哪一个key的哪一个depend需要收集依赖;只能针对一个单独的depend对象来添加你的依赖对象;那么正确的应该是在哪里收集呢?应该在我们调用了Proxy的get捕获器时,因为如果一个函数中使用了某个对象的key,那么它应该被收集依赖;

const obj = {
  name: "sun",
  age: 20,
};

class Depend {
  constructor() {
    this.reactiveFns = [];
  }
  addDepend(fn) {
    this.reactiveFns.push(fn);
  }
  notify() {
    this.reactiveFns.forEach((item) => {
      item();
    });
  }
}

let reactiveFn = null;

function watchFn(fn) {
  reactiveFn = fn;
  fn();
  reactiveFn = null;
}

const targetMap = new WeakMap();
function getDepend(obj, key) {
  // 根据对象获取对应的map对象
  let objMap = targetMap.get(obj);
  if (!objMap) {
    objMap = new Map();
    targetMap.set(obj, objMap);
  }

  // 根据key获取对应的depend对象
  let depend = objMap.get(key);
  if (!depend) {
    depend = new Depend();
    objMap.set(key, depend);
  }
  return depend;
}

// 检测对象的变化
const objProxy = new Proxy(obj, {
  get(target, key, receiver) {
    const dep = getDepend(target, key);
    dep.addDepend(reactiveFn);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver);
    const dep = getDepend(target, key);
    dep.notify();
  },
});

watchFn(function () {
  console.log("执行了obj.name响应");
  console.log(objProxy.name);
});

objProxy.name = "lebron";
// 执行了obj.name响应
// sun
// 执行了obj.name响应
// lebron

对Depend重构这里有两个问题:

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

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

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

  • 解决问题一的方法:不使用数组,而是使用Set;
  • 解决问题二的方法:添加一个新的方法,用于收集依赖;
class Depend {
  constructor() {
    this.reactiveFns = new Set();
  }
  addDepend(fn) {
    this.reactiveFns.push(fn);
  }
  notify() {
    this.reactiveFns.forEach((item) => {
      item();
    });
  }
  depend() {
    if (reactiveFn) {
      this.reactiveFns.add(reactiveFn);
    }
  }
}

let reactiveFn = null;

function watchFn(fn) {
  reactiveFn = fn;
  fn();
  reactiveFn = null;
}

const targetMap = new WeakMap();
function getDepend(obj, key) {
  // 根据对象获取对应的map对象
  let objMap = targetMap.get(obj);
  if (!objMap) {
    objMap = new Map();
    targetMap.set(obj, objMap);
  }

  // 根据key获取对应的depend对象
  let depend = objMap.get(key);
  if (!depend) {
    depend = new Depend();
    objMap.set(key, depend);
  }
  return depend;
}

let reactive = function (obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      const dep = getDepend(target, key);
      dep.depend();
      return Reflect.get(target, key, receiver);
    },
    set(target, key, newValue, receiver) {
      Reflect.set(target, key, newValue, receiver);
      const dep = getDepend(target, key);
      dep.notify();
    },
  });
};
let objProxy = reactive({
  name: "sun",
  age: 20,
});
let obj1Proxy = reactive({
  height: 190,
});
watchFn(function () {
  console.log("执行了obj.name响应");
  console.log(objProxy.name);
});
watchFn(function () {
  console.log(objProxy.age, "fn----------");
});
watchFn(function () {
  console.log(obj1Proxy.height, "obj1加入响应式");
});
objProxy.name = "lebron";
objProxy.age = 37;
obj1Proxy.height = 191;
// 执行了obj.name响应
// sun
// 20 fn----------
// 190 obj1加入响应式
// 执行了obj.name响应
// lebron
// 37 fn----------
// 191 obj1加入响应式

这也是vue3实现响应式的原理,vue2实现响应式的原理基本与之相同,不同的是vue2使用的Object.defineProperty来监听对象属性的变化。

function reactive(obj) {
  Object.keys(obj).forEach(key => {
    let value = obj[key]
    Object.defineProperty(obj, key, {
      get: function() {
        const depend = getDepend(obj, key)
        depend.depend()
        return value
      },
      set: function(newValue) {
        value = newValue
        const depend = getDepend(obj, key)
        depend.notify()
      }
    })
  })
  return obj
}