目录
v3通过使用proxy来取代Object.defineProperty
JS的程序性
<script>
// 给定一个对象product
let product = {
price: 10,
quantity: 2
}
// 设置变量total
let total = product.price * product.quantity
// 打印总价格
console.log(`总价格:${total}`);
// 修改产品的数量
product.quantity = 5
console.log(`总价格:${total}`);
</script>
控制台打印结果
在上面的代码中明明更改了产品的数量,但是产品的总价格依然是20。
初步具备响应性
<script>
// 给定一个对象product
let product = {
price: 10,
quantity: 2
}
// 定义变量total用来存储产品总价
let total = 0
// 定义一个effect方法用来计算产品总价
let effect = () => {
total = product.price * product.quantity
}
// 调用effect方法
effect()
console.log(`总价格为:${total}`); // 20
// 修改产品数量
product.quantity = 5
effect()
console.log(`总价格为:${total}`); // 50
</script>
控制台打印结果
所以,当我们需要对产品的属性进行修改的时候,我们可以通过调用effect的方法来进行更新。每次手动调用effect方法比较麻烦。
v2的响应性Object.defineProperty
代码实现流程
将产品数量变为10的时候,控制台打印结果为
代码片段
<script>
// 定义这个变量quantity的意义在于避免setter和getter死循环
let quantity = 2
// 给定一个对象product
let product = {
price: 10,
quantity: quantity
}
// 定义变量total作为产品总价
let total = 0
// 定义一个effect方法用来计算产品总价
let effect = () => {
total = product.price * product.quantity
}
// 调用effect方法
effect()
console.log(`总价格${total}`); //20
// Object.defineProperty,第一个参数为指定对象,第二个参数为指定对象的属性
Object.defineProperty(product, 'quantity', {
// 当对quantity属性赋值的时候会触发setter行为
set(newVal) {
console.log('setter');
// 修改的是变量quantity
quantity = newVal
// 调用effect方法的时候,会触发getter行为
effect()
},
// 当对quantity属性读取值的时候会触发getter行为
get() {
console.log('getter');
// 返回修改完的最新变量quantity
return quantity
}
})
</script>
v2的响应性Object.defineProperty缺陷
案例通过vue2构建
<template>
<div id="app">
<ul>
<li v-for="(value, key, index) in obj" :key="index">
{{ key }} --- {{ value }}
</li>
</ul>
<button @click="addObjProp">添加对象属性</button>
<ul>
<li v-for="(item, index) in arr" :key="index">
{{ item }}
</li>
</ul>
<button @click="addArrIndex">添加数组元素</button>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
obj: {
name: "张三",
age: 16,
},
arr: [1, 2],
};
},
methods: {
addObjProp() {
this.obj.sex = "男";
console.log(this.obj);
},
addArrIndex() {
this.arr[2] = 3;
console.log(this.arr);
},
},
};
</script>
<style>
</style>
控制台打印结果
综上所述,给对象添加一个没有在data中声明的属性时,新增属性不是响应式的,数组也是如此。
其原因就是v2是通过Object.defineProperty来实现的响应性,对于Object.defineProperty而言它只能监听指定对象指定属性的getter和setter,属性具备响应性的就是因为其被监听了getter和setter行为,这就意味着想要属性具备响应性就必须知道对象中存在该属性,但是因为js的限制,无法检测到对象新增的属性,所以新增的属性就没办法通过Object.defineProperty来监听getter和setter行为,所以其就不具备响应性。
将上面的话翻译过来就是,因为JavaScript不能监听对象中新增的属性,所以新增的属性没办法通过Object.defineProperty来监听其本身的getter和setter行为,所以当数组和对象发生变化的时候,vue检测不到,也就不具备响应性了~!
v3通过使用proxy来取代Object.defineProperty
通过proxy代替Object.definfProperty
代码实现
<script>
// 给定一个对象product
let product = {
price: 10,
quantity: 2
}
// product被代理对象
// proxtProduct代理对象
// 只能使用代理对象触发setter和getter
const proxyProduct = new Proxy(product, {
set(target, key, value, receiver) {
console.log(target, key, value, receiver);
target[key] = value
return true
console.log('setter');
},
get(target, key, receiver) {
console.log(target, key, receiver);
console.log('getter');
}
})
// 定义变量total作为产品总价
let total = 0
// 定义一个effect方法用来计算产品总价
let effect = () => {
// 因为代理对象和被代理对象拥有相同的属性,所以使用proxyProduct
total = proxyProduct.price * proxyProduct.quantity
}
// 调用effect方法
effect()
console.log(`总价格${total}`); //20
</script>
- 通过new Proxy生成一个proxy的实例,使用proxyProduct来接受
- proxy的第一个参数就是被代理对象,第二个参数handler是一个object用来写setter和getter
- set拥有四个参数,target(被代理对象),key(需要修改的属性),value(需要修改的属性值),recevier(proxy实例),
- get拥有三个参数,target(被代理对象),key(需要修改的属性),recevier(proxy实例)
- set中return true是proxy的固定格式。
- 代理对象和被代理对象拥有相同的属性,所以在effect方法中使用proxyProduct
proxy和Object.defineProperty对比
- Object.defineProperty通过指定对象和指定属性的形式,添加setter和getter方法,只有被监听的属性才能触发setter和getter。
- proxy通过代理对象的方式,可以对被代理对象的任意一个属性触发handler中的setter和getter
- 所以当v3将Object.defineProperty替换为proxy后,就不会出现在data外添加对象或数组新元素时,新增元素不具备响应性的问题了。
proxy的好搭档Reflect拦截js操作
Reflect.get方法,第一个参数就是目标对象,第二个参数就是目标对象的键名,第三个参数就是this指向值。
<script>
const p1 = {
lastName: '张',
firstName: '三',
// 添加get标识符的意义在于可以通过p1.fullname触发方法
get fullName() {
return this.lastName + this.firstName
}
}
const p2 = {
lastName: '李',
firstName: '四',
// 添加get标识符的意义在于可以通过p1.fullname触发方法
get fullName() {
return this.lastName + this.firstName
}
}
console.log(p1.fullName);//张三
console.log(Reflect.get(p1, 'fullName'));//张三
console.log(Reflect.get(p1, 'fullName', p2));//李四
</script>
proxy+reflect
因为代理对象拥有和被代理对象一样的属性,所以通过代理对象proxy.fullName拿到了张三,getter只触发了一次,但是由代码可得,getter应该被触发了三次,第一次读取fullname,第二次读取lastname,第三次读取firstName,所以应该打印三次getter触发。这是因为只有proxy代理对象触发getter行为才会打印,proxy只读取了一个fullName,因此只打印一次,另外的两次是在p1中触发的读取,不算代理对象proxy的,它的那个this是指向p1的,因此想要正确的触发三次,就需要把this指向代理对象proxy,所以reflect.get的第三个参数放上代理对象就可以将this指向代理对象了,所以只需要将原来的return target[key]改为return Reflect.get(targer,key,receiver)就可以了。
receiver其实就是代理对象proxy。
当我们需要去监听代理对象的getter和setter的时候,就需要使用reflect.get方法, 使receiver(proxy)作为this,而不是target[key]以达到期望值。就像上面的getter行为本该触发三次结果只触发了一次的情况。
总结
当需要实现响应式数据的时候,就需要多做一些事情,例如v2通过Object.defineProperty来完成响应性数据,但是其缺陷在于当你想要在data外添加新元素的时候,新元素不具备响应性,因为Object.defineProperty需要通过指定对象指定需要添加getter和setter方法,所以在某些情况下v2中的对象和数组会失去响应性,v3通过proxy代替了Object.defineProperty来完成响应性,虽然说代理对象拥有了被代理对象一模一样的属性,但是一旦在被代理对象中通过this去触发setter和getter的时候,无法被监听到。所以需要配合Reflect.get方法来完善其本身,保证每一次的getter和setter都能被监听到。