什么是 defineModel

  • 一句话定义defineModel是 Vue 3.3 新增的宏(macro),用于在组件中快速声明一个可以与父组件通过v-model双向绑定的模型。

  • 解决的问题:传统自定义 v-model 需要手动自定义 props emit,代码冗余

  • 核心优势:

    • 减少样板代码(无需手动写 propsemit)。

    • 支持 TypeScript 类型推导。

    • 兼容 Vue 2 的 v-model 行为(默认 modelValue)。


传统自定义 v-model 写法

父组件的写法

<template>
  <div>
    <h2>父组件</h2>
    <p>父组件的值:{{ state }}</p>
    <Child v-model="state" />
  </div>
</template>

<script setup>
import { ref } from 'vue';
import Child from './Child.vue';

const state = ref(0);
</script>

子组件的写法

<template>
  <div>
    <h3>子组件</h3>
    <input
      type="text"
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
    />
    <p>子组件的值:{{ modelValue }}</p>
  </div>
</template>

<script setup>
import { defineProps, defineEmits } from 'vue';

const props = defineProps({
  modelValue: String
});

const emit = defineEmits(['update:modelValue']);
</script>

defineModel 的写法

父组件的写法

<template>
  <div>
    <h2>父组件</h2>
    <p>父组件的值:{{ state }}</p>
    <Child v-model="state" />
  </div>
</template>

<script setup>
import { ref } from 'vue';
import Child from './Child.vue';

const state = ref(0);
</script>

子组件的写法

<template>
  <div>
    <h3>子组件</h3>
    <input
      type="text"
      :value="modelValue"
      @input="(e) => (model = e.target.value)"
    />
    <p>子组件的值:{{ modelValue }}</p>
  </div>
</template>

<script setup>
import { defineModel } from 'vue';
const value = defineModel();
</script>

从子组件的对比写法来看,代码量减少了,并且导入依赖也变少了

defineModel 的扩展

内置修饰符

Vue 的 v-model 默认支持 .lazy.trim 等修饰符,通过 defineModel 可以自动继承这些行为

<!-- 父组件 -->
<CustomInput v-model.trim="username" />

子组件无需额外处理,defineModel 会自动将修饰符应用到模型值上


自定义修饰符

defineModel 允许通过 modifiers 选项声明自定义修饰符,并在逻辑中处理

eg:实现一个 .capitalize 修饰符(自动首字母大写)

<!-- 子组件 CapitalizedInput.vue -->
<script setup>
const model = defineModel({
  // 声明支持的修饰符
  modifiers: { capitalize: false }
})

watch(model, (value) => {
  if (model.modifiers.capitalize) {
    // 应用修饰符逻辑
    model.value = value.charAt(0).toUpperCase() + value.slice(1)
  }
})
</script>

<template>
  <input v-model="model" />
</template>

父组件中应用

<template>
  <!-- 应用自定义修饰符 -->
  <CapitalizedInput v-model.capitalize="username" />
</template>

  • 子组件通过 modifiers 选项声明可接受的修饰符。

  • 父组件通过 v-model.modifierName 传递修饰符。

  • 在子组件中通过 model.modifiers.modifierName 访问修饰符状态。


转换器:数据预处理

通过 defineModel getset 函数实现双向数据转换

eg: 将输入值转换为数字类型

<!-- 子组件 NumberInput.vue -->
<script setup>
const model = defineModel({
  // 类型声明为 Number
  type: Number,
  // 转换器
  get(value) {
    // 从父组件接收的值(已确保是 Number)
    return value
  },
  set(value) {
    // 向父组件发送前,将字符串转为 Number
    return Number(value)
  }
})
</script>

<template>
  <input 
    type="number" 
    :value="model" 
    @input="(e) => (model = e.target.value)"
  >
</template>

注意事项

  1. 修饰符命名冲突:避免与 Vue 内置修饰符(如 .lazy)重名。

  2. 转换器性能:避免在 get/set 中执行高开销计算(如复杂正则)。

文刊借鉴

文章内容有 ai 生成!

https://juejin.cn/post/7331021519965356071https://cn.vuejs.org/api/sfc-script-setup.html#definemodel