前言
在写表单组件时常常会遇到这种情况,父组件将一个对象交给子组件,子组件要对对象的值进行编辑。
我们常见的做法有这么几种
- 使用
computed
,get
获取值,set
触发emit
。这样是可以的,但是对象的属性多的话,需要写许多的computed
一一对应,麻烦。 - 什么都不管,直接就是
v-model
对象的属性。这种打破了单向数据流
,本质的子组件修改了值。 - 使用
watch
,这个跟第一种computed
差不多,也是麻烦,而且更容易乱。
我觉得大部分人都是用这3种的一种,前一段时间在刷短视频的时候,发现了一个很厉害的写法(有人的应该刷到过),使用computed
与Proxy
来巧妙的处理,只能说还是水平不够。
正文
在这之前还是要了解一下什么是Proxy
,有些人可能只在背面试题中背过。
什么是Proxy
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
语法
const p = new Proxy(target, handler)
target 要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理的行为。
handler 对象的方法
下面例举了一部分的方法,具体的可以看Proxy – JavaScript | MDN (mozilla.org),但是我们会用上的基本只用get
和set
let obj = { name: 'John' };
let newObj = { age: 10 };
let proxy = new Proxy(obj, {
// 用于拦截对对象的 Object.getPrototypeOf() 操作
getPrototypeOf(target) {
console.log('getPrototypeOf 被调用');
return Reflect.getPrototypeOf(target);
},
//用于拦截对对象的 Object.setPrototypeOf() 操作
setPrototypeOf(target, proto) {
console.log('setPrototypeOf 被调用');
return Reflect.setPrototypeOf(target, proto);
},
//Object.isExtensible 方法的捕捉器。
isExtensible: function (target) {
console.log("Called isExtensible on proxy");
return Object.isExtensible(target);
},
//Object.preventExtensions 方法的捕捉器。
preventExtensions: function (target) {
console.log("Called preventExtensions on proxy");
Object.preventExtensions(target);
return true;
},
//Object.getOwnPropertyDescriptor 方法的捕捉器。
getOwnPropertyDescriptor: function (target, prop) {
console.log(`Called getOwnPropertyDescriptor on ${prop}`);
return Object.getOwnPropertyDescriptor(target, prop);
},
//Object.defineProperty 方法的捕捉器。
defineProperty: function (target, property, descriptor) {
console.log(`定义属性 ${property}`);
return Reflect.defineProperty(target, property, descriptor);
},
//in 操作符的捕捉器
has(target, prop) {
console.log(`检查 ${prop}`);
return Reflect.has(target, prop);
},
//属性读取操作的捕捉器
get(target, prop, receiver) {
console.log(`读取 ${prop}`);
return Reflect.get(target, prop, receiver);
},
//属性设置操作的捕捉器。
set(target, prop, value, receiver) {
console.log(`设置 ${prop}`);
return Reflect.set(target, prop, value, receiver);
},
//delete 操作符的捕捉器
deleteProperty(target, prop) {
if (prop in target) {
delete target[prop];
console.log(`Property ${prop} has been deleted.`);
return true;
} else {
console.log(`Property ${prop} does not exist.`);
return false;
}
},
//Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器
ownKeys(target) {
return Reflect.ownKeys(target).filter(key => typeof key !== 'symbol');
},
});
深度监听对象
let obj = {
name: 'aa',
age: 19,
children: {
childrenName: 'bb',
childrenAge: 20
}
}
const setProxyObj = (obj) => {
if (typeof obj !== 'object' || obj === null) {
return
}
return new Proxy(obj, {
get(target, key) {
console.log('get', key)
if (typeof target[key] === 'object') {
return setProxyObj(target[key])
}
return Reflect.get(target, key)
},
set(target, key, value) {
console.log('set', key)
return Reflect.set(target, key, value)
}
})
}
let proxy = setProxyObj(obj)
proxy.age = {
num: 12,
}
proxy.age.num = 14
//输出
// set age
// get age
// set num
什么是Object.defineProperty
Object.defineProperty()
静态方法会直接在一个对象上定义一个新属性,或修改其现有属性,并返回此对象。
语法
Object.defineProperty(obj, prop, descriptor)
obj 要定义属性的对象。
prop 一个字符串或Symbol,指定了要定义或修改的属性键。
descriptor 要定义或修改的属性的描述符。
例子
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42,
});
console.log(object1.property1);
对象中存在的属性描述符有两种主要类型:数据描述符和访问器描述符。数据描述符是一个具有可写或不可写值的属性。访问器描述符是由 getter/setter 函数对描述的属性。
描述符只能是这两种类型之一,不能同时为两者。也就是说,一个属性不能同时有
value
或writable
和get
或set
键。
数据描述符
value
与属性相关联的值。可以是任何有效的 JavaScript 值(数字、对象、函数等)。默认值为 undefined。
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42,
});
console.log(object1.property1); // 输出: 42
writable
属性值可以使用赋值运算符更改。默认值为 false
。
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42,
});
object1.property1 = 77;
console.log(object1.property1); // 输出: 42
//----------------------------------------------------------
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42,
writable: true
});
object1.property1 = 77;
console.log(object1.property1); // 输出: 77
enumerable
(与访问器描述符
共享可选键)
属性可以在循环中被枚举。默认值为 false
。
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42,
enumerable: false,
});
console.log(Object.keys(object1)); //[]
//----------------------------
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42,
enumerable: true,
});
console.log(Object.keys(object1)); //[ 'property1' ]
configurable
(与访问器描述符
共享可选键)
属性设置为 true
或 false
,
- 如果
configurable
设置为true
,那么这个属性描述符的类型可以被改变,也可以从对应的对象上被删除。 - 如果
configurable
设置为false
,那么这个属性描述符的类型不可以被改变,也不可以从对应的对象上被删除。
“这个属性描述符的类型可以被改变”的意思是,如果一个属性的configurable
设置为true
,那么你可以再次使用Object.defineProperty()
来修改这个属性的描述符。
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42,
configurable: true
});
Object.defineProperty(object1, 'property1', {
value: 29,
});
console.log(object1.property1); // 输出: 29
delete object1.property1; // 删除属性
console.log(object1.property1); // 输出: undefined
访问器描述符
get
用作属性 getter 的函数,如果没有 getter 则为 undefined
。当访问该属性时,将不带参地调用此函数,并将 this
设置为通过该属性访问的对象(因为可能存在继承关系,这可能不是定义该属性的对象)。返回值将被用作该属性的值。默认值为 undefined
。
set
用作属性 setter 的函数,如果没有 setter 则为 undefined
。当该属性被赋值时,将调用此函数,并带有一个参数(要赋给该属性的值),并将 this
设置为通过该属性分配的对象。默认值为 undefined
。
例子
let obj = {
_name: 'default' // 私有变量
};
Object.defineProperty(obj, 'name', {
get: function() {
return this._name;
},
set: function(value) {
this._name = value;
}
});
console.log(obj.name); // 输出: 'default'
obj.name = 'new name'; // 使用 setter
console.log(obj.name); // 输出: 'new name'
深度监听对象
let obj = {
name: 'aa',
age: 19,
children: {
childrenName: 'bb',
childrenAge: 20
}
}
const setDefinePropertyObj = (obj) => {
if (typeof obj !== 'object' || obj === null) {
return
}
Object.keys(obj).forEach((key) => {
let value = obj[key]
if (typeof value === 'object') {
setDefinePropertyObj(value)
}
Object.defineProperty(obj, key, {
get() {
console.log('get', key)
return value
},
set(newValue) {
console.log('set', key)
if (typeof newValue === 'object') {
setDefinePropertyObj(newValue)
}
value = newValue
}
})
})
}
setDefinePropertyObj(obj)
Proxy实战使用
前面讲了那么多,只是为了让大家了解知识,真正还是要看知识在实际中的运用(使用computed
配合Proxy
拦截v-model
),不然就是光说不做假把戏。
父组件
<template>
<div id="app">
<Test v-model="form.user" />
</div>
</template>
<script lang="ts">
import Test from './pages/test/index.vue'
export default {
components: {
Test,
},
props: {
},
data() {
return {
form: {
user: {
name: "aaaa",
age: 12,
}
}
}
},
mounted() {
},
methods: {
}
}
</script>
<style scoped></style>
子组件
<template>
<div>
<el-input v-model="model.name" placeholder="请输入内容"></el-input>
<el-input v-model="model.age" placeholder="请输入内容"></el-input>
</div>
</template>
<script>
export default {
props: {
value: {
type: Object,
default: () => { }
}
},
data() {
return {
}
},
computed: {
model: {
get() {
let that = this
const proxy = new Proxy(this.value, {
get(target, key) {
return Reflect.get(target, key)
},
set(target, key, value) {
console.log('set', key, value);
that.$emit('input', { ...that.value, [key]: value });
return true
}
})
return proxy
},
set(val) {
this.$emit('input', val)
}
}
},
methods: {
}
}
</script>
<style scoped></style>
结语
感兴趣的可以去试试
原文链接:https://juejin.cn/post/7338634091397431330 作者:卸任