Proxy的使用细节

633 阅读5分钟

前言

当初在看 ES6 的书籍时,我首次接触到了 Proxy 这个概念。"这不就是 Object.defineProperty 的替代品吗?功能与 defineProperty 如出一辙", 那时的我并未深究两者间的差异,毕竟在日常开发中鲜有机会触及这些高级特性。因此,我对这部分内容的了解浅尝辄止,如同翻阅了一本小说一样,匆匆一瞥后便迅速淡忘,只依稀记得 JavaScript 世界中存在这样一项特性。

然而,随着 Vue 3 的发布,Vue 3 的响应式系统基于 Proxy 进行了重构,这一变化在网络上引发了广泛的讨论和深入的分析。 ProxyObject.defineProperty 之间的区别已成为 Vue 面试的标配,网络上关于 ProxyObject.defineProperty 的对比讨论如雨后春笋般涌现。

如今,Proxy 已经成为 Vue 3 中不可或缺的一部分。

语法

Proxy 被用于创建一个对象的代理,以便对其进行操作拦截。

code1.png
  • target: 被代理的原始对象。
  • handler: 配置拦截操作的对象。

当 handler 为 {} 时,所有对代理对象的操作默认转给原始对象 target 上。

code2.png

一般我们常用的拦截操作就是对象的读取和修改的操作。handler 通过 getset 捕获器来进行拦截。

code3.png

当代理一个包含 getter 属性的对象时

code4.png

当这代理对象是另一个对象的原型时,this 的指向会指向被代理的原始对象,这可能会在原型链中引发问题。

code5.png

Reflect 就是用来解决这个问题。

Reflect 的作用

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法,例如常用的 getset 操作:

code6.png

这里差异在第三个参数 receiver 上,它用于指定 getter 中的 this 对象,以及 getset 拦截器的第三个参数。

code7.png

读取 name 属性时触发 get 捕获器执行 Reflect.set(target, prop, receiver) 代码,将 get name 中的 this 指向了 receiver 也就是 b ,因此打印的值是 'test'。

Proxy的工作原理

创建代理对象时指定的拦截函数,实际上是用来自定义代理对象本身的内部方法和行为的,而不是用来指定被代理对象的内部方法和行为的。

其他拦截操作

in 拦截

要拦截 in 操作,可以通过 has 捕获器。

code8.png

for in 拦截

for in 操作的拦截要通过 ownKeys 捕获器

code9.png

delete 拦截

delete 操作的拦截要通过 deleteProperty 捕获器

code10.png

函数 拦截

函数拦截要通过 apply 捕获器

code11.png

数组代理

代理数组不需要对内置操作数组的方法重写,会自动作用在原数组对象上。虽然不用重写,但需要注意 getset 捕获器的触发顺序。

push

code12.png

push 操作执行流程是将 push 的参数遍历执行 Set 操作,然后再设置 length 属性。

code14.png

打印结果可以看出:

  • push 操作首先会读取数组的 push 属性,而执行 get 操作,打印 get push
  • 然后再读取数组 length 属性,而再一次执行 get 操作,打印 get lenght
  • 然后遍历将 push 的参数添加到数组末尾而执行 set 操作,打印 set 0:1set 1:2
  • 最后还需要修改 length 的值,而执行 set 操作,打印 set length:2

push 的操作会多次执行 getset, 在对数组做拦截操作时,如果不清楚 push 的执行流程就很容易出现问题。

includes

数组的代理对象执行查找 includes 操作同样可以作用在原数组对象上。

code15.png

但同样需要注意 getset 捕获器的触发顺序:

code16.png

执行流程:

  • 读取数组的 includes 属性,而执行 get 操作,打印 get includes
  • 然后再读取数组 length 属性,而再一次执行 get 操作,打印 get lenght
  • 遍历读取数组的项直到找到指定的值,然后返回 true

遍历数组

常用的遍历数组的方式主要有 for...offorEachmapfor...in

for...of

for...of 的操作是执行数组对象上 Symbol.iterator 函数,要拦截器操作需要 get 方法中判断 key 值是否是Symbol.iterator,再返回自定义的函数即可。

code17.png

forEach、map

forEach 操作不用单独处理,执行流程和 includes 类似。

code18.png

先读取 forEach 然后再读取 length 之后再遍历读取数组中的项。

shift 和unshif 操作的性能问题

shiftunshif 操作和 push操作不一样,会导致数组中每个项都移动,并且都会触发一次 set 捕获器,在使用 Poxy 对象代理数组时要注意操作时的性能问题

code19.png

打印结果可以看出 unshif 的执行流程:

  • 读取数组对象的 unshif 属性,执行 get 方法,打印get unshift
  • 读取数组的 length 属性,执行 get 方法,打印 get length
  • 依次将数组的项往后移动一个位置。
  • 将0设置为数组第一个位置上。
  • 设置 length 加一。

代理 Map 和 Set 对象

Map 和Set 对象会将项目存储在内部插槽中,而这个内部插槽是只能内置方法直接访问,不能通过 get/set 方法来拦截,因此proxy无法拦截这个内部插槽。

code20.png

因为 Map.prototype.set 方法中 this 是 proxy,因此访问 this 的内部插槽不会走代理对象的 get 方法,在proxy 中无法找到这个内部插槽而报错。

解决方法就是将 this 指向原 Map 对象。

code21.png

目前使用 Proxy 的常用框架

  • Vue3 响应式。
  • 微前端框架 qiankun 中使用 Poxy 实现 js 沙箱。