一、前言
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 的运行效果】
六、踩坑记录
- prop/事件名对不上协议 :prop 写了
value(Vue2 习惯)、事件写update:value,模板 v-model 却默认找 modelValue------名字必须严格是 modelValue / update:modelValue(或带参数版) - 子组件里对 prop 直接 v-model :
<input v-model="modelValue" />等于在改 props,警告 + 失灵。要么:value + @input上报,要么用 defineModel 的 model ref - 老版本项目用 defineModel 报错:defineModel 需要 Vue 3.4+,先查 package.json 的 vue 版本
- defineModel 解构丢响应式 :
const { value } = defineModel()❌------它返回的 ref 不能解构,整个用 - 多字段 v-model 参数用了驼峰又写 kebab :
v-model:firstName与子组件 propfirstName匹配;写成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------祖传数据的"任意门",跨层级通信的正确打开方式。