讲解
上一章说过,input 上的 v-model 是 :value + @input 的语法糖。自定义组件也支持 v-model——比如自己封装的搜索框、评分控件、开关——让父组件用起来和原生表单一样顺手。Vue 3.4 之前要用 modelValue prop + update:modelValue 事件手动实现这层糖;3.4 引入 defineModel 宏后,一行就搞定。
defineModel 在子组件里返回一个可读写的 ref:const model = defineModel(),子组件改 model.value 就自动向父组件同步。父组件照旧 <Rating v-model="score">,完全感知不到内部实现。想支持多个 v-model(比如对话框的 visible 和 form 数据分开绑)也简单:defineModel('visible')、defineModel('form') 各自独立,父组件写 v-model:visible、v-model:form。
defineModel 还支持修饰符和变换:defineModel({ type: Number, default: 0 }) 配类型和默认值;get/set 变换可以在父子之间转换数据形态(父存分、子显示元)。什么时候自己封装 v-model 组件?当一个输入控件在项目里出现第三次,或者它内部有复杂交互(搜索联想、级联选择)时,值得封。
示例
手动展开 v-model 的底层协议(modelValue + update:modelValue),理解 defineModel 替你做了什么(在本教程构建时被真实执行):
import assert from 'node:assert/strict';
// 子组件内部:defineModel 展开后就是这层协议
function createRatingComponent(emit) {
let modelValue = 0;
return {
get modelValue() {
return modelValue;
},
setProps(v) {
modelValue = v; // 父组件的 v-model 值流进来
},
rate(star) {
emit('update:modelValue', star); // 子组件改值 = 发更新事件
},
};
}
// 父组件
const parent = { score: 3 };
const child = createRatingComponent((event, value) => {
assert.strictEqual(event, 'update:modelValue');
parent.score = value; // 父组件响应更新事件
});
child.setProps(parent.score);
assert.strictEqual(child.modelValue, 3); // 初始值流下来
child.rate(5); // 用户点了五星
assert.strictEqual(parent.score, 5); // 父组件状态被更新
child.setProps(parent.score);
assert.strictEqual(child.modelValue, 5);
// 多 v-model:visible 和 keyword 独立通道
const dlg = { visible: false, keyword: '' };
function bindModel(obj, key) {
return {
get: () => obj[key],
set: (v) => (obj[key] = v),
};
}
bindModel(dlg, 'visible').set(true);
bindModel(dlg, 'keyword').set('vue');
assert.deepStrictEqual(dlg, { visible: true, keyword: 'vue' });
console.log('评分: 3 → 用户点 5 星 → 父组件 score =', parent.score);
console.log('多 v-model 状态:', JSON.stringify(dlg));
真实组件(Vue 3.4+ 写法):
<script setup>
// Rating.vue
const model = defineModel({ type: Number, default: 0 });
</script>
<template>
<span
v-for="star in 5"
:key="star"
:class="{ on: star <= model }"
@click="model = star"
>
★
</span>
</template>
<script setup>
// Parent.vue
import { ref } from 'vue';
import Rating from './Rating.vue';
const score = ref(3);
</script>
<template>
<Rating v-model="score" />
<p>当前评分:{{ score }} 星</p>
</template>
常见坑
- 还在手写 modelValue/update:modelValue:Vue 3.4+ 直接用 defineModel,老写法只剩维护遗留代码时需要看懂。
- 子组件直接改 modelValue prop:prop 只读,必须通过 emit(或 defineModel 的 ref)写回。
- v-model 和 .value 层级混:defineModel 返回 ref,子组件模板里用
model,JS 里用model.value。 - 给 v-model 传非响应式值:父组件
v-model="score"的 score 必须是 ref/reactive 属性,否则只是单向传值。 - 一个组件塞太多 v-model:超过两三个说明组件职责过重,考虑拆分或改用 props/emits 显式接口。
小结
组件 v-model 的协议是 prop + update 事件,defineModel 一行封装;多通道用 v-model:xxx。下一章讲组件的内容分发:插槽。