一、v-model 传值的核心本质
v-model 是 语法糖,本质是「父组件传值 + 子组件触发更新事件」的组合:
- 对原生表单元素:
v-model="xxx"≈:value="xxx" @input="xxx = $event.target.value" - 对自定义组件:
v-model="xxx"≈:modelValue="xxx" @update:modelValue="xxx = $event"("update:modelValue"仅仅是个事件名而已)
这个规则是所有用法的基础,记住它就能灵活应对各种场景。
二、场景:父子组件的 v-model 传值(核心重点)
1、Vue的单向数据流,禁止子组件直接修改 props(包括用 v-model 绑定)因为props是只读的;
2、子组件不能直接给 props.modelValue 绑 v-model,必须通过「内部变量(ref) / 计算属性(computed)」中转,因为v-model 是 语法糖;
默认传值(modelValue)
js
<!-- 父组件 Father.vue -->
<template>
<input type="number" v-model="msg">
<Child v-model="msg"></Child>
</template>
<script setup>
import { ref } from 'vue'
import Child from './components/test.vue'
const msg = ref(100)
// 仿照接口的异步数据
setTimeout(() => {
msg.value = 2000
}, 1000)
</script>
js
<!-- 子组件 Child.vue -->
<template>
<input
v-model="innerValue"
/>
</template>
<script setup>
import { ref, watch } from 'vue';
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const innerValue = ref(props.modelValue)
// 监听外部变量父组件props变化 → 同步到本地变量(父 → 子)
// immediate: true 确保初始化时也能触发(如果是异步数据必须加,不然初始化数据一直是100,同步数据可不加)
watch(() => props.modelValue, (newVal) => {
innerValue.value = newVal
}, { immediate: true })
// 监听本地变量子组件的数据变化 → 触发emit通知父组件(子 → 父)
watch(innerValue, (newVal) => {
emit('update:modelValue', newVal)
})
</script>
Tips: 因为仿照接口的异步数据,所以16-18行代码必须加),通过v-model和watch的结合,父子组件的值,双向绑定效果就出来了。
js
<!-- 父组件 Father.vue -->
<template>
<div>
<Test v-model="message"></Test>
<span>父传子:<input type="number" v-model="message"></span>
</div>
</template>
<script setup >
import { ref } from 'vue';
import Test from './components/test.vue'
const message = ref(100);
setTimeout(() => {
message.value = 20000
},1000);
</script>
js
<!-- 子组件 Child.vue -->
<template>
<div>
子传父:<input type="number" v-model="message">
</div>
</template>
<script setup>
import { computed, ref, watch } from 'vue';
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
modelValue: Number
})
const emits = defineEmits(['update:modelValue'])
// 不能直接修改computed的值,会报错:[Vue warn] Write operation failed: computed value is readonly
// const message = computed(() => props.modelValue) // 报错
const message = computed({
get(){
return props.modelValue
},
set(val){
emits('update:modelValue', val)
}
})
</script>
Tips: computed 的 get 方法本身就会监听依赖的响应式变化(包括异步数据),只要依赖变了,computed 的值就会自动更新,所以不用像watch那么复杂监听2次。
js
<!-- 父组件 -->
<template>
<el-button type="primary" @click="openInnerDialog">
Open the inner Dialog
</el-button>
<InnerDialog v-model="innerVisible" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import InnerDialog from './components/test.vue'
const innerVisible = ref(false)
const openInnerDialog = () => {
innerVisible.value = true
}
</script>
js
<!-- 子组件 -->
<template>
<el-dialog
v-model="visible"
width="500"
title="Inner Dialog"
append-to-body
>
<span>This is the inner Dialog</span>
</el-dialog>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
</script>
Tips:弹框组件的常见用法:props和emit(缺点:父组件,每个入口触发 emit)、ref(缺点:父组件直接操作子组件实例,耦合性高,不符合 "单向数据流"、Vue3 中需手动 defineExpose 暴露方法很麻烦)等。