【vue3】v-model 的 “新玩法“

发布于:2025-08-14 ⋅ 阅读:(25) ⋅ 点赞:(0)

前言

Vue3.6 已经进入 Alpha 阶段,Vue3.4 早在 2023 年 12 月就把 defineModel 转正式了!

defineModel 是 Vue 3.3+ 中引入的一个编译器宏(Compile-time Macro),用于简化组件中 v-model的双向绑定实现。它解决了传统 v-model 实现中需要手动声明 props 和 emit 的繁琐问题,让代码更简洁。

defineModel宏简化了这一过程。它会返回一个ref,这个ref可以像普通的数据ref一样使用,但会自动处理prop和事件。

自动实现 v-model 的双向绑定

声明一个响应式的 ref;自动注册对应的 prop(如 modelValue);自动注册对应的事件(如 update:modelValue

defineModel 是什么

一句话定义:让子组件像原生 <input> 一样直接支持 v-model 的语法糖; 说白了就是一个 宏(macro),在编译期把 defineModel() 展开成 props + emit

特性:

1. 自动连接prop和事件:内部自动声明了`modelValue` prop和`update:modelValue`事件。

2. 支持修饰符:可以处理`v-model`的修饰符(如`.trim`, `.number`)。

如果父组件使用修饰符,例如`v-model.trim`,那么修饰符会以对象的形式传递给`defineModel`的第二个参数。我们可以根据修饰符对值进行处理。

3. 支持多个`v-model`:通过指定参数,如`defineModel('foo')`,可以用于多个`v-model`绑定(例如`v-model:foo`)。

宏 VS 函数

  • :编译期代码生成,运行时 0 额外开销。

  • 函数:运行时真实调用。
    因此 defineModel 不需要 import,也不能在普通 <script> 或 .js/.ts 文件里使用。

    // 书写
    const model = defineModel<string>({ default: 'hello' })
    
    // 编译后
    const props = defineProps({
      modelValue: { type: String, default: 'hello' }
    })
    const emit  = defineEmits(['update:modelValue'])
    const model = computed({
      get: () => props.modelValue,
      set: val => emit('update:modelValue', val)
    })

    注意:基于 <script setup>,可直接复制到 *.vue 文件运行。

传统实现 vs defineModel

方式 传统实现 defineModel
代码量 需要手动声明 props + emit + 事件处理 一行声明
数据更新 需调用 emit('update:modelValue', value) 直接给 ref 赋值
修饰符 需通过 props.modelModifiers 手动处理 在 setter 中自动处理
类型安全 需额外类型声明 支持泛型类型

单 v-model 

父组件

<template>
  <UserName v-model="name" />
  <p>父组件拿到的值:{{ name }}</p>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import UserName from './Names.vue'

const name = ref('名称')
</script>

子组件 Names.vue

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

<script setup lang="ts">
const modelValue = defineModel<string>()
// 等价于 const modelValue = defineModel<string>({ required: true })
</script>

多个 v-model 

父组件

<template>
  <UserForm
    v-model:name="form.name"
    v-model:age="form.age"
    v-model:phone="form.phone"
  />
  <pre>{{ form }}</pre>
</template>

<script setup lang="ts">
import { reactive } from 'vue'
import UserForm from './UserForm.vue'

const form = reactive({
  name: '张三',
  age: 18,
  phone: '13800138000'
})
</script>

子组件
 

<template>
  <input v-model="name" placeholder="姓名" />
  <input v-model="age"   placeholder="年龄" />
  <input v-model="phone" placeholder="手机号" />
</template>

<script setup lang="ts">
const name  = defineModel<string>('name')
const age   = defineModel<number>('age')
const phone = defineModel<string>('phone')
</script>

带修饰符 & 转换器

不用手动 .trim

父组件

<template>
  <TrimInput v-model.trim="keyword" />
  <p>父组件值:{{ keyword }}</p>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import TrimInput from './TrimInput.vue'

const keyword = ref('')
</script>

子组件TrimInput.vue

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

<script setup lang="ts">
const [modelValue, modifiers] = defineModel<string, 'trim'>()

// 当父组件写 v-model.trim 时,modifiers.trim === true
if (modifiers.trim) {
  // 通过 set 函数实时转换
}
</script>

实时转换,用 get / set

const [modelValue, modifiers] = defineModel<string, 'trim'>({
  set(val) {
    return modifiers.trim ? val.trim() : val
  }
})

TypeScript 高阶

需求

写法

必填

defineModel<string>({ required: true })

可选+默认值

defineModel<string>({ default: '张三' })

联合类型

defineModel<'male' | 'female'>()

复杂对象

defineModel<User>()

注意:默认值如果是对象/数组,请用函数返回新实例,避免引用共享:

defineModel<string[]>({ default: () => ['A', 'B'] })

注意事项

版本要求:Vue ≥ 3.3,且需配置构建工具:

// vite.config.js
export default {
  plugins: [
    vue({
      script: {
        defineModel: true // 启用宏
      }
    })
  ]
}

本质是语法糖:编译后会展开为:

// defineModel() 编译结果
const modelValue = defineModel()
// ↓ 编译后 ↓
const modelValue = ref(props.modelValue)
watch(modelValue, (v) => emit('update:modelValue', v))

修饰符处理:当父组件使用 v-model.trim 时,子组件可通过 defineModel 的 set 选项处理。

总结

defineModel 是 Vue 组件双向绑定的趋势,它:

✅ 减少样板代码(无需手动处理 props/emit)

✅ 提供更直观的响应式体验

✅ 完美支持 TypeScript

✅ 简化修饰符处理


网站公告

今日签到

点亮在社区的每一天
去签到