你可能不知道的ES6中Proxy详解

187 阅读1分钟

代理

我们都知道Vue3不再采用Object.defineProperty来实现响应式系统了,取而代之的是ES6新增的Proxy代理。

因为Object.defineProperty有众多缺点:

  • 需要手动去遍历对象绑定GetSet,所以当数据结构很复杂时,还需要递归绑定,所以Vue2初始化data时会造成一定的性能损失。
  • 无法侦听到对象新增的属性,所以Vue2给我们提供了$setAPI。

所以Vue2是通过重写数组的那七个改变原数组的方法来实现修改数组实现响应的。

相比ES6的Proxy代理来说,Object.defineProperty的好处就是兼用IE,但是如今IE已经走远了,所以尤大当时设计Vue3的时候选择ES6新增的Proxy真是艺高人胆大。

以上说到的缺点,proxy都能解决,这就是proxy的强大之处。代理是一种由你创建的特殊对象,它封装另外一个普通对象,或者说挡在这个普通对象的前面。你可以在代理对象上注册特殊的处理函数,代理上执行各种操作的时候会调用这个程序。这些处理函数除了把操作转发给原始目标/被封装对象之外,还有机会执行额外的逻辑。

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

用法

我们可以代理一个对象,当我们试图访问对象属性的时候,它拦截[[Get]]运算。

const obj = { a: 1 };
​
const handles = {
  get(target, key, context) {
    // target === obj
    // context === p
    console.log("监听到你读取的是:", key);
    return Reflect.get(target, key, context);
  },
};
​
const p = new Proxy(obj, handle);
​
console.log(p.a)
// 监听到你读取的是:a
// 1     

我们在handles对象上声明了get(...)处理函数,它接受一个target对象的引用(这里是obj)、key属性名(这里是'a')以及接收者(这里是p)。

除了Get操作,还有Set操作:

const obj = { a: 1 };
​
const handle = {
  set(target, prop, value) {
    console.log(`监听到你在修改obj对象中的:${prop},要修改的值是:${value}`);
    Reflect.set(target, prop, value);
    // 表示成功
    return true;
  },
};
​
const p = new Proxy(obj, handle);
​
p.a = 666;
// 监听到你在修改obj对象中的:a,要修改的值是:666

Reflect

这里使用了Reflect转发,你不知道的JavaScript-中卷这样定义它:它是持有对应于各种可控的元编程任务的静态函数。这些函数一对一对应着代理可以定义的处理函数方法。

这些函数一部分看起来和Object上的同名函数类似:

  • Reflect.getOwnPropertyDescriptor( ... )
  • Reflect.defineProperty( ... )
  • Reflect.getPrototypeof( ... )
  • Reflect.setPrototypeof( ... )
  • Reflect.preventExtenions( ... )
  • Reflect.isExtensible( ... )

一般来说这些工具和Object.对应的工具行为方式类似。但是,有一个区别是如果第一个参数(即目标对象)不是对象的时候,Object.相应工具会试图把它类型转换为一个对象。而这种情况下,Refect.方法只会抛出一个错误。

  • Reflect.get(...):

    举例:Reflect.get( obj,'foo' )提取obj.foo

  • Reflect.set(...):

    举例:Reflect.set(obj,'foo',66)实际上就是执行obj.foo = 66

Reflect的元编程能力提供了模拟各种语法特性的编程等价物,把之前隐藏的抽象操作暴露出来。

代理的局限性

以下一些操作无法被拦截:

const obj ={ a:1,b:2 }
const handles = {...}
                
const p = new Proxy(obj,handles)
​
// 以下操作不会触发handles,并从代理p对象转发到目标obj上
typeof obj
String(obj)
obj + ''
obj == p
obj === p

可取消代理

普通代理总是陷入到目标对象,并且在创建之后不能修改,只要还保持着对这个代理的引用,代理的机制就将维持下去。但是,可能会存在这样的情况,比如你想要创建一个在你想要停止它作为代理时便可以被停用的代理。那就可以使用可取消代理

const obj = { a: 1 };
​
const handle = {
  get(target, key, context) {
    console.log(`监听到你在修改obj对象中的:${key}`);
    Reflect.get(target, key, context);
  },
};
​
const { proxy: p, revoke: prevoke } = Proxy.revocable(obj, handle);
​
p.a; // 监听到你在修改obj对象中的:aprevoke()  // 取消代理
p.a  // TypeError

可取消代理用Proxy.revocable(...)创建,这是一个普通函数,而不像Proxy(...)一样是构造器。除此之外,它接收的两个参数是:targethandles

和new Proxy(...)不一样,Proxy.revocable(...)的返回值不是代理本身。而是一个有两个属性,分别是proxyrevoke的对象,我们使用对象解构,把这两个属性分别赋值给了pprevoke

一旦取消代理,任何对它的访问(触发他的任意trap:比如Get、Set)都会抛出TypeError。

可取消代理的应用场景:在你的应用中把代理分发到第三方其中管理你的模型数据,而不是给出真实模型本身的引用,如果你的模型对象改变或者被替换,就可以使分发出去的代理失效/取消,这样第三方能够知晓变化并请求更新到这个模型的引用。

总结

  1. Proxy通常搭配Reflect一起使用(Vue3源码中也使用到)。
  2. Proxy的出现,正好解决了Object.defineProperty的众多缺点。

参考资料

《你不知道的 JavaScript-下卷》