前言
不知道大家遇到过没有,就是子组件需要修改父组件的值,说通俗一点就是,双向prop,一般情况下,我们做的就是父组件写一个方法,子组件emit触发父组件的方法,然后父组件修改值,这样就可以了,其实我也想过,能不能用v-model关联父组件的值,然后子组件修改v-model的值,这样就可以修改父组件的值了,但是这样很明显是不符合规范的,最近我刷b站的时候看到了渡一前端的解决思路,和大家分享一下。
常规操作修改
父组件
1 2 3 4 5 6 7 8 9 10
| <template> <div> <Child v-model="inputValue" /> </div> </template> <script setup lang="ts"> import { ref } from 'vue' import Child from './components/Child.vue' const inputValue = ref('') </script>
|
子组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <template> <input :modelValue="modelValue" @update:modelValue="handleValueChange" /> </template> <script setup lang="ts"> import { defineEmits } from 'vue'
defineProps<{ modelValue:string }>();
const emit = defineEmits(['update:modelValue']);
const handleValueChange = (e:Event) => { // 子组件值修改,触发父组件的update:modelValue事件,并将新的值传过去,父组件将msg更新为新的值 emit('update:modelValue', (e.target as HTMLInputElement).value) } </script>
|
一般我们的修改值就是这样的,因为父组件的v-model就相当于
1
| <Child :modelValue="inputValue" @update:modelValue="newVal=>inputVal=newVal" />
|
这段很容易理解,官网也有说明
使用computed实现
这里为了展示效果,我给父组件修改了一下,初始化添加一个1来校验getter
父组件
1 2 3 4 5 6 7 8 9 10
| <template> <div> <Child v-model="inputValue" /> </div> </template> <script setup lang="ts"> import { ref } from 'vue' import Child from './components/Child.vue' const inputValue = ref('1') </script>
|
子组件
这里就使用了computed设置了getter和setter,当子组件的值改变时,触发父组件的update:modelValue事件,将新的值传过去,父组件将msg更新为新的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <template> <input v-model="msg" /> </template> <script setup lang="ts"> import { defineEmits,computed } from 'vue'
const props=defineProps<{ modelValue:string }>();
const emit = defineEmits(['update:modelValue']);
const msg=computed({ get(){ return props.modelValue }, set(val){ console.log(val) emit('update:modelValue',val) } }) </script>
|
展示效果
也许你认为这样就很简单完成了,实际上并不是这样的,如果我们的参数是一个对象传递下来呢
父组件传值为对象
虽然效果也能实现,但是我们定义了多个computed,这样就会很麻烦。
父组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <template> <div> <div style="color:red">name:{{form.name}}</div> <div style="color:blue">age:{{form.age}}</div> <div style="color:green">sex:{{form.sex}}</div> <Child v-model="form" /> </div> </template> <script setup lang="ts"> import { ref } from 'vue' import Child from './components/Child.vue' const form = ref({ name:'张三', age:18, sex:'man' }) </script>
|
子组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| <template> <input v-model="name" /> <input v-model="age" /> <input v-model="sex" /> </template> <script setup lang="ts"> import { defineEmits,computed } from 'vue'
const props=defineProps<{ modelValue:{ name:string, age:number, sex:string } }>();
const emit = defineEmits(['update:modelValue']);
const name=computed({ get(){ return props.modelValue.name }, set(val){ console.log(val,'name') emit("update:modelValue", { ...props.modelValue, name: val, }); } })
const age=computed({ get(){ return props.modelValue.age }, set(val){ console.log(val,'age') emit("update:modelValue", { ...props.modelValue, age: val, }); } })
const sex=computed({ get(){ return props.modelValue.sex }, set(val){ console.log(val,'sex') emit("update:modelValue", { ...props.modelValue, sex: val, }); } }) </script>
|
展示效果
也许你会说,不能直接computed一个对象吗,这样显然是不行的
子组件computed整个对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <template> <input v-model="form.name" /> <input v-model="form.age" /> <input v-model="form.sex" /> </template> <script setup lang="ts"> import { defineEmits,computed } from 'vue'
const props=defineProps<{ modelValue:{ name:string, age:number, sex:string } }>();
const emit = defineEmits(['update:modelValue']);
const form=computed({ get(){ return props.modelValue }, set(val){ console.log(val) emit('update:modelValue',val) } }) </script>
|
你可以看到,这时set并没有生效,因为此时set要生效就需要改变form=xxx,而不是form.xx=xxx,但是你可能又发现了,父元素中的form值依旧是修改了的,这是什么原因呢,这个其实和我们下面这段代码是一样的,貌似执行没有问题,实际上打破了单行数据流的规则,开发中是最好不要这样写,很容易造成数据污染。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <!-- 父组件 --> <Child v-model="msg"></Child>
<!-- 子组件 --> <template> <div> <input v-model="msg"></input> </div> </template>
<script setup> const props = defineProps({ msg: { type: String, default: "", }, });
</script>
|
为了解决这个set不生效的问题,我们完全可以使用vue3中的实现逻辑,使用proxy实现代理
使用proxy实现
子组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| <template> <input v-model="form.name" /> <input v-model="form.age" /> <input v-model="form.sex" /> </template> <script setup lang="ts"> import { defineEmits,computed } from 'vue'
const props=defineProps<{ modelValue:{ name:string, age:number, sex:string } }>();
const emit = defineEmits(['update:modelValue']);
const form=computed({ get() { return new Proxy(props.modelValue, { get(target, key) { return Reflect.get(target, key); }, set(target, key, value,receiver) { console.log(key,value) emit("update:modelValue", { ...target, [key]: value, }); return true; }, }); }, set(newValue) { emit("update:modelValue", newValue); }, }) </script>
|
封装一个hook
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { computed } from "vue";
export default function useVModle(props, propName, emit) { return computed({ get() { return new Proxy(props[propName], { get(target, key) { return Reflect.get(target, key) }, set(target, key, newValue) { emit('update:' + propName, { ...target, [key]: newValue }) return true } }) }, set(value) { emit('update:' + propName, value) } }) }
|
子组件使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <template> <div> <input v-model="form.name"></input> <input v-model="form.age"></input> <input v-model="form.sex"></input> </div> </template> <script setup> import useVModel from "../hooks/useVModel";
const props = defineProps({ modelValue: { type: Object, default: () => {}, }, });
const emit = defineEmits(["update:modelValue"]);
const form = useVModel(props, "modelValue", emit);
</script>
|
介绍一下官网多个参数实现
远古版本
这里顺便说一下官网的多个参数实现,在一开始的时候,是这样实现的
父组件
1 2 3 4
| <UserName v-model:first-name="first" v-model:last-name="last" />
|
子组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <script setup> defineProps({ firstName: String, lastName: String })
defineEmits(['update:firstName', 'update:lastName']) </script>
<template> <input type="text" :value="firstName" @input="$emit('update:firstName', $event.target.value)" /> <input type="text" :value="lastName" @input="$emit('update:lastName', $event.target.value)" /> </template>
|
现在的版本
重头戏来了,vue其实早就想到了我们这样的需求,发布了defineModel这个api
以前的双向prop
1 2 3 4 5 6 7 8 9 10 11 12
| <script setup lang="ts"> const props = defineProps<{ modelValue: number }>()
const emit = defineEmits<{ (evt: 'update:modelValue', value: number): void }>()
// 更新值 emit('update:modelValue', props.modelValue + 1) </script>
|
现在只需要
1 2 3 4
| <script setup> const modelValue = defineModel() modelValue.value++ </script>
|
结语
本篇文章就到这里,更多内容敬请期待