赞
踩
作者:Hanpeng_Chen
公众号:前端极客技术
大家有使用Vue开发想必对响应式都有了解,知道Vue2是用Object.defineProperty
实现数据劫持,进而实现的双向绑定。在已经发布快一年的Vue3中,数据响应式的实现由Object.defineProperty
API改成了Proxy
API。
接下来我们一起来看看这两个API的基本用法。
Object.defineProperty()
是ES5提供的方法,该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
Object.defineProperty(obj, prop, description)
description参数表示的属性描述符有两种主要形式:数据描述符和存取描述符。一个描述符只能是这两者中的一个,不能同时是两者。
数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。
存取描述符是由get
和set
函数所描述的属性。
两种描述符都是对象。
configurable
当且仅当该属性的configurable
键值为true时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认值为false。
enumerable
当且仅当该属性的enumerable
键值为true时,该属性才会出现再对象的枚举属性中。默认为false。
value
该属性对应的值。可以是任何有效的JavaScript值。默认为 undefined
。
writable
当且仅当该属性的 writable
键值为 true 时,属性的值,也就是上面的 value ,才能被赋值运算符改变。默认为false。
get
一个给属性提供 getter 函数的方法,如果没有getter为undefined。该方法返回值被用作属性值。默认为 undefined。
set
一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined。
我们通过下面一个例子来看看defineProperty是如何在访问以及赋值的时候执行get和set。
let obj = {}, value = null; Object.defineProperty(obj, 'title', { get: function() { console.log('执行了 get 操作'); return value; }, set: function(newValue) { console.log('执行了 set 操作'); value = newValue; } }) obj.title = 'defineProperty demo'; // 执行了 set 操作 console.log(obj.title); // 执行了 get 操作 // defineProperty demo obj.desc = 'xxxxxx'; // 不执行set 因为没有对desc这个属性进行劫持 console.log(obj.desc); // xxxxxx 不执行get,因为没有对desc这个属性进行劫持
从上面的例子我们可以看到,defineProperty 只能代理某个属性,如果没有对属性进行劫持代理,对其进行操作是不会走 get 和 set 方法
我们再来看看属性描述符是数据描述符的例子:
Object.defineProperty({}, 'title', {
value: 'demo',
writable: true,
enumerable: true,
configurable: true
});
如果属性描述符同时是数据描述符和存取描述符两种形式,则会报错:
// 报错:TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, #<Object>
Object.defineProperty({}, 'title', {
value: 'demo',
get: function() {
return 'defineProperty demo'
}
})
使用defineProperty只能重定义属性的get和set行为,到了ES6,提供了Proxy,可以重定义更多的行为,比如in、delete、函数调用等更多行为。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
var proxy = new Proxy(target, handler)
target:要使用 Proxy
包装的目标对象(可以是任何类型的对象,包括原生数组、函数,甚至另一个代理)。
handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了再执行各种操作时代理p
的行为。回调方法的合集。
从上面我们可以发现,Proxy也有get和set方法。和defineProperty相比,Proxy接收的target可以为任何类型的对象,包括原生数组、函数,甚至是另一个代理对象。
先来看下get和set的例子:
let proxy = new Proxy({}, {
get: (obj, propKey) => {
console.log(' get 操作')
return obj[propKey]
},
set: (obj, propKey, value) => {
console.log('设置 set 操作')
obj[propKey] = value
}
})
proxy.title = 'proxy demo' // 设置 set 操作
console.log(proxy.title) // get 操作 // proxy demo
proxy.desc = 'xxxx' // 设置 set 操作
console.log(proxy.desc) // get 操作 // xxxx
如果对象时一个多层嵌套对象,我们来看下Proxy对其属性的监听:
let obj = { a: 1, b: { a1: 11, b1: { a2: 21 } } } let proxy = new Proxy(obj, { get: (obj, propKey) => { console.log(' get 操作') return obj[propKey] }, set: (obj, propKey, value) => { console.log('设置 set 操作') obj[propKey] = value } }) console.log(proxy.a) // get 操作 // 1 console.log(proxy.b.a1) // get 操作 // 11 console.log(proxy.b.b1.a2) // get 操作 // 21 console.log('--proxy.a--') proxy.a = 'a' // 设置 set 操作 console.log('--proxy.b.a1--') proxy.b.a1 = 'a1' // 不触发 set
从上面我们可以发现,嵌套再深,我们都可以通过get监听到属性的访问。但是set并不像get自带递归,所以我们想要实现响应式,就需要对嵌套的对象或者数组,再次进行响应式处理。实现如下:
let obj = { a: 1, b: { a1: 11, b1: { a2: 21 } } } const handler = { get(obj, prop) { console.log(' get 操作') const val = obj[prop] if(val !== null && typeof val=== 'object'){ return new Proxy(val, handler);//代理内层 }else{ return val; // 返回obj[prop] } }, set(obj, prop, value) { console.log('set', prop) obj[prop] = value return true // set需要返回true 代表赋值完成,否则会报错 } } const proxy = new Proxy(obj, handler) console.log(proxy.a) // get 操作 // 1 console.log(proxy.b.a1) // get 操作 // 11 console.log(proxy.b.b1.a2) // get 操作 // 21 console.log('--proxy.a--') proxy.a = 'a' // set a console.log('--proxy.b.a1--') proxy.b.a1 = 'a1' // set a1
除了get和set,proxy可以拦截多达13种操作,比如:has(target, propKey)
,可以拦截propKey in proxy 的操作,返回一个布尔值。
// 使用 has 方法隐藏某些属性,不被 in 运算符发现
let handler = {
has (target, key) {
if (key[0] === '_') {
return false
}
return key in target;
}
};
let target = { _prop: 'foo', prop: 'foo' };
let proxy = new Proxy(target, handler);
console.log('_prop' in proxy); // false
console.log('prop' in proxy); // true
Vue3在响应式的实现性能上做了优化,其中数据响应式的实现由Object.defineProperty
API 改成了Proxy
API。因此可能会有人认为Proxy
API 的性能要优于 Object.defineProperty
,但是实际上Proxy
在性能上要比Object.defineProperty
差的。具体的可以参考这篇文章:Thoughts on ES6 Proxies Performance。
若对象内部属性要全部递归代理,Proxy 可以只在调用的时候递归,而 Object.definePropery 需要一次完成所有递归,性能比 Proxy 差。
在Vue2响应式实现中,definePropery 是在一开始,将传入的对象的所有属性全部进行递归,之后才处理set和get。 但是Vue3中的Proxy的递归是在set中,这样我们就可以根据需求来调整递归原则。也就是说,在一些条件下,让其不进行递归。这才是Vue3在响应式性能上优于Vue2的主要原因之一。
如果你觉得这篇内容对你有帮助的话:
1、点赞支持下吧,让更多的人也能看到这篇内容
2、公众号:前端极客技术,我们一起学习一起进步。
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。