v-model
最开始接触v-model,是用于input原生组件中,可以实现值的双向绑定,在input中输入 ,text也会同步更新
<input v-model="text" />
其实际是一个语法糖,展开如下
<input
v-bind:value="text"
@input="text = $event.target.value"
/>
通过这样的方式实现了值的双向绑定
父子组件
我们使用v-model语法糖,同样可以实现父组件传递给子组件的值实现‘双向绑定’,实现‘子组件中的值发生改变,相对应的父组件的值会自动更新改变’
但是首先我们需要记住的原则是,prop是单向数据流,在子组件中是不能修改prop的,否则就会出现Unexpected mutation of “XXX” prop vue/no-mutating-props这样的报错。
那v-model是如何实现“双向数值绑定的呢”,其实还是需要子组件向父组件emit一个事件,不过父组件不需要再额外定义方法,只需使用v-model进行绑定即可
示例
定义一个father组件
<template>
<div>
<div>{{ value }}</div>
<div>{{ value2 }}</div>
<child v-model:value="value" v-bind:value2="value2"></child>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import child from "@/views/test/vmodel-test/child.vue";
let value = ref(111);
let value2 = ref({ value: 222 });
</script>
<style scoped></style>
其中value是通过v-model语法糖进行绑定的,其展开应该是这样子的:
<child
:value="value"
@update:value="newValue => value= newValue"
/>
那么我们就需要在子组件中定义一个emit事件为**@update:value**
<template>
<div>
<h1 @click="$emit('update:value', Math.random())">{{ value }}</h1>
<h3>{{ value2 }}</h3>
</div>
</template>
<script lang="ts" setup name="child">
defineEmits(["update:value"]);
const props = defineProps<{ value: number; value2: { value: number } }>();
console.log(props.value);
</script>
<style scoped></style>
通过emit向父组件传递事件,通过这样的方式实现修改value变量的值,当点击h1标签时,父组件value变量的值会发生相应的变化。
实验一
如果我们在子组件中再次使用v-model绑定到原生组件input上,会发生什么呢?
<template></template>
<div>
<h1 @click="$emit('update:value', Math.random())">{{ value }}</h1>
<h3>{{ value2 }}</h3>
<input type="text" v-model="value" />
</div>
</template>
<script lang="ts" setup name="child">
defineEmits(["update:value"]);
const props = defineProps<{ value: number; value2: { value: number } }>();
console.log(props.value);
</script>
<style scoped></style>
显然是会报错的,因为input展开是这样的
<input
v-bind:value="value"
@input="value= $event.target.value"
/>
报错如下:
v-model cannot be used on a prop, because local prop bindings are not writable.
因为此时还是修改了value的值
我们需要采用最原始的写法了,当触发input事件时需要向父组件emit事件:
<template>
<div>
<h1 @click="$emit('update:value', Math.random())">{{ value }}</h1>
<h3>{{ value2 }}</h3>
<input type="text" :value="value" @input="$emit('update:value', $event.target.value)" />
</div>
</template>
<script lang="ts" setup name="child">
defineEmits(["update:value"]);
const props = defineProps<{ value: number; value2: { value: number } }>();
console.log(props.value);
</script>
<style scoped></style>
效果如图,在input中输入值父组件和子组件的值都会更新:
实验二
如果子组件中继续嵌套一个组件,将value值继续传递给子组件,此时使用v-model情况是怎样的,会不会报错,会不会直接更新第一层父组件的value?
定义一个grandchild组件
<template>
<div>
{{ value }}
</div>
</template>
<script lang="ts" setup>
type prop = {
value: string;
};
const props = defineProps<prop>();
</script>
<style scoped></style>
在子组件中引入
<template>
<div>
<h1 @click="$emit('update:value', Math.random())">{{ value }}</h1>
<h3>{{ value2 }}</h3>
<input type="text" :value="value" @input="handleInput($event)" />
<grand-child v-model:value="value"></grand-child>
</div>
</template>
<script lang="ts" setup name="child">
import grandChild from "./grandchild.vue";
const emit = defineEmits(["update:value"]);
const props = defineProps<{ value: string; value2: { value: number } }>();
console.log(props.value);
const handleInput = event => {
emit("update:value", event.target.value);
console.log(event.target.value);
};
</script>
<style scoped></style>
此时发生报错
v-model cannot be used on a prop, because local prop bindings are not writable.
Use a v-bind binding combined with a v-on listener that emits update:x event instead.
prop是不能被修改的
那如何解决呢,有两种方法
方法一 定义emit事件
用emit方法向上传递,grandchild组件修改如下
<template>
<div @click="$emit('handleClick', Math.random() + 1)">grand-child: {{ value }}</div>
</template>
<script lang="ts" setup>
defineEmits(["handleClick"]);
type prop = {
value?: string;
value2?: { value: number };
};
const props = defineProps<prop>();
console.log(props.value);
</script>
<style scoped></style>
child组件修改如下:
<template>
<div>
<h1 @click="$emit('update:value', Math.random())">{{ value }}</h1>
<h3>{{ value2 }}</h3>
<input type="text" :value="value" @input="handleInput($event)" />
<grand-child :value="value" @handle-click="newValue => $emit('update:value', newValue)"></grand-child>
</div>
</template>
<script lang="ts" setup name="child">
import grandChild from "./grandchild.vue";
const emit = defineEmits(["update:value"]);
const props = defineProps<{ value: string; value2: { value: number } }>();
console.log(props.value);
const handleInput = event => {
emit("update:value", event.target.value);
console.log(event.target.value);
};
</script>
<style scoped></style>
一层层向上emit,直到到达父组件更新value
方法二 使用计算属性
修改child组件中的value为计算属性,<grand-child v-model:value="value"></grand-child>
使用v-model就不会报错:
<template>
<div>
<h1 @click="$emit('update:value', Math.random())">{{ value }}</h1>
<h3>{{ value2 }}</h3>
<input type="text" :value="value" @input="handleInput($event)" />
<grand-child v-model:value="value"></grand-child>
</div>
</template>
<script lang="ts" setup name="child">
import { computed } from "vue";
import grandChild from "./grandchild.vue";
const emit = defineEmits(["update:value"]);
const props = defineProps<{ value: string; value2: { value: number } }>();
console.log(props.value);
const handleInput = event => {
emit("update:value", event.target.value);
console.log(event.target.value);
};
const value = computed({
get() {
return props.value;
},
set(newValue: string) {
emit("update:value", newValue);
}
});
</script>
<style scoped></style>
接着grandchild中便可以继续使用update:value事件完成修改value的值
<template>
<div @click="$emit('update:value', Math.random() + 10)">grand-child: {{ value }}</div>
</template>
<script lang="ts" setup>
defineEmits(["handleClick", "update:value"]);
type prop = {
value?: string;
value2?: { value: number };
};
const props = defineProps<prop>();
console.log(props.value);
</script>
<style scoped></style>
效果如图,触发grandchild的click事件,父组件中的value实现更新,再一次使用了语法糖v-model
实际使用场景 el-dialog的二次封装
对于el-dialog的数值绑定,官方给出的是v-model,即el-dialog子组件可能也要修改dialogVisible的值(可能是视图上的×图标)
<el-dialog
v-model="dialogVisible">
</el-dialog>
但是如果此时我们对el-dialog进行了二次封装,即dialogVisible值是来自于父组件的prop,此时我们如果直接绑定的话便会报错Unexpected mutation of “XXX” prop vue/no-mutating-props,因此可以考虑使用方法二将其转换为计算属性,便不会再报错