高级前端vue【Q011】vue3.0 中为什么要使用 Proxy,它相比以前的实现方式有什么改进

vue3.0 中为什么要使用 Proxy,它相比以前的实现方式有什么改进

Issue 欢迎在 Gtihub Issue 中回答此问题: Issue 12

Author 回答者: xiaoai7904

简单描述就是: 性能更好,解决无法监听数组变化问题

Author 回答者: konglingwen94

  1. 可以提高实例初始化启动速度,优化数据响应式系统,由全部监听改为惰性监听(lazy by default)。
  2. 数据响应式系统全语言特性支持,添加数组索引修改监听,对象的属性增加和删除。

Author 回答者: hefeng1208

  1. Vue2.x通过给每个对象添加getter setter属性去改变对象,实现对数据的观测,Vue3.x通过Proxy代理目标对象,且一开始只代理最外层对象,嵌套对象lazy by default ,性能会更好
  2. 支持数组索引修改,对象属性的增加,删除

Author 回答者: zhangrlhaha

在Vue 3.0 中使用 Proxy 主要是为了实现响应式数据的变化检测和触发。相比于 Vue 2.x 中使用的 Object.defineProperty,Proxy 提供了更强大和灵活的功能,以及更好的性能表现。

以下是 Proxy 相比 Object.defineProperty 的一些改进和优势:

  1. 更强大和灵活:Proxy 可以代理整个对象,而不仅仅是对象的属性。这意味着您可以监听对象的整个操作,包括属性的增删改查等。而 Object.defineProperty 只能监听对象的属性的读取和赋值操作。

  2. 更直观和易用:使用 Proxy 可以更直观地监听对象的操作,而不需要像 Object.defineProperty 那样手动定义 getter 和 setter。这使得代码更加清晰和易于理解。

  3. 更好的性能:Proxy 的性能通常比 Object.defineProperty 更好,因为 Proxy 是原生实现的,而 Object.defineProperty 是 JavaScript 引擎的内部实现。Proxy 的底层实现更高效,可以更好地处理大型对象和多次操作。

  4. 支持数组变化的检测:使用 Proxy 可以很容易地监听数组的变化,包括数组的 push、pop、splice 等操作。而 Object.defineProperty 难以实现对数组变化的监听。

Author 回答者: peipeiyinuo

首先,Vue2x中使用的是 Object.defineProerty(obj, prop, descriptor)来实现的响应式。那我们来看下defineProerty这个函数。

Object.defineProerty
  • 作用:是在一个对象定义一个新属性,或者修改一个对象的现有属性,并返回一个对象。
  • 参数:
    • 要添加属性的对象
    • 要定义或修改的属性的名称或 [Symbol]
    • 要定义或修改的属性描述符

这个函数通常是劫持对象中某一个属性,重写该属性的set,get函数,实现对属性的监听。但是在使用的场景中,我们通常需要一次性监听多个属性的变化,甚至需要监听复杂的对象嵌套,这个时候就只能在遍历该对象,并在遍历过程中递归,才可以实现复杂对象的监听。 注意的是这里不能直接遍历监听的obj对象,因为在 get 方法里面,我们在访问 obj身上的属性时,就会触发 get 方法,返回 person[key] ,但是访问 person[key] 也会触发 get 方法,导致异常递归调用,最终栈溢出。 所以这里需要设置一个中转 Observer,来让 get 中 return 的值并不是直接访问 obj[key] 在劫持数组时又有问题,当声明数组后,使用defineProerty只能劫持到声明时候的内容,也就是通过索引访问或者修改数组中已经存在的元素,是可以触发get和set的,但是对于通过push、unshift增加的元素,会增加一个索引,这种情况需要手动初始化,新增加的元素才能被监听到。另外, 通过 pop 或 shift 删除元素,会删除并更新索引,也会触发 setter 和 getter 方法。而且在Vue2.x中,通过重写Array原型上的方法解决了这个问题,此处就不展开说了,有兴趣的可以再去了解下。 事实上,在上面的讲述中,我们还有问题没有解决:那就是当我们要给对象新增加一个属性时,也需要手动去监听这个新增属性。 这也就是vue2为什么要用vm.$set来向数组及对象添加响应式内容,vm.$set简单来说就是重新走一遍上述遍历过程,来保持新增加的属性是响应式。 至此vue2的响应式大概内容说完了,可以看到上述通过 Object.definePorperty() 进行数据监听是比较麻烦的,需要大量的手动处理。

我们来看vue3中的Proxy

const p = new Proxy( target, handler );
  • 作用:通过Proxy,我们可以对设置代理的对象上的一些操作进行拦截,外界对这个对象的各种操作,都要先通过这层拦截。(和defineProperty差不多)
  • 参数:
    • target:要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
    • handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

Proxy代理的是整个对象,而不是对象的某个特定属性,不需要我们通过遍历来逐个进行数据绑定。 值得注意的是:之前我们在使用Object.defineProperty()给对象添加一个属性之后,我们对对象属性的读写操作仍然在对象本身。 但是一旦使用Proxy,如果想要读写操作生效,我们就要对Proxy的实例对象 p 进行操作。

解决vue2 Object.defineProperty的难题

  1. 一次只能对一个属性进行监听,需要遍历来对所有属性监听。这个我们在上面已经讲过了。
  2. 在遇到一个对象的属性还是一个对象的情况下,需要递归监听。
  3. 对于对象的新增属性,需要手动监听
  4. 对于数组通过push、unshift方法增加的元素,也无法监听 但是看过vue3源码的同学可以发现,vue3的响应式并不是只用了proxy来实现的,还用了一个搭档Reflect。也就是Proxy(代理) 配合 Reflect(反射) 进行设计的。那么就有疑问了 为什么要用上Reflect。

什么是 Reflect ?

Reflect 其实和 Proxy 一样都是属于 ES6 的高级API,Reflect 也是属于 window 的一个内置类,可以通过 window.Reflect 访问到。 20241106105359 细心的同学在阅读 Proxy 的 MDN 文档上可能会发现其实 Proxy 中 get 陷阱中还会存在一个额外的参数 receiver 。 通过我们上述对 window.Reflect 的打印可以看到,Reflect 的方法、属性和 Proxy 是一样的,所以 Reflect get 也是有这 第三个 receiver 属性的;这个receiver是触发代理对象的劫持时保证正确的 this 上下文指向,它正是可以修改属性访问中的 this 指向为传入的 receiver 对象。也是为了框架健壮性。使用 Object.defineProperty() 重复声明的属性会报错,因为 JavaScript 是单线程语言,一旦抛出异常,后边的任何逻辑都不会执行,所以为了避免这种情况,我们在底层就要写 大量的 try catch 来避免,不够优雅。 但是使用 Reflect.defineProperty() 是有返回值的,所以通过 返回值 来判断你当前操作是否成功。 至此vue3的响应式的大概也说完了,很多地方讲的不够细致。