Vue3响应式原理

238 阅读4分钟

Vue3响应式原理

1. 你需要先了解的

1.1. Set

集合对象。

Set中的元素只会出现一次,即Set中的元素是唯一的。

1.2. Map

Map对象。

一组键值对结构。能够快速通过键查询值。

1.3. WeakMap

WeakMap对象。

同Map一样也是一组键值对的结构,但它的键必须是对象(Object类型),而值可以是任意的。

1.4. Proxy

一个ES6的新特性。

Proxy是一个对象,它封装了另一个对象或函数,并允许我们拦截对其的访问。

Proxy构造器接受2个参数:

  • target
    • 需要被代理的对象
  • handler
    • 处理器对象,包含所有trap(用于拦截不同操作)

实例:

let product = { price: 5, quantity: 2 };

let proxiedProduct = new Proxy(product, {
  get(target, key) {
    console.log('通过代理访问');
    return target[key];
  },
  set(target, key, value) {
    console.log('通过代理修改');
    target[key] = value;

    return true;
  }
});

proxiedProduct.quantity = 4;
console.log(proxiedProduct.quantity);
// result
// 通过代理修改
// 通过代理访问
// 4

所以当一个被Proxy封装的对象被访问或被修改时,可以自动触发一些操作。

1.5. Reflect

Reflect是一个对象,提供了多种与Proxy trap签名相同的方法。

使用Reflect是为了确保trap的执行结果与默认行为保持一致,而避免因为手动操作产生的任何副作用。

用Reflect改写一下上面的例子:

let product = { price: 5, quantity: 2 };

let proxiedProduct = new Proxy(product, {
  get(target, key, receiver) {
    console.log('通过代理访问');
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    console.log('通过代理修改');
    return Reflect.set(target, key, value, receiver);
  }
});

proxiedProduct.quantity = 4;
console.log(proxiedProduct.quantity);
// 通过代理修改
// 通过代理访问
// 4

2. 响应式原理

2.1. 实现响应式需要解决的问题

  1. 一个响应式的属性被修改时,需要执行一个或多个回调,这个回调需要被记录在某个地方以备Vue调用
  2. 一个对象可能包含多个响应式的属性,它们所对应的回调需要被记录在某个地方以备Vue调用
  3. 可能存在多个对象包含多个响应式的属性,它们所对应的回调需要被记录在某个地方以备Vue调用
  4. 当一个响应式属性被访问时,记录其所在的回调
  5. 当一个响应式的属性被修改时,调用其所在的回调

2.2. 收集依赖

这里的依赖就是指当一个响应式的属性被修改后,需要执行的回调,在Vue3中被称为effect。

// 为了解决问题3,我们利用WeakMap数据结构,记录不同对象的依赖,可以直接将对象作为键来查询
const targetMap = new WeakMap()
 
// track函数用于记录依赖
function track(target, key) {
  // 首先查询目标对象是否已经被记录过依赖
  let depsMap = targetMap.get(target)
 
  if (!depsMap) {
    // 如果目标对象没有被记录过依赖,则在WeakMap中新建一个键值对,目标对象为键,值为一个空Map对象
    // 为了解决问题2,我们利用Map数据结构,记录对象中不同属性的依赖,通过属性名来查询
    targetMap.set(target, (depsMap = new Map()))
  }
 
  // 根据属性名在Map中查询对应的依赖
  let dep = depsMap.get(key)
  if (!dep) {
    // 如果当前属性没有被记录过依赖,则在Map中新建一个键值对,目标属性为键,值为一个空Set对象
    // 为了解决问题1,我们利用Set数据结构,记录对象属性所对应的所有依赖,不需要单独查询
    depsMap.set(key, (dep = new Set()))
  }
 
  // 将依赖添加进目标属性对应的Set对象
  // 这里的effect是一个全局变量,它的值就是目标属性的依赖
  dep.add(effect)
}

2.4. 自动化收集/执行依赖

现在有了收集和执行依赖的能力,那么我们需要在访问对象属性时收集依赖,修改对象属性时执行依赖。

function trigger(target, key) {
  // 首先根据目标对象查询
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    return
  }
 
  // 再根据目标属性查询
  let dep = depsMap.get(key)
  if (dep) {
    // 查询到对应的依赖后,遍历Set中的依赖并执行
    dep.forEach(effect => {
      effect()
    })
  }
}

2.5. 实现响应式

let product = reactive({ price: 5, quantity: 2 });
let total = 0;
let effect = () => {
    // product.price和product.quantity被访问后,当前回调会被记录
    // 当product.price或product.quantity被修改后,当前回调会被执行
    total = product.price * product.quantity;
};
 
 
// 首次执行effect用于收集依赖
effect();
console.log(total); // 10
 
 
// 修改响应式属性,将会执行其对应的依赖,从而更新total
product.quantity = 3;
console.log(total); // 15