二、Proxy 和 Reflect

在上一节内容中,介绍了如何在数据发生变化后,自动更新数据,但存在的问题是,每次需要手动通过触发 track() 函数搜集依赖,通过 trigger() 函数执行所有副作用,达到数据更新目的。

这一节将来解决这个问题,实现这两个函数自动调用。

1. 如何实现自动操作

这里我们引入 JS 对象访问器的概念,解决办法如下:

  • 在读取(GET 操作)数据时,自动执行 track() 函数自动收集依赖;
  • 在修改(SET 操作)数据时,自动执行 trigger() 函数执行所有副作用;

那么如何拦截 GET 和 SET 操作?接下来看看 Vue2 和 Vue3 是如何实现的:

需要注意的是:Vue3 使用的 Proxy 和 Reflect API 并不支持 IE。

 Object.defineProperty() 函数这边就不多做介绍,可以阅读文档,下文将主要介绍  Proxy 和  Reflect API。

2. 如何使用 Reflect

通常我们有三种方法读取一个对象的属性:

  1. 使用 . 操作符:leo.name ;
  2. 使用 [] : leo['name'] ;
  3. 使用 Reflect API: Reflect.get(leo, 'name') 。

这三种方式输出结果相同。

3. 如何使用 Proxy

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。语法如下:

const p = new Proxy(target, handler)
  • 1.

参数如下:

  • target : 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
  • handler : 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

我们通过官方文档,体验一下  Proxy API

let product = { price: 10, quantity: 2 };
let proxiedProduct = new Proxy(product, {
    get(target, key){
      console.log('正在读取的数据:',key);
    return target[key];
  }
})
console.log(proxiedProduct.price); 
// 正在读取的数据: price
// 10
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

这样就保证我们每次在读取 proxiedProduct.price 都会执行到其中代理的 get 处理函数。其过程如下:

vue3的响应式原理(二)_API

然后结合 Reflect 使用,只需修改 get 函数:

get(target, key, receiver){
    console.log('正在读取的数据:',key);
    return Reflect.get(target, key, receiver);
  }
  • 1.
  • 2.
  • 3.
  • 4.

输出结果还是一样。

接下来增加 set 函数,来拦截对象的修改操作:

let product = { price: 10, quantity: 2 };
let proxiedProduct = new Proxy(product, {
  get(target, key, receiver){
    console.log('正在读取的数据:',key);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver){
    console.log('正在修改的数据:', key, ',值为:', value);
    return Reflect.set(target, key, value, receiver);
  }
})
proxiedProduct.price = 20;
console.log(proxiedProduct.price); 
// 正在修改的数据: price ,值为: 20
// 正在读取的数据: price
// 20
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

这样便完成 get 和 set 函数来拦截对象的读取和修改的操作。为了方便对比 Vue 3 源码,我们将上面代码抽象一层,使它看起来更像 Vue3 源码:

function reactive(target){
  const handler = {  // ① 封装统一处理函数对象
    get(target, key, receiver){
      console.log('正在读取的数据:',key);
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver){
      console.log('正在修改的数据:', key, ',值为:', value);
      return Reflect.set(target, key, value, receiver);
    }
  }
  
  return new Proxy(target, handler); // ② 统一调用 Proxy API
}

let product = reactive({price: 10, quantity: 2}); // ③ 将对象转换为响应式对象
product.price = 20;
console.log(product.price); 
// 正在修改的数据: price ,值为: 20
// 正在读取的数据: price
// 20
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

这样输出结果仍然不变。

4. 修改 track 和 trigger 函数

通过上面代码,我们已经实现一个简单 reactive() 函数,用来将普通对象转换为响应式对象。但是还缺少自动执行 track() 函数和 trigger() 函数,接下来修改上面代码:

const targetMap = new WeakMap();
let total = 0;
const effect = () => { total = product.price * product.quantity };
const track = (target, key) => { 
  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(effect);
}

const trigger = (target, key) => {
  const depsMap = targetMap.get(target);
  if(!depsMap) return;
    let dep = depsMap.get(key);
  if(dep) {
    dep.forEach( effect => effect() );
  }
};

const reactive = (target) => {
  const handler = {
    get(target, key, receiver){
      console.log('正在读取的数据:',key);
      const result = Reflect.get(target, key, receiver);
      track(target, key);  // 自动调用 track 方法收集依赖
      return result;
    },
    set(target, key, value, receiver){
      console.log('正在修改的数据:', key, ',值为:', value);
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      if(oldValue != result){
         trigger(target, key);  // 自动调用 trigger 方法执行依赖
      }
      return result;
    }
  }
  
  return new Proxy(target, handler);
}

let product = reactive({price: 10, quantity: 2}); 
effect();
console.log(total); 
product.price = 20;
console.log(total); 
// 正在读取的数据: price
// 正在读取的数据: quantity
// 20
// 正在修改的数据: price ,值为: 20
// 正在读取的数据: price
// 正在读取的数据: quantity
// 40
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.

vue3的响应式原理(二)_API_02