Code前端首页关于Code前端联系我们

Vue3里defineModel和ref咋配合用?常见疑问一次讲透

terry 20小时前 阅读数 392 #Vue
文章标签 defineModel ref

最近好多同学问Vue3里defineModelref的用法,尤其是它们咋配合实现组件通信、双向绑定这些场景,今天挑几个高频疑问,结合实际代码例子掰碎了讲~

defineModel是干啥的?和ref有啥关联?

先理解 defineModel :它是Vue3.4版本后新增的“语法糖”,专门简化「自定义组件双向绑定(v-model)」的写法,以前写自定义v-model,得手动声明props接收值,再emit事件通知父组件更新;现在用defineModel,一行代码就能同时处理“接收父值+触发更新”。

那和 ref 的关联在哪?Vue内部实现里,defineModel本质是帮我们自动创建了一个「响应式的ref」,还偷偷处理了propsemit的联动,比如子组件写 const modelRef = defineModel(),这个modelRef既是响应式(和ref一样能触发视图更新),又会自动同步父组件v-model传的值,修改它时还能自动触发emit通知父组件,可以说,defineModel是“基于ref封装的、专门解决双向绑定的工具”。

用defineModel时,咋处理父组件传的v-model值?和自己声明ref有啥区别?

举个实际场景:父组件用 <Child v-model="parentValue" /> 传值,子组件接收时——

  • defineModelconst childValue = defineModel(),这时候childValue直接对应父组件的parentValue子组件修改childValue(比如childValue.value = 10),父组件的parentValue会自动更新,因为defineModel内部帮我们做了emit(触发update:modelValue事件)。

  • 自己声明refconst localRef = ref(0),这时候localRef是子组件的“局部状态”,和父组件完全没关系,想让父组件更新,得手动写emit('update:modelValue', localRef.value),代码多了一步,还容易忘。

总结区别:defineModelref是“双向绑定的桥梁”,自己声明的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>

这里两个refparentValuedefineModel生成的,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,类型和你指定的泛型一致(比如上面的modelValueRef<number | undefined>countRef<number>,因为给了默认值)。

父组件没传v-model,defineModel的默认值咋设置?

propsdefault选项类似,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既处理了父传子(nummodelValue),又处理了子改父(modelValue变化时自动emit更新num),还能设置默认值,代码简洁到飞起~

总结下,defineModel是Vue3对“组件双向绑定”的一次大简化,而它的底层离不开ref的响应式能力,理解两者的关系后,写自定义v-model组件时,代码量能少一半,逻辑还更清晰,要是你之前被props+emit绕晕过,现在用defineModel绝对能爽到~

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门