Vue3里defineModel和ref咋配合用?常见疑问一次讲透
最近好多同学问Vue3里defineModel和ref的用法,尤其是它们咋配合实现组件通信、双向绑定这些场景,今天挑几个高频疑问,结合实际代码例子掰碎了讲~
defineModel是干啥的?和ref有啥关联?
先理解 defineModel :它是Vue3.4版本后新增的“语法糖”,专门简化「自定义组件双向绑定(v-model)」的写法,以前写自定义v-model,得手动声明props接收值,再emit事件通知父组件更新;现在用defineModel,一行代码就能同时处理“接收父值+触发更新”。
那和 ref 的关联在哪?Vue内部实现里,defineModel本质是帮我们自动创建了一个「响应式的ref」,还偷偷处理了props和emit的联动,比如子组件写 const modelRef = defineModel(),这个modelRef既是响应式(和ref一样能触发视图更新),又会自动同步父组件v-model传的值,修改它时还能自动触发emit通知父组件,可以说,defineModel是“基于ref封装的、专门解决双向绑定的工具”。
用defineModel时,咋处理父组件传的v-model值?和自己声明ref有啥区别?
举个实际场景:父组件用 <Child v-model="parentValue" /> 传值,子组件接收时——
-
用
defineModel:const childValue = defineModel(),这时候childValue直接对应父组件的parentValue,子组件修改childValue(比如childValue.value = 10),父组件的parentValue会自动更新,因为defineModel内部帮我们做了emit(触发update:modelValue事件)。 -
自己声明
ref:const localRef = ref(0),这时候localRef是子组件的“局部状态”,和父组件完全没关系,想让父组件更新,得手动写emit('update:modelValue', localRef.value),代码多了一步,还容易忘。
总结区别:defineModel的ref是“双向绑定的桥梁”,自己声明的ref是“子组件内部状态”,前者天生和父组件v-model联动,后者只管自己。
子组件里,defineModel的变量能和自己的ref联动不?
必须能!因为defineModel返回的是ref,和自己用ref声明的变量“血统一样”,都是响应式数据,举个需求:子组件有个输入框,要同时同步父组件v-model的值,还要有自己的临时状态(比如输入时防抖)。
代码例子:
<script setup>
import { ref, watch, defineModel } from 'vue'
// 父组件v-model绑定的值,用defineModel接收
const parentValue = defineModel()
// 子组件自己的局部ref
const localInput = ref(parentValue.value)
// 当localInput变化时,同步更新parentValue(触发父组件更新)
watch(localInput, (newVal) => {
parentValue.value = newVal
})
// 反过来,父组件传值变化时,更新localInput
watch(parentValue, (newVal) => {
localInput.value = newVal
})
</script>
<template>
<input v-model="localInput" />
</template>
这里两个ref(parentValue是defineModel生成的,localInput是自己声明的)通过watch互相监听,实现“父传子+子改父+子内部临时状态”的复杂联动。
父组件v-model + 子组件defineModel,和传统props+emit比有啥优势?
传统写法(Vue3.4前很常见):
<!-- 子组件 -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const handleChange = (val) => {
emit('update:modelValue', val)
}
</script>
<template><input :value="modelValue" @input="handleChange" /></template>
用defineModel后:
<!-- 子组件 --> <script setup> const modelValue = defineModel() </script> <template><input v-model="modelValue" /></template>
优势肉眼可见:
- 代码量暴减:从“声明
props+emit+手动触发”变成“一行defineModel”,少了至少3行代码。 - 逻辑更内聚:双向绑定的逻辑被封装到
defineModel里,不用关心emit事件名(比如update:modelValue这种约定式命名),减少记忆成本。 - 响应式更自然:
defineModel返回的ref,和普通ref用法完全一致(改value、用v-model绑定),学习成本低。
defineModel支持多个v-model绑定不?咋配置不同参数名?
支持!比如父组件想同时双向绑定“用户名”和“密码”,可以这么写:
父组件:
<Child v-model:username="userName" v-model:password="userPwd" />
子组件接收时,用defineModel声明不同的“参数名”:
<script setup>
const username = defineModel('username') // 对应v-model:username
const password = defineModel('password') // 对应v-model:password
</script>
<template>
<input v-model="username" placeholder="用户名" />
<input v-model="password" placeholder="密码" type="password" />
</template>
每个defineModel的参数,对应父组件v-model后的“修饰符名”(比如username password),子组件里每个defineModel返回的ref,各自独立管理双向绑定,互不干扰。
组合式API和选项式API里,defineModel用法有啥不一样?
先明确:defineModel是「组合式API」的语法糖,只在<script setup>或组合式API的setup函数里能用。
选项式API(非setup语法)里,想实现自定义v-model,得用传统方式:
<script>
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
methods: {
handleInput(val) {
this.$emit('update:modelValue', val)
}
}
}
</script>
<template><input :value="modelValue" @input="handleInput" /></template>
而组合式API(尤其是<script setup>)里,用defineModel直接起飞:
<script setup> const modelValue = defineModel() </script> <template><input v-model="modelValue" /></template>
简单说:选项式API还得手动写props+emit,组合式API用defineModel把这些“重复活”全自动化了。
defineModel的类型咋定义?和ref的泛型有啥关系?
如果用TypeScript,defineModel支持泛型传参,和ref的类型定义逻辑一致,比如父组件传的是数字类型,子组件可以这么写:
<script setup lang="ts">
// 声明modelValue的类型为number
const modelValue = defineModel<number>()
// 或者指定默认值时,同时定类型
const count = defineModel<number>('count', { default: 0 })
</script>
这么做的好处是:父组件传值类型不对时,TypeScript会直接报错,避免运行时bug,而且defineModel返回的ref,类型和你指定的泛型一致(比如上面的modelValue是Ref<number | undefined>,count是Ref<number>,因为给了默认值)。
父组件没传v-model,defineModel的默认值咋设置?
和props的default选项类似,defineModel支持传一个配置对象,指定默认值。
<script setup>
// 父组件没传v-model时,modelValue默认是空字符串
const modelValue = defineModel('modelValue', { default: '' })
</script>
这样即使父组件没写<Child v-model="xxx" />,子组件里modelValue的初始值也会是,避免undefined导致的渲染问题。
实战小案例:串起defineModel和ref的用法
需求:做一个带“步进器”的输入框组件,父组件通过v-model绑定数值,子组件用defineModel接收,同时支持“+”“-”按钮修改值。
子组件代码:
<template>
<div class="stepper">
<button @click="modelValue.value--">-</button>
<input type="number" v-model="modelValue" />
<button @click="modelValue.value++">+</button>
</div>
</template>
<script setup>
import { defineModel } from 'vue'
// 接收父组件v-model的值,默认值设为0
const modelValue = defineModel('modelValue', { default: 0 })
</script>
<style scoped>
.stepper { display: flex; align-items: center; }
button { padding: 4px 12px; }
input { width: 60px; text-align: center; }
</style>
父组件使用:
<template>
<div>
<p>父组件绑定的值:{{ num }}</p>
<Stepper v-model:modelValue="num" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import Stepper from './Stepper.vue'
const num = ref(5) // 初始值5
</script>
运行后会发现:父组件的num和子组件的modelValue完全同步,点“+”“-”或输入框,两边值都会变,这里defineModel既处理了父传子(num→modelValue),又处理了子改父(modelValue变化时自动emit更新num),还能设置默认值,代码简洁到飞起~
总结下,defineModel是Vue3对“组件双向绑定”的一次大简化,而它的底层离不开ref的响应式能力,理解两者的关系后,写自定义v-model组件时,代码量能少一半,逻辑还更清晰,要是你之前被props+emit绕晕过,现在用defineModel绝对能爽到~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


