如果我们需要传给子组件一个 formData 对象,并且子组件可以进行编辑,最后子组件完成修改时提醒父组件,如何实现?
JavaScript
const formData=reactive({
name:'zhangsan',
age:23,
city:'beijing',
})
v-model
- 将 formData 的属性逐个双向绑定:
html
// Father.vue
<Child v-model:name="formData.name" v-model:age="formData.age" v-model:city="formData.city"/>
// Child.vue
<input :value="name" @input="$emit('update:name',$event.target.value)"/>
<input :value="age" @input="$emit('update:age',$event.target.value)"/>
<input :value="city" @input="$emit('update:city',$event.target.value)"/>
<script setup>
defineProps(['age','name','city'])
defineEmits(['update:age','update:name','update:city'])
</script>
- 可以看出,有多少个属性就要写多少个。使用 computed 进行简化。 最简单的 computed。让 input 上只绑定一个 v-model。
JavaScript
<input @v-model="name">
const props = defineProps(['age', 'name', 'city'])
const emits = defineEmits(['update:age', 'update:name', 'update:city', 'submit'])
const name=computed({
get() {
return props.name
},
set(value) {
emit('update:name', value)
}
})
将多个 computed 加到 myModel 对象上。这里使用 Reflect 避免读取对象的属性时 ts 报错。
JavaScript
<template>
<div>
// 模板部分更简洁
<input v-model="myModel.name" />
<input v-model="myModel.age" />
<input v-model="myModel.city" />
<button @click="$emit('submit')"></button>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive } from 'vue';
// 新增数据只需修改这一对象
const data = { age: '', name: '', city: '' }
// 下面代码不再需要修改
const myModel = reactive(data)
const props = defineProps(Object.keys(data))
const emits = defineEmits([...Object.keys(data).map(key=>'update:'+key), 'submit'])
onMounted(() => {
Reflect.ownKeys(props).forEach(key => {
Reflect.set(myModel, key, computed({
get() {
return Reflect.get(props, key)
},
set(value) {
emits('update:' + String(key) as any, value)
}
}))
})
console.log(Reflect.ownKeys(props))
})
</script>
然而,父组件使用该组件时需要传入大量属性:
JavaScript
// Father.vue
<Child v-model:name="formData.name" v-model:age="formData.age" v-model:city="formData.city"/>
v-bind
实际上,可以直接 v-bind 一个对象,并且不会有警告,但这种写法是违背单向数据流的(尽管 element 组件库也是这样写的)
html
// Father.vue
<Child :form="formData"/>
// Child.vue
<template>
<div>
<input v-model="form.name" />
<input v-model="form.age" />
<input v-model="form.city" />
<button @click="$emit('submit')"></button>
</div>
</template>
<script setup lang='ts'>
defineProps(['form'])
defineEmits(['submit'])
</script>
子组件维护
如果改变需求,让子组件维护表单呢?
- 父组件不传参。
- 子组件内部维护一个 formData,负责修改和提交表单。
不足:父组件无法得知子组件的状态。 解决办法:
- 子组件定义一个提交事件,将 formData 作为参数提交即可。父组件只需绑定提交事件,就可以获得数据。
- 子组件在维护表单数据的同时,向 store 同步,其它组件通过 store 读取表单的数据。不推荐,因为引入 store 增加了维护的难度,除非确实要在多处位置共享这一表单数据。
总结
参考 element 组件库的 form 组件,我们不难得出结论,就是使用 v-bind 来绑定我们的 formData,在子组件内部对传入的 formData 的属性进行修改,并通过事件通知父组件。在父组件中提交表单。这样,子组件的职责就只是单纯的表单编辑功能,不包含执行表单提交调用 api 的义务。尽管使用 v-bind 完成了并不是它初衷的任务(使用单向数据流的指令却实现数据的双向绑定),但对父组件还是子组件来说, ,这是代码量最少的方法了,而在这里,代码量少,意味这更高的可维护性。