最近開了一個讀者回饋表單郵箱,無論是對文章的感想或是對部落格的感想,有什麼想回饋的都可以發郵箱跟我說:i_kkkp@163.com

vue-Proxy和Reflect

引言

既然 Vue.js 3 的响应式数据是基于 Proxy 实现的,那么我们就有必要了解 Proxy 以及与之相关联的 Reflect。什么是 Proxy 呢?简单地说,使用 Proxy 可以创建一个代理对象。它能够实现对其他对象的代理,这里的关键词是其他对象,也就是说,Proxy 只能代理对象,无法代理非对象值,例如字符串、布尔值等。那么,代理指的是什么呢?所谓代理,指的是对一个对象基本语义的代理。它允许我们拦截并重新定义对一个对象的基本操作。

创建对象代理 Proxy

内置的对象 Reflect

当我们讨论编程语言中的”基本语义”时,我们指的是对数据进行读取和修改的最基本操作。在JavaScript中,这些操作通常包括读取属性值和设置属性值。例如,在给定一个对象obj的情况下,以下操作被认为是基本语义的操作:

  1. 读取属性值:obj.foo (读取属性foo的值)
  2. 设置属性值:obj.foo = newValue (设置属性foo的值)

在上述代码中,Proxy对象允许我们拦截(或者说重定义)这些基本语义的操作。Proxy的构造函数接受两个参数:被代理的对象和一个包含拦截器(也称为夹子或陷阱)的对象。在拦截器对象中,我们可以定义get方法来拦截读取属性操作,定义set方法来拦截设置属性操作。这样,我们就可以在这些操作发生时执行自定义的逻辑。

关于Reflect对象,它是JavaScript的一个全局对象,提供了与Proxy拦截器方法一一对应的方法。这些Reflect方法提供了默认的操作行为。例如,Reflect.get(target, key)方法提供了访问对象属性的默认行为,与直接使用target[key]是等价的。同时,Reflect方法还可以接受第三个参数,用来指定函数调用时的this值。

理解这些基本语义操作以及如何使用ProxyReflect来拦截和处理这些操作,是理解JavaScript中响应式数据(Reactive Data)实现的关键。在响应式数据中,我们可以利用ProxyReflect来追踪对象属性的读取和修改,从而实现数据的响应式更新。

Proxy 的基本用法

当我们谈论基本语义时,我们指的是 JavaScript 中的一些基本操作,比如读取对象属性值和设置对象属性值。考虑下面的对象 obj

const obj = { foo: 1 };

在这里,obj.foo 是一个读取属性的基本语义操作,obj.foo = newValue 是一个设置属性的基本语义操作。

现在,我们可以使用 Proxy 来拦截这些基本语义的操作。

const handler = {
  get(target, key) {
    console.log(`读取属性 ${key}`);
    return target[key];
  },
  set(target, key, value) {
    console.log(`设置属性 ${key}${value}`);
    target[key] = value;
  }
};

const proxyObj = new Proxy(obj, handler);

proxyObj.foo; // 输出:读取属性 foo
proxyObj.foo = 2; // 输出:设置属性 foo 为 2

在上面的代码中,我们创建了一个 handler 对象,其中定义了 getset 方法,用来拦截属性的读取和设置。然后,我们使用 Proxy 构造函数创建了一个代理对象 proxyObj,它会拦截 obj 对象上的读取和设置操作。当我们访问 proxyObj.foo 时,会触发 get 方法,输出相应的信息。当我们设置 proxyObj.foo 的值时,会触发 set 方法,同样输出相应的信息。

这样,Proxy 允许我们在基本语义操作发生时执行自定义的逻辑,而不需要直接操作原始对象。在实际应用中,这种能力可以用来实现响应式数据、数据验证、日志记录等功能。

当我们使用 Proxy 拦截对象属性的读取操作时,我们需要特别注意访问器属性(accessor properties)的情况,因为访问器属性使用 getter 函数来定义,而这个函数内部的 this 关键字会根据调用方式而变化。

Reflect 在响应式中的用法

在拦截器函数中,我们希望建立副作用函数与响应式数据之间的关联,确保当属性被访问时,能够正确地进行依赖收集,以便在属性发生变化时触发副作用函数的重新执行。然而,如果我们直接使用 target[key] 来获取属性值,那么访问器属性内部的 this 关键字将指向原始对象,而不是代理对象,这会导致无法正确建立响应关系。

解决这个问题的方法是使用 Reflect.get(target, key, receiver) 来代替 target[key]。这样,Reflect.get 的第三个参数 receiver 就能正确地指向代理对象,而不是原始对象。这样一来,在访问器属性的 getter 函数内部,this 关键字就会指向代理对象,从而建立了正确的响应关系。

以下是使用 Reflect.get 的修正代码:

const handler = {
  get(target, key, receiver) {
    track(target, key); // 响应式数据依赖收集
    return Reflect.get(target, key, receiver); // 使用 Reflect.get 获取属性值
  },
  // 其他拦截器方法...
};

const proxyObj = new Proxy(obj, handler);

effect(() => {
  console.log(proxyObj.bar); // 在副作用函数内部访问 bar 属性
});

proxyObj.foo++; // 触发副作用函数的重新执行

我们可以再看一个简单一点的示例:

当使用代理对象时,receiver参数主要用于确保在代理的拦截函数内部,this指向代理对象,从而建立响应式联系。下面我将对比使用receiver参数和不使用的情况,以便更清楚地理解它的作用。

1. 使用receiver参数的情况:

const data = {
  foo: 1
};

const proxy = new Proxy(data, {
  get(target, key, receiver) {
    // 使用 Reflect.get 保证 this 指向代理对象
    const result = Reflect.get(target, key, receiver);
    // 在实际应用中,你可能还需要进行其他处理,例如触发更新操作等
    console.log(`Accessed ${key} property with value ${result}`);
    return result;
  }
});

console.log(proxy.foo); // 输出: Accessed foo property with value 1

在这个例子中,我们使用了receiver参数传递给Reflect.get,确保在get拦截函数内部,this指向代理对象proxy。当你访问proxy.foo时,get拦截函数被触发,并且this指向proxy对象。

2. 不使用receiver参数的情况:

const data = {
  foo: 1
};

const proxy = new Proxy(data, {
  get(target, key) {
    // 不使用 receiver 参数,this 指向原始对象 data

    
    const result = target[key];
    // 在实际应用中,你可能还需要进行其他处理,例如触发更新操作等
    console.log(`Accessed ${key} property with value ${result}`);
    return result;
  }
});

console.log(proxy.foo); // 输出: Accessed foo property with value 1

在这个例子中,我们没有使用receiver参数。由于没有传递receiver参数,thisget拦截函数内部指向的是原始对象data。虽然代理对象proxy被使用,但get拦截函数内部的this并不指向proxy,而是指向原始对象data。因此,这种情况下,响应式联系不会得到建立。

虽然说两个函数的输出是一致的,但显然没有使用receiver参数时响应式联系不会得到建立。也就是说在effect函数里面,对象不会得到正确的响应。

Vue-浅响应与深响应 vue-过期的副作用函数

評論