写在开头
希望你阅读完这篇文章后能轻松回答出以下的几个问题:
- vue3 为什么要用 proxy
- defineProperty 和 proxy 的区别
- 你是怎么理解 ES6 中的 Proxy?
- Proxy 的使用场景有哪些?
Object.defineProperty
第一次接触这个 API 是在 JS 对象中,这个 API 的功能就是修改对象属性描述符。
对象属性描述符:主要是对属性进行一些限制,比如这个属性是否可以通过 delete 删除?这个属性是否可以在 for-in 遍历的时候被遍历出来呢?属性描述有两种类型:
- 数据属性描述符
- 访问器属性描述符
- get:获取属性时会执行的函数,默认为 undefined(vue2 主要使用)
- set:设置属性时会执行的函数,默认为 undefined(vue2 主要使用)
- 等等
vue2 利用 Object.defineProerty
劫持对象的访问器(get
与 set
),在属性值发生变化时,我们可以获取变化,从而进一步操作的。
简单例子
Object.defineProperty
语法:
- obj:要定义属性的对象
- prop:一个字符串或者 Symbol,指定了要定义或者修改的属性键
- descriptor:要定义或修改的属性的描述符
Object.defineProperty(obj, prop, descriptor)
利用上述描述的 Object.defineProperty 来实现一个简单的响应函数defineReactive
function update(){
app.innerText = obj.foo
}
function defineReactive(obj,key,val){
Object.defineProperty(obj,key,{
get(){
console.log(`get ${key}:${val}`)
return val
},
set(newVal){
if(newVal!==val){
val = newVal
update()
}
}
})
}
调用defineReactive
,数据发生变化触发update
方法,实现数据响应式:
const obj = {}
defineReactive(obj, 'foo', '')
setTimeout(()=>{
obj.foo = new Date().toLocaleTimeString()
},1000)
缺陷一:只能劫持对象的属性
上述定义的简单响应式函数并不能满足大部分的要求,针对以下情况还需要补充:
- 在对象存在多个
key
情况下,需要进行遍历 - 如果存在嵌套对象的情况,还需要在
defineReactive
中进行递归 - 当给
key
赋值为对象的时候,还需要在set
属性中进行递归
function update() {
app.innerText = obj.foo
}
//如果存在嵌套对象的情况,还需要在defineReactive中进行递归
function defineReactive(obj, key, val) {
observe(val)
Object.defineProperty(obj, key, {
get() {
console.log(`get ${key}:${val}`);
return val
},
//当给key赋值为对象的时候,还需要在set属性中进行递归
set(newVal) {
if (newVal !== val) {
val = newVal
observe(newVal) // 新值是对象的情况
update()
}
}
})
}
//在对象存在多个key情况下,需要进行遍历
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
以上算是能够实现了对一个对象的基本响应式,但是仍然存在一些问题:
- 现在对一个对象进行删除与添加属性操作,也是无法劫持到
缺陷二:只能劫持数组的部分操作
我们总说,Object.defineProperty
不能拦截数组,这种说法不太准确,看示例:
let list = [1,2,3,4];
observe(list);
console.log(list[0]) // 拦截到正在获取属性:0
list[0] = 2; // 拦截到正在修改属性:0
list[6] = 6; // 无法拦截...
list.push(3); // 无法拦截...
通过索引去访问或修改已经存在的元素,是可以拦截到的。如果是不存在的元素,或者是通过push
等方法去修改数组,则无法拦截。
- push
- pop
- shift
- unshift
- splice
- sort
- reverse
因此,vue2 在使用时,使用了一些hack,重写了数组原型上的七个方法来解决无法拦截的问题。
Proxy
proxy 译为代理,可以理解为在操作目标对象前有一层程序,将所有本该我们手动操作的工作都交给这层程序来处理。生活中也有许多“proxy”,比如:代购、中介等。
除此之外,Proxy 拦截的是整个对象,根本不用关心具体的 key,它可以去「修改 obj 上的任意 key」 和「读取 obj 上的任意 key」,最后返回一个新的对象
语法
- target 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
- handler 一个通常以函数作为属性的对象,用来定制拦截行为
const proxy = new Proxy(target, handle)
常见的 handler 方法如下:
方法 | 描述 |
---|---|
handler.has() | in 操作符的捕捉器。 |
handler.get() | 属性读取操作的捕捉器。 |
handler.set() | 属性设置操作的捕捉器。 |
handler.deleteProperty() | delete 操作符的捕捉器。 |
handler.ownKeys() | Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。 |
handler.apply() | 函数调用操作的捕捉器。 |
handler.construct() | new 操作符的捕捉器 |
简单例子
利用上述描述的proxy
来实现一个简单的响应函数defineReactive
const reactive = obj => {
// 如果obj不是一个对象,就没必要包装了
if(typeof obj !== 'object' || !obj) {
return obj;
}
const proxyConfig = {
get(target, key, receiver) {
console.log('拦截到正在获取属性:' + key);
return Reflect.get(target, key, receiver)
},
set(target, key, val, receiver) {
console.log('拦截到正在修改属性:' + key);
return Reflect.set(target, key, val, receiver);;
}
};
const observed = new Proxy(obj, proxyConfig);
return observed;
}
测试一下简单数据的操作,发现都能劫持
const state = reactive({
foo: 'foo'
})
// 1.获取
state.foo // ok
// 2.设置已存在属性
state.foo = 'fooooooo' // ok
// 3.设置不存在属性
state.dong = 'dong' // ok
// 4.删除属性
delete state.dong // ok
测试一下简单数组的操作:
let listProxy = reactive(['张三','李四','王五','赵六']);
console.log(listProxy[0]) // 拦截到正在获取属性:0
listProxy[0] = '李四'; // 拦截到正在修改属性:0
listProxy[6] = 6; // 拦截到正在修改属性:6
/**
* 拦截到正在获取属性:push
* 拦截到正在获取属性:length
* 拦截到正在修改属性:7
* 拦截到正在修改属性:length
*/
listProxy.push('赵七');
解释一下,为何
listProxy.push
会经历这么多过程:
- 获取 push 方法:首先需要获取数组的 push 方法,这会触发 get 拦截器,输出:拦截到正在获取属性:push
- 获取 length 属性:push 方法需要知道数组的当前长度,以便在数组的末尾添加新元素,这会触发 get 拦截器,输出:拦截到正在获取属性 push
- 设置新元素:push 方法会在末尾添加新元素,这会触发 set 拦截器,输出:拦截到正在修改属性:7
- 更新 leng 属性:push 方法会更新数组的 length 属性以反映新元素的添加,这会触发 set 拦截器,输出:拦截到正在修改属性:length
人无完人:嵌套对象
虽然 proxy 可以代理整个对象,但是对于这种嵌套对象来说,会有以下问题:
- 深层嵌套对象未被代理:在上述的简单 reactive 函数中,代理只应用于顶层对象。如果一个对象的某个属性本身是一个对象(如:person.address),那么该嵌套对象的属性访问和修改不会被 完全拦截
let person = {
name: 'yuanwill',
age: 26,
address: {
home: 'guangzhou',
now: 'shenzhen'
}
};
const personProxy = reactive(person)
personProxy.name = {
firstName: 'yuan',
lastName: 'will'
}; // 拦截到正在修改属性:name
personProxy.name.firstName = 'haha'; // 拦截到正在获取属性:name
console.log(personProxy.name); // 拦截到正在获取属性:name
/**
* 拦截到正在获取属性:address
* shenzhen
*/
console.log(personProxy.address.now)
personProxy.address.home = '北京'//拦截到正在获取属性:address
可以看到,person.name.firstName
依然没有拦截到正在修改firstName
属性。原因在于,get
返回的可能是个对象,我们需要对这个对象再次代理,所以修改如下:
const observer = obj => {
// 如果obj不是一个对象,就没必要包装了
if(typeof obj !== 'object' || !obj) {
return obj;
}
const proxyConfig = {
get(target, key, receiver) {
console.log('拦截到正在获取属性:' + key);
const result = Reflect.get(target, key, receiver);
return observer(result);
},
set(target, key, val, receiver) {
console.log('拦截到正在修改属性:' + key);
return Reflect.set(target, key, val, receiver);;
}
};
const observed = new Proxy(obj, proxyConfig);
return observed;
}
在 get 拦截器中使用了递归调用observer
来尝试代理嵌套对象,以解决嵌套对象代理问题,验证如下:
const personProxyNew = observer(person)
personProxyNew.name = {
firstName: 'yuan',
lastName: 'will'
}; // 拦截到正在修改属性:name
/**
* 拦截到正在获取属性:name
* 拦截到正在修改属性:firstName
*/
personProxyNew.name.firstName = 'haha';
/**
* 拦截到正在获取属性:name
* { firstName: 'haha', lastName: 'will' }
*/
console.log(personProxyNew.name);
/**
* 拦截到正在获取属性:address
* 拦截到正在获取属性:now
* shenzhen
*/
console.log(personProxyNew.address.now)
/**
* 拦截到正在获取属性:address
* 拦截到正在修改属性:home
*/
personProxyNew.address.home = '北京'
人无完人:兼容性
Proxy
的劣势就是兼容性问题,而且无法用polyfill
磨平。
应用场景
Porxy 提供了对对象操作的全面拦截能力,功能类似于设计模式中的代理模式,因此,可以应用的场景如下:
- 拦截和监视外部对对象的访问:Vue 中的响应式原理
- 在复杂操作前对操作进行校验或对所需资源进行管理
使用Proxy在访问或修改对象属性时,自动进行格式化和数据转换。
const user = {
firstName: 'John',
lastName: 'Doe',
age: 25
};
const userProxy = new Proxy(user, {
get(target, property) {
if (property === 'fullName') {
return `${target.firstName} ${target.lastName}`;
}
return target[property];
},
set(target, property, value) {
if (property === 'age') {
if (typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
if (value < 0) {
throw new RangeError('Age must be a positive number');
}
}
target[property] = value;
return true;
}
});
// 使用代理对象
console.log(userProxy.fullName); // 自动格式化输出:John Doe
userProxy.age = 30; // 正常设置
console.log(userProxy.age); // 输出:30
try {
userProxy.age = 'thirty'; // 触发类型错误
} catch (e) {
console.error(e.message); // 输出:Age must be a number
}
try {
userProxy.age = -5; // 触发范围错误
} catch (e) {
console.error(e.message); // 输出:Age must be a positive number
}
Proxy
使用场景还有很多很多,不再一一列举,如果你需要在某一个动作的生命周期内做一些特定的处理,那么Proxy
都是适合的
最后
阅读完这篇文章后,你是否对开篇的问题有了更明确的答案?
如果您看到这里了,并且觉得这篇文章对您有所帮助,希望您能够点赞👍和收藏⭐支持一下作者🙇🙇🙇,感谢🍺🍺!如果文中有任何不准确之处,也欢迎您指正,共同进步。感谢您的阅读,期待您的点赞👍和收藏⭐!
接下来,我会推出一系列Vue 3.0的文章,欢迎关注,一起探索!