likes
comments
collection
share

Proxy为什么需要Reflect,从原理层面理解它们

作者站长头像
站长
· 阅读数 18

一、开篇

最近在研究Vue3的响应原理,发现Vue3响应式数据是基于Proxy实现的,为了更好学习响应数据原理,需要对Proxy深入理解。在某些时刻,Proxy还需要Reflect辅助才能五杀,那么下来我们对它们进行深入解刨,研究研究

二、Proxy

先看一下,Proxy的基本用法

const obj = { name: "World" };
const handler = {
  get(target, key) {
    return `Hello ${target[key]}`;
  },
};
const p = new Proxy(obj, handler);
console.log(p.name); // Hello World

在执行p.name的时候,Proxy做了些什么

不知道大家有木有和我有同样的疑问❓,在执行p.name的时候,Proxy做了些什么,直接调用handler对象内的方法get。那咱们看看 ECMAScript@2023 10.5 里面介绍了Proxy Handler Methods

Internal MethodHandler Method
[[GetPrototypeOf]]getPrototypeOf
[[SetPrototypeOf]]setPrototypeOf
[[IsExtensible]]isExtensible
[[PreventExtensions]]preventExtensions
[[GetOwnProperty]]getOwnPropertyDescriptor
[[DefineOwnProperty]]defineProperty
[[HasProperty]]has
[[Get]]get
[[Set]]set
[[Delete]]deleteProperty
[[OwnPropertyKeys]]ownKeys
[[Call]]apply
[[Construct]]construct

当执行 p.name的时候, 相当于 执行了 Proxy 内置放法 [[Get]] 然后再执行 Handler Method,那它是怎么调用 get 呢,咱们继续 ECMAScript@2023 10.5.8 [[GET]]的介绍 Proxy为什么需要Reflect,从原理层面理解它们 核心步骤是1-7、10

  1. handler 成为 O.[[ProxyHandler]] --- O是代理对象
  2. 如果 handlernull,抛出语法异常
  3. 断言 handler 类型是 Object
  4. target 成为 O.[[ProxyTarget]]
  5. trap 成为 GetMethod(handler, "get") --- (查阅GetMethod,从handler内获取get属性)
  6. 如果 trapundefined,返回 target.[[Get]](P, Receiver) 7、10. 否则 返回 Call(traphandler, « targetPReceiver »)

细心的小伙伴,从上面看出,如果 handlerundefined,就抛出异常 Proxy为什么需要Reflect,从原理层面理解它们 如果 handler 中没有 get方法,从代理对象中[[GET]]获取值并返回,即target.[[Get]](P, Receiver) 那么直接返回 handler 中的get方法执行的值,get.call(handler,target, P, Receiver)(P是key--p.name的name)

以上就是p.name 获取值的整个流程

二、Receiver 是什么

第6、7步中 Receiver 是什么呢,我们可以从下面的案例1中看看 到底是啥

const obj = {
  name: "obj",
  get value() {
    return this.name;
  },
};
const handler = {
  get(target, key, receiver) {
    console.log(receiver === proxy); // true
    return target[key];
  },
};
const proxy = new Proxy(obj, handler);
console.log(proxy.value); // obj

上面的案例,receiver 就是代理对象

那么再看看下面的案例2

const obj = {
  name: "obj",
  get value() {
    return this.name;
  },
};
const handler = {
  get(target, key, receiver) {
    console.log(receiver === proxy); // false
    console.log(receiver === child); // true
    console.log(target === obj); // true
    return target[key];
  },
};
const proxy = new Proxy(obj, handler);
const child = {
  name: "child",
};
Object.setPrototypeOf(child, proxy);
console.log(child.value); // obj

child 继承 proxy 之后,child.value 输出的是 obj,但是这个不符合我们想要的,应该是输出值是 child 的。那我们就来分析一下

  1. child.value,由于 child 没有 value 属性,因此需要从父级获取 value
  2. 父级是 proxy,child.value 就相当于是 proxy.value,因此就和上面的例子一样,输出obj.valueobj 那我们怎么才能 child.value 输出 child呢? 细心的小伙伴,可能发现这里的 receiver 值发生了变更,案例1 中是 proxy,而 案例2中是 child

可以得出的结论receiver 就是Proxy或者继承Proxy的对象,proxy.value,proxy是Proxy,receiver就是 proxychild.valuechild是继承Proxy的对象,因此receiver就是 child

这里需要用到 Reflect 结合 receiver 来实现我们想要的结果了(child.value 输出 child)。 修改一下get,引入Reflect来实现 child.value 输出 child

const obj = {
  name: "obj",
  get value() {
    return this.name;
  },
};
const handler = {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver);
  },
};
const proxy = new Proxy(obj, handler);
const child = {
  name: "child",
};
Object.setPrototypeOf(child, proxy);
console.log(child.value); // child

三、Reflect 工作原理

那 Reflect 怎么实现能实现上面的功能呢,即 child.value 输出 child

1. 从 MDN 来看 Reflect

先看看MDN是怎么介绍 Reflect.get(target, propertyKey[, receiver]) 方法的:

Reflect.get() 方法与从 对象 (target[propertyKey]) 中读取属性类似,但它是通过一个函数执行来操作的。

Reflect.get()就相当于 target[propertyKey] 从target读取值 另外还有一段 receiver解释

如果target对象中指定了getterreceiver则为getter调用时的this值。

上面的这句话就极为重要了,原来 receiver是来执行 target 对象内的 getter调用时的this指向的。那一切就真相大白了。 案例3 部分代码

const obj = {
  name: "obj",
  get value() {
    return this.name;
  },
};

const handler = {
  get(target, key, receiver) {
    console.log(receiver === child); // true
    return Reflect.get(target, key, receiver);
  },
}

案例3receiver 指向 child, 通过 Reflect.get(target, key, receiver) 修改 objgetterthis指向为 child,因此 getter 返回的 this.name 就是 child.name,因此 child.value 输出 child

2. 从 ECMAScript规范中分析Reflect.get

Proxy为什么需要Reflect,从原理层面理解它们

  1. 如果 target 不是 Object,就会抛出语法错误
  2. 让key 成为 ? ToPropertyKey(propertyKey)
  3. 如果 receiver不存在,将 receiver 赋值为 target
  4. 返回 ? target.[Get]

Reflect.get(target, key, receiver) 这里直接写成Reflect.get(obj, value, child) 那么 Reflect.get(obj, value, child) 等价与 obj.[Get]。接下来分析 obj.[[Get]] Proxy为什么需要Reflect,从原理层面理解它们

  1. 返回 ? OrdinaryGet 那么 Reflect.get(obj, value, child) 等价与 OrdinaryGet。这里的 OobjPvalue, childReceiver。 接下来分析 OrdinaryGet Proxy为什么需要Reflect,从原理层面理解它们
  2. desc 成为 O.[GetOwnProperty]
  3. 如果 descundefined
    1. parent 成为 O.[GetPrototypeOf].
    2. 如果 parentnull,返回 undefined
    3. 否则返回 parent.[Get].
  4. [IsDataDescriptor](desc) 为 true,直接把 desc 对应的值返回
  5. 断言 desc 拥有 get
  6. getter 成为 desc.[[Get]].
  7. 如果getterundefined, 返回 undefined
  8. 否则 返回 Call(getter, Receiver),不在分析 Call原理了,这里就相当于 getter.call(Receiver),有兴趣的小伙伴去看一下 ECMAScript® 2023 Language

经过上面的一番分析,原来 Reflect.get(obj,value,child), 就相当于 执行 obj 里面的 getter,修改了 getter内部的指针 get.call(child)

四、总结

  1. Proxy 功 规范 10.5.8 了解到 [[GET]] 调用内部 get捕捉器
  2. Receiver是:Proxy或者继承Proxy的对象
  3. Proxy 代理的对象,需要配合 Reflect 修改 getter setter内部指针(但是不是所有的地方都需要配置 Reflect,Vue3 响应数据根据情况使用 Reflect
  4. Reflect 从 规范 28.15 来分析,如果target对象中指定了getterreceiver则为getter调用时的this

五、参考

  1. MDN-Proxy
  2. MDN-Reflect
  3. ECMAScript® 2023 Language

上面如果分析不妥,还请大佬指教,谢谢 😊😊😊

如果对您有帮助,欢迎来个👍🏻