Vue3 零基础每日笔记(027):组件上的 v-model——让自定义组件也能双向绑定

一、前言

026 篇揭底:组件的 v-model 就是 props(modelValue) + emit(update:modelValue) 的语法糖。今天自己动手实现它------让自定义组件像 input 一样能用 v-model。三种姿势全覆盖:经典写法、3.4+ 的 defineModel、多字段 v-model。学会这个,弹窗、输入器、开关类的封装直接降一个难度级。

二、经典写法:手写 modelValue 协议

目标效果:父组件 <MyInput v-model="msg" />,子组件输入框打字,msg 跟着变。

按"协议"拆解,Vue 编译器会把上面那行展开成:

vue 复制代码
<MyInput :model-value="msg" @update:model-value="(v) => msg = v" />

所以子组件只要满足两条:接收 modelValue prop + 触发 update:modelValue 事件

vue 复制代码
<!-- src/components/MyInput.vue -->
<template>
  <input
    :value="modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

<script setup>
defineProps({
  modelValue: { type: String, default: '' }
})
const emit = defineEmits(['update:modelValue'])
</script>
vue 复制代码
<!-- App.vue -->
<template>
  <MyInput v-model="msg" />
  <p>父组件实时收到:{{ msg }}</p>
</template>

<script setup>
import { ref } from 'vue'
import MyInput from './components/MyInput.vue'

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

逐点核对(都是新手易错处):

  • 子组件 input 用的是 :value + @input,不是 v-model(在 prop 上直接用 v-model 等于子组件改 props,违反单向流)
  • prop 名必须是 modelValue ,事件名必须是 update:modelValue------写错任何一半,v-model 失灵
  • 打字 → emit 上报 → 父组件回写 msg → props 流回来------数据依旧只被父组件修改,单向流没破

【截图位置:子组件输入框打字,父组件文字实时同步的效果】

三、defineModel:3.4+ 的官方简化(推荐)

上面那套"协议样板代码"太啰嗦,Vue 3.4 推出 defineModel 宏,一步到位:

vue 复制代码
<!-- src/components/MyInput.vue(defineModel 版) -->
<template>
  <!-- defineModel 返回一个"模型 ref",直接 v-model 绑它 -->
  <input v-model="model" />
</template>

<script setup>
const model = defineModel()
</script>

父组件用法一模一样<MyInput v-model="msg" />

defineModel 编译后自动生成完整的"props + emit + 回写"逻辑,model 是个特殊 ref:

  • model.value = xxx → 自动向父组件 emit 更新
  • 父组件数据变 → model.value 自动跟上

还能带默认值和校验:

js 复制代码
const model = defineModel({
  default: '',
  required: false,
  validator: (v) => v.length <= 20
})

注意:这是 Vue 3.4+ 的 API,老项目(3.3 及以下)只能用第二节的手写版。两种都要认识------读老代码时手写版无处不在。

四、多字段 v-model:一个组件绑多个值

弹窗组件最常见的诉求:标题、内容、开关都想双向绑定。v-model 支持带参数,一套协议用多份:

vue 复制代码
<!-- src/components/GoodsEditor.vue -->
<template>
  <div class="editor">
    <input
      :value="name"
      @input="emit('update:name', $event.target.value)"
      placeholder="商品名"
    />
    <input
      :value="price"
      @input="emit('update:price', $event.target.value)"
      placeholder="价格"
    />
  </div>
</template>

<script setup>
defineProps({
  name: { type: String, default: '' },
  price: { type: [Number, String], default: '' }
})
const emit = defineEmits(['update:name', 'update:price'])
</script>
vue 复制代码
<!-- App.vue -->
<template>
  <!-- v-model:参数名,参数名对应子组件的 prop 名 -->
  <GoodsEditor v-model:name="goods.name" v-model:price="goods.price" />
  <p>{{ goods }}</p>
</template>

<script setup>
import { reactive } from 'vue'
import GoodsEditor from './components/GoodsEditor.vue'

const goods = reactive({ name: '', price: '' })
</script>

规则直白:v-model:name 等价于 :name + @update:name。经典版和 defineModel 版都支持参数:const name = defineModel('name')

五、加分项:自定义 v-model 修饰符

还记得 020 篇的 .trim/.number 吗?自己的组件也能支持修饰符。配合 defineModel 的 get/set 写法:

vue 复制代码
<!-- src/components/CapInput.vue -->
<template>
  <input v-model="model" placeholder="自动首字母大写" />
</template>

<script setup>
// 第一个返回值是模型 ref,第二个是修饰符对象
const [model, modifiers] = defineModel({
  set(value) {
    if (modifiers.capitalize) {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
    return value
  }
})
</script>
vue 复制代码
<!-- App.vue -->
<template>
  <CapInput v-model.capitalize="text" />
  <p>{{ text }}</p>
</template>

<script setup>
import { ref } from 'vue'
import CapInput from './components/CapInput.vue'

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

输入 vue3,回写给父组件的就是 Vue3.capitalize 这个修饰符名完全自定义,modifiers 对象里能收到 capitalize: true。面试被问"组件的 v-model 修饰符怎么实现",答这个就是满分。

【截图位置:输入 vue3 后父组件显示 Vue3 的运行效果】

六、踩坑记录

  1. prop/事件名对不上协议 :prop 写了 value(Vue2 习惯)、事件写 update:value,模板 v-model 却默认找 modelValue------名字必须严格是 modelValue / update:modelValue(或带参数版)
  2. 子组件里对 prop 直接 v-model<input v-model="modelValue" /> 等于在改 props,警告 + 失灵。要么 :value + @input 上报,要么用 defineModel 的 model ref
  3. 老版本项目用 defineModel 报错:defineModel 需要 Vue 3.4+,先查 package.json 的 vue 版本
  4. defineModel 解构丢响应式const { value } = defineModel() ❌------它返回的 ref 不能解构,整个用
  5. 多字段 v-model 参数用了驼峰又写 kebabv-model:firstName 与子组件 prop firstName 匹配;写成 v-model:first-name 也行(自动转换),但同一处代码风格要统一

七、今日小结

  • 组件 v-model 协议:modelValue prop 下行 + update:modelValue 事件上行
  • defineModel(3.4+):一个宏代替整套样板代码,返回可读可写的模型 ref
  • 多字段:v-model:参数名,prop 与事件同名不同参
  • 修饰符自定义:defineModel 的 [model, modifiers] + set 钩子

下篇预告

props 要一层层传,隔了五层的组件也要中转,"层层透传"把中间组件全污染了。下一篇 028 provide / inject------祖传数据的"任意门",跨层级通信的正确打开方式。

相关推荐
云浪1 小时前
从 0 手写一个 MCP Server:让 Copilot 调用 Tool 完成四则运算
javascript·node.js·mcp
自不量力的A同学1 小时前
xbatis 1.10.9 正式发布
笔记
萧行之3 小时前
可视化技术复盘:从图形语法看 D3、Vega-Lite、ECharts、Observable Plot
前端·学习·数据可视化
尾善爱看海10 小时前
Vue 面试收官篇:SSR、性能优化落地、30 道高频面试题精讲(附标准答案)
前端·javascript·vue.js·面试·vue
weixin_BYSJ198711 小时前
【计算机毕设】基于SpringBoot与Vue的文物保护档案管理系统08621
vue.js·spring boot·spring cloud·微服务·架构·django·课程设计
老当益壮梁奶奶11 小时前
51单片机DS18B20温度传感器详解:单总线时序与C51驱动实现
笔记·stm32·单片机·嵌入式硬件·51单片机
Bs_MoneyMagnet11 小时前
基于springboot+vue的个人健康管理系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·vue3·springboot3·计算机毕业设计
驳是11 小时前
一个组件,提升 react-router 开发的幸福感
前端·preact
GreenTea11 小时前
OpenAI Agents API 上手实测:一次调用把整个 agent loop 甩给 OpenAI
前端·后端·算法