Proxy 与 Vue
尤雨溪发表了名为 Vue3.0 Updates
的主题演讲,对 Vue3.0
的更新计划、方向进行了详细阐述(感兴趣的小伙伴可以看看完整的 PPT),表示已经放弃使用了 Object.defineProperty
,而选择了使用更快的原生 Proxy
!!
这将会消除了之前 Vue2.x
中基于 Object.defineProperty
的实现所存在的很多限制:无法监听 属性的添加和删除、数组索引和长度的变更,并可以支持 Map
、Set
、WeakMap
和 WeakSet
!
Proxy介绍
概述
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
1 | var obj = new Proxy({}, { |
上面代码对一个空对象架设了一层拦截,重定义了属性的读取(get
)和设置(set
)行为。这里暂时先不解释具体的语法,只看运行结果。对设置了拦截行为的对象obj
,去读写它的属性,就会得到下面的结果。
1 | obj.count = 1 |
上面代码说明,Proxy 实际上重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义。
ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。
1 | var proxy = new Proxy(target, handler); |
Proxy 对象的所有用法,都是上面这种形式,不同的只是handler
参数的写法。其中,new Proxy()
表示生成一个Proxy
实例,target
参数表示所要拦截的目标对象,handler
参数也是一个对象,用来定制拦截行为。
下面是另一个拦截读取属性行为的例子。
1 | var proxy = new Proxy({}, { |
上面代码中,作为构造函数,Proxy
接受两个参数。第一个参数是所要代理的目标对象(上例是一个空对象),即如果没有Proxy
的介入,操作原来要访问的就是这个对象;第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。比如,上面代码中,配置对象有一个get
方法,用来拦截对目标对象属性的访问请求。get
方法的两个参数分别是目标对象和所要访问的属性。可以看到,由于拦截函数总是返回35
,所以访问任何属性都得到35
。
注意,要使得Proxy
起作用,必须针对Proxy
实例(上例是proxy
对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。
如果handler
没有设置任何拦截,那就等同于直接通向原对象。
1 | var target = {}; |
上面代码中,handler
是一个空对象,没有任何拦截效果,访问proxy
就等同于访问target
。
一个技巧是将 Proxy 对象,设置到object.proxy
属性,从而可以在object
对象上调用。
1 | var object = { proxy: new Proxy(target, handler) }; |
Proxy 实例也可以作为其他对象的原型对象。
1 | var proxy = new Proxy({}, { |
上面代码中,proxy
对象是obj
对象的原型,obj
对象本身并没有time
属性,所以根据原型链,会在proxy
对象上读取该属性,导致被拦截。
同一个拦截器函数,可以设置拦截多个操作。
1 | var handler = { |
对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。
下面是 Proxy 支持的拦截操作一览,一共 13 种。
- 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)
。
高级用法
get
利用 Proxy,可以将读取属性的操作(get
),转变为执行某个函数,从而实现属性的链式操作。
1 | var pipe = function (value) { |
上面代码设置 Proxy 以后,达到了将函数名链式使用的效果。
set
有时,我们会在对象上面设置内部属性,属性名的第一个字符使用下划线开头,表示这些属性不应该被外部使用。结合get
和set
方法,就可以做到防止这些内部属性被外部读写。
1 | const handler = { |
上面代码中,只要读写的属性名的第一个字符是下划线,一律抛错,从而达到禁止读写内部属性的目的。
has
has
方法用来拦截HasProperty
操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in
运算符。
has
方法可以接受两个参数,分别是目标对象、需查询的属性名。
下面的例子使用has
方法隐藏某些属性,不被in
运算符发现。
1 | var handler = { |
上面代码中,如果原对象的属性名的第一个字符是下划线,proxy.has
就会返回false
,从而不会被in
运算符发现。
如果原对象不可配置或者禁止扩展,这时has
拦截会报错。
1 | var obj = { a: 10 }; |
上面代码中,obj
对象禁止扩展,结果使用has
拦截就会报错。也就是说,如果某个属性不可配置(或者目标对象不可扩展),则has
方法就不得“隐藏”(即返回false
)目标对象的该属性。
值得注意的是,has
方法拦截的是HasProperty
操作,而不是HasOwnProperty
操作,即has
方法不判断一个属性是对象自身的属性,还是继承的属性。
Vue中对Proxy的使用
vue2.x 是怎么实现数据的响应的呢?
递归遍历data中的数据,使用 Object.defineProperty()劫持 getter和setter,在getter中做数据依赖收集处理,在setter中 监听数据的变化,并通知订阅当前数据的地方。 部分源码 src/core/observer/index.js#L156-L193, 版本为 2.6.11 如下
1 | let childOb = !shallow && observe(val) |
这么做有什么问题呢?
- 检测不到对象属性的添加和删除:当你在对象上新加了一个属性
newProperty
,当前新加的这个属性并没有加入vue检测数据更新的机制(因为是在初始化之后添加的)。vue.$set
是能让vue知道你添加了属性, 它会给你做处理,$set
内部也是通过调用Object.defineProperty()
去处理的 - 无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。
- 当data中数据比较多且层级很深的时候,会有性能问题,因为要遍历data中所有的数据并给其设置成响应式的。
vue3.0 使用了Proxy
vue3.0还未正式发布,不过vue-next 的相关代码已经开源出来了,目前处于Alpha版本。
为什么使用 Proxy 可以解决上面的问题呢?主要是因为Proxy是拦截对象,对对象
进行一个"拦截",外界对该对象的访问,都必须先通过这层拦截。无论访问对象的什么属性,之前定义的还是新增的,它都会走到拦截中,
举例
下面分别用Object.defineProperty()
和 Proxy
实现一个简单的数据响应
使用Object.defineProperty()
实现:
1 | class Observer { |
上面代码的执行结果为
1 | // 修改 obj原有的属性 age的输出 |
可以看到,给对象新增一个属性,内部并没有监听到,新增的属性需要手动再次使用Object.defineProperty()
进行监听。 这就是为什么 vue 2.x
中 检测不到对象属性的添加和删除的原因,内部提供的$set
就是通过调用Object.defineProperty()
去处理的。
下面我们使用 Proxy
替代 Object.defineProperty()
实现
1 | const obj = { |
可以看到下面输出
1 | // 修改原对象的age属性 |
参考文章: