vue双向prop
发表于:2023-10-31 |

前言

不知道大家遇到过没有,就是子组件需要修改父组件的值,说通俗一点就是,双向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>

结语

本篇文章就到这里,更多内容敬请期待

上一篇:
【Canvas学习】02-了解canvas(二)
下一篇:
【可视化学习】51-了解react-fibre-three库