前言
当初在看 ES6
的书籍时,我首次接触到了 Proxy
这个概念。"这不就是 Object.defineProperty
的替代品吗?功能与 defineProperty
如出一辙", 那时的我并未深究两者间的差异,毕竟在日常开发中鲜有机会触及这些高级特性。因此,我对这部分内容的了解浅尝辄止,如同翻阅了一本小说一样,匆匆一瞥后便迅速淡忘,只依稀记得 JavaScript 世界中存在这样一项特性。
然而,随着 Vue 3 的发布,Vue 3 的响应式系统基于 Proxy
进行了重构,这一变化在网络上引发了广泛的讨论和深入的分析。 Proxy
与 Object.defineProperty
之间的区别已成为 Vue 面试的标配,网络上关于 Proxy
与 Object.defineProperty
的对比讨论如雨后春笋般涌现。
如今,Proxy
已经成为 Vue 3 中不可或缺的一部分。
语法
Proxy 被用于创建一个对象的代理,以便对其进行操作拦截。
- target: 被代理的原始对象。
- handler: 配置拦截操作的对象。
当 handler 为 {}
时,所有对代理对象的操作默认转给原始对象 target 上。
一般我们常用的拦截操作就是对象的读取和修改的操作。handler 通过 get
和 set
捕获器来进行拦截。
当代理一个包含 getter
属性的对象时
当这代理对象是另一个对象的原型时,this 的指向会指向被代理的原始对象,这可能会在原型链中引发问题。
Reflect 就是用来解决这个问题。
Reflect 的作用
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法,例如常用的 get
和 set
操作:
这里差异在第三个参数 receiver
上,它用于指定 getter
中的 this
对象,以及 get
和 set
拦截器的第三个参数。
读取 name
属性时触发 get
捕获器执行 Reflect.set(target, prop, receiver)
代码,将 get name
中的 this 指向了 receiver
也就是 b
,因此打印的值是 'test'。
Proxy的工作原理
创建代理对象时指定的拦截函数,实际上是用来自定义代理对象本身的内部方法和行为的,而不是用来指定被代理对象的内部方法和行为的。
其他拦截操作
in 拦截
要拦截 in
操作,可以通过 has
捕获器。
for in 拦截
for in
操作的拦截要通过 ownKeys
捕获器
delete 拦截
delete
操作的拦截要通过 deleteProperty
捕获器
函数 拦截
函数拦截要通过 apply
捕获器
数组代理
代理数组不需要对内置操作数组的方法重写,会自动作用在原数组对象上。虽然不用重写,但需要注意 get
和 set
捕获器的触发顺序。
push
push
操作执行流程是将 push 的参数遍历执行 Set
操作,然后再设置 length
属性。
打印结果可以看出:
push
操作首先会读取数组的push
属性,而执行get
操作,打印get push
。- 然后再读取数组
length
属性,而再一次执行get
操作,打印get lenght
。 - 然后遍历将
push
的参数添加到数组末尾而执行set
操作,打印set 0:1
和set 1:2
。 - 最后还需要修改
length
的值,而执行set
操作,打印set length:2
。
push
的操作会多次执行 get
和 set
, 在对数组做拦截操作时,如果不清楚 push
的执行流程就很容易出现问题。
includes
数组的代理对象执行查找 includes
操作同样可以作用在原数组对象上。
但同样需要注意 get
和 set
捕获器的触发顺序:
执行流程:
- 读取数组的
includes
属性,而执行get
操作,打印get includes
。 - 然后再读取数组
length
属性,而再一次执行get
操作,打印get lenght
。 - 遍历读取数组的项直到找到指定的值,然后返回
true
。
遍历数组
常用的遍历数组的方式主要有 for...of
、forEach
、map
、for...in
。
for...of
for...of
的操作是执行数组对象上 Symbol.iterator
函数,要拦截器操作需要 get
方法中判断 key
值是否是Symbol.iterator
,再返回自定义的函数即可。
forEach、map
forEach
操作不用单独处理,执行流程和 includes
类似。
先读取 forEach
然后再读取 length
之后再遍历读取数组中的项。
shift 和unshif 操作的性能问题
shift
、 unshif
操作和 push
操作不一样,会导致数组中每个项都移动,并且都会触发一次 set
捕获器,在使用 Poxy
对象代理数组时要注意操作时的性能问题。
打印结果可以看出 unshif
的执行流程:
- 读取数组对象的
unshif
属性,执行get
方法,打印get unshift
。 - 读取数组的
length
属性,执行get
方法,打印get length
。 - 依次将数组的项往后移动一个位置。
- 将0设置为数组第一个位置上。
- 设置
length
加一。
代理 Map 和 Set 对象
Map 和Set 对象会将项目存储在内部插槽中,而这个内部插槽是只能内置方法直接访问,不能通过 get/set 方法来拦截,因此proxy无法拦截这个内部插槽。
因为 Map.prototype.set
方法中 this 是 proxy
,因此访问 this 的内部插槽不会走代理对象的 get
方法,在proxy
中无法找到这个内部插槽而报错。
解决方法就是将 this 指向原 Map 对象。
目前使用 Proxy 的常用框架
- Vue3 响应式。
- 微前端框架 qiankun 中使用 Poxy 实现 js 沙箱。