Object.defineProperty与Proxy
Object.defineProperty与Proxy
JunsObject.defineProperty 与 Proxy
在 Vue2 中,响应式采用Object.defineProperty
来实现,而在 Vue3 中,使用的是Proxy
。同时,这也是一个十分常见的面试知识点,这里先了解一下这俩个 api 的作用和功能,之后下一篇文章来深入一下 Vue 的响应式原理。
Object.defineProperty
直接在一个对象上定义一个新属性,或修改其现有属性,并返回此对象。
Object.defineProperty(obj, prop, descriptor) |
type PropertyKey = string | number | symbol // 第二个参数 |
基本使用
const obj = {} |
监听 person 上的 name 属性变化:
const person = {} |
监听对象上的多个属性
咋一看感觉思路很简单,通过Object.keys()
拿到对象的所有键,然后遍历劫持就可以,所以就写出如下的代码:
const person = { |
运行一下就会发现报错了,再看下代码就可以得知在访问 person 身上的属性时,就会触发 get 方法,返回 person[key],但是访问 person[key]也会触发 get 方法,导致递归调用,最终栈溢出。
所以需要设置一个中转站来解决这个问题:
const person = { |
深度监听一个对象
我们也需要解决对象嵌套对象的这种情况,可以观察到,上面的 Observer 就是我们想要的监听函数,只需要加一层递归就可以实现了。
const person = { |
监听数组
如果监听的对象属性是一个数组呢?如何实现监听
let arr = [1, 2, 3] |
可以发现,通过push
方法给数组增加元素,set 方法是监听不到的。
事实上,通过索引访问或者修改数组中已经存在的元素,是可以触发 get 和 set 的。但是对于通过 push、unshift 增加的元素,会增加一个索引,这种情况需要手动初始化,新增加的元素才能被监听到。
在 Vue2 中,通过重写 Array 原型上的方法解决了这个问题。下一篇文章中具体研究。
Proxy
在上面还有一个问题没有解决,就是给一个对象新增属性的时候,也需要手动监听新的属性
正是因为这个原因,使用 vue 给 data 中的数组或对象新增属性时,需要使用
vm.$set
才能保证新增的属性也是响应式的。
可以看到,通过Object.definePorperty()
进行数据监听是比较麻烦的,需要大量的手动处理。这也是为什么在 Vue3.0 中尤雨溪转而采用 Proxy。
基本使用
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
const p = new Proxy(target, handler) |
- target: 要使用
Proxy
包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理) - handler: 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理
p
的行为。
通过 Proxy,我们可以对设置代理的对象上的一些操作进行拦截,外界对这个对象的各种操作,都要先通过这层拦截。一般配合Reflect
一起使用。
const obj = { |
可以看出,Proxy
代理的是整个对象,而不是对象的某个特定属性,不需要我们通过遍历来逐个进行数据绑定。
值得注意的是: 之前使用
Object.defineProperty()
给对象添加一个属性之后,对对象属性的读写操作仍然在对象本身。但是使用 Proxy,如果想要读写操作生效,我们就要对
Proxy
的实例对象proxyObj
进行操作。
解决Object.defineProperty()
中遇到的问题
在上文遇到的问题有:
一次只能对一个属性进行监听,需要遍历来对所有属性监听。这个可以解决
在遇到一个对象的属性还是一个对象的情况下,需要递归监听
对于对象的新增属性,需要手动监听
对于数组通过 push、unshift 方法增加的元素,无法监听
对于 2 来看一下
const obj = { |
可以看到,访问 proxyObj 的深层属性时,并不会触发 set。所以 proxy 如果想实现深度监听,也需要实现一个类似上文的Observer
的递归函数,使用 proxy 逐个对对象中的每个属性进行拦截,具体的实现逻辑可以参考上文。
第三个问题也解决了
p.children.height = 20 |
对于数组进行一下测试
const arr = ['111'] |
其中 push 有俩次 get 和 set 和原理有关,也很正常,push 会造成其他属性变化,长度+1 这样的。
另外,重复触发 set 可能会导致重复派发更新,可以关注下 vue3 是如何解决这个问题的。
Proxy 支持 13 种拦截操作
Objdect.defineProperty()
仅仅支持getter
和setter
,详情看文档吧
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
CV 过来的一些简介:
- get(target, propKey, receiver):拦截对象属性的读取,比如 proxy.foo 和 proxy[‘foo’]。
- set(target, propKey, value, receiver):拦截对象属性的设置,比如 proxy.foo = v 或 proxy[‘foo’] = v,返回一个布尔值。
- has(target, propKey):拦截 propKey in proxy 的操作,返回一个布尔值。
- deleteProperty(target, propKey):拦截 delete proxy[propKey]的操作,返回一个布尔值。
- ownKeys(target):拦截 Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in 循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而 Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
- getOwnPropertyDescriptor(target, propKey):拦截 Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
- defineProperty(target, propKey, propDesc):拦截 Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
- preventExtensions(target):拦截 Object.preventExtensions(proxy),返回一个布尔值。
- getPrototypeOf(target):拦截 Object.getPrototypeOf(proxy),返回一个对象。
- isExtensible(target):拦截 Object.isExtensible(proxy),返回一个布尔值。
- setPrototypeOf(target, proto):拦截 Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
- apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如 proxy(…args)、proxy.call(object, …args)、proxy.apply(…)。
- construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如 new proxy(…args)。
Proxy 中有关 this 的问题
虽然 Proxy 完成了对目标对象的代理,但是它不是透明代理,也就是说:即使 handler 为空对象(即不做任何代理),他所代理的对象中的 this 指向也不是该对象,而是 proxyObj 对象。让我们来看一个例子:
const target = { |
从 log 可以看出,被代理对象的内部 this 指向的是 proxyObj,这个可能会引起问题,例如:
const _name = new WeakMap() |
在上面的例子中,由于 jerry 对象的 name 属性的获取依靠 this 的指向,而 this 又指向 proxyObj,所以导致了无法正常代理。
这个问题的解决方法是代理这个类本身:
const _name = new WeakMap() |
此外有些 js 内置对象的正确属性的获取也需要正确的 this
const target = new Date() |
解决方法:手动绑定 this
const target = new Date('2024-01-01') |
小结
CV 自 js 红宝书
代理是 ECMAScript 6 新增的令人兴奋和动态十足的新特性。尽管不支持向后兼容,但它开辟出了一片前所未有的 JavaScript 元编程及抽象的新天地。
从宏观上看,代理是真实 JavaScript 对象的透明抽象层。代理可以定义包含捕获器的处理程序对象,
而这些捕获器可以拦截绝大部分 JavaScript 的基本操作和方法。在这个捕获器处理程序中,可以修改任何基本操作的行为,当然前提是遵从捕获器不变式。
与代理如影随形的反射 API,则封装了一整套与捕获器拦截的操作相对应的方法。可以把反射 API 看作一套基本操作,这些操作是绝大部分 JavaScript 对象 API 的基础。
代理的应用场景是不可限量的。开发者使用它可以创建出各种编码模式,比如(但远远不限于)跟踪属性访问、隐藏属性、阻止修改或删除属性、函数参数验证、构造函数参数验证、数据绑定,以及可观察对象。