Vue 3 defineModel 翻车实录:多个 v-model 绑定到底怎么写?

问题场景

同事小张从 Vue 2 迁移到 Vue 3 + Composition API,遇到一个常见需求:一个表单组件需要同时绑定「标题」「内容」「开关状态」三个数据。

Vue 2 时代他这样写:

vue 复制代码
<!-- 父组件 -->
<MyForm
  :title.sync="title"
  :content.sync="content"
  :visible.sync="visible"
/>

Vue 3 里 .sync 被废了,官方文档说用 v-model 传多个绑定。小张开写:

vue 复制代码
<!-- 父组件 -->
<MyForm
  v-model:title="title"
  v-model:content="content"
  v-model:visible="visible"
/>

子组件直接套用旧写法:

vue 复制代码
<script setup>
const props = defineProps({
  title: String,
  content: String,
  visible: Boolean,
})
const emit = defineEmits(['update:title', 'update:content', 'update:visible'])
</script>

结果跑起来没问题,但被 Code Review 打回来 ------ 你在 defineProps 里声明了默认值吗?数据流清晰吗?这里有个潜在的双向绑定「黑洞」问题。

原因分析

1. defineModel 的正确用法

Vue 3.4+ 推出了 defineModel 宏,专治 v-model 多绑定场景。先看一个简单版:

vue 复制代码
<script setup>
// 默认 v-model
const model = defineModel()
// 等价于:
// const props = defineProps({ modelValue: String })
// const emit = defineEmits(['update:modelValue'])
</script>

但大多数人不知道的是:defineModel 返回的是个 ref,可以加配置,还能定义多个绑定

2. 没有使用 defineModel 的隐患

小张那种手动 defineProps + emit 写法在简单场景没问题,但在这些场景下会翻车:

  • 双向绑定的默认值defineProps 声明 default 后,父组件首次没传值时,子组件直接改 props 会触发「不期望的突变」警告
  • 深层对象双向绑定v-model:config="config" 时,子组件改了嵌套属性,父组件不感知
  • TS 推导丢失:手动声明类型容易和运行时行为不一致

解决方案:使用 defineModel 重构

第一步 :用 defineModel 替代手动 defineProps + emit

vue 复制代码
<!-- MyForm.vue -->
<script setup lang="ts">
// 每个 v-model 对应一个 defineModel
const title = defineModel<string>('title', { required: true })
const content = defineModel<string>('content', { default: '' })
const visible = defineModel<boolean>('visible', { default: false })
</script>

<template>
  <div>
    <input v-model="title" />
    <textarea v-model="content" />
    <Switch :model-value="visible" @update:model-value="val => visible = val" />
  </div>
</template>

第二步 :在模板里直接用 v-model 绑定 defineModel 返回的 ref

特别注意:defineModel 返回的是可写的 ref ,在模板里可以直接写 v-model="title",不需要再写 :value="title" @input="..."

第三步:给每个 defineModel 指定类型和默认值

typescript 复制代码
// 带类型 + 转换器
const count = defineModel<number>('count', {
  default: 0,
  // 自定义转换:始终读成 number
  set(value: string | number) {
    return Number(value) || 0
  }
})

避坑指南

🔴 坑 1:defineModel 的名字要和 v-model 绑定对上

父组件写了 v-model:title="title",子组件里必须用 defineModel('title')名字完全一致。少一个字母都不行。

🔴 坑 2:不要混用 defineModel 和手动 props

vue 复制代码
<script setup>
// ❌ 这样会冲突
const props = defineProps({ title: String })
const titleModel = defineModel('title')

// ✅ 选一个:全用 defineModel
const title = defineModel('title')
</script>

🔴 坑 3:复杂对象的深拷贝问题

v-model 绑定一个对象时,子组件直接改对象属性不会触发父组件更新。正确做法:

vue 复制代码
<script setup>
const config = defineModel<{ theme: string; fontSize: number }>('config', {
  default: () => ({ theme: 'light', fontSize: 14 }),
})

function updateTheme(theme: string) {
  // ✅ 必须重新赋值(触发 setter)
  config.value = { ...config.value, theme }
}
</script>

🔴 坑 4:类型定义用泛型

typescript 复制代码
// ✅ 推荐:显式类型
const title = defineModel<string>('title')

// ❌ 不推荐:类型推断有时不准
const title = defineModel('title')

要点总结

场景 推荐写法
单个 v-model const model = defineModel()
多个 v-model const title = defineModel('title')
带默认值 defineModel('x', { default: 0 })
带类型 defineModel<string>('x')
自定义 setter defineModel('x', { set(val) { ... } })

一句话记忆 :Vue 3.4+ 写子组件双向绑定,别再手写 props + emitdefineModel 一键搞定,还自带类型推导和默认值支持。

相关推荐
甲维斯1 小时前
坦克大战测试全翻车了!豆包,DeepSeek,Qwen,GPT,Claude
前端·人工智能·游戏开发
乘风gg2 小时前
还在养虾吗?虾王已诞生:微信龙虾 ClawBot
前端·ai编程·claude
小小小小宇2 小时前
LLM 长期记忆构建
前端
lichenyang4533 小时前
从 Express 老项目到 NestJS + Docker:一次车辆管理系统的渐进式重构
前端
Momo__4 小时前
VueUse createReusableTemplate —— 单文件组件内的模板复用神器
前端·vue.js
程序员小富4 小时前
我开源了一个开发者专属的智能 JSON 工具,得到了媳妇高度认可
前端·vue.js·后端
小小小小宇4 小时前
程序员如何给 LLM 装工具以及看懂推理过程
前端
写代码的皮筏艇4 小时前
React中的forwardRef
前端·react.js·面试
槑有老呆4 小时前
花三个月工资请了个 AI 程序员,结果它连青岛啤酒股价都查不了
前端