跳到正文

defineModel 已经声明了什么:避免重复 v-model 契约和默认值分叉

在 Vue 3.4+ 中,defineModel() 不只是把一段读写代码写短。它已经声明了 model 对应的 prop 和更新事件。迁移旧组件时,如果只加这一个宏,却保留原来的同名 defineProps() 和 defineEmits(),同一个公开契约就有了两份声明。

先明确组件暴露的 model

<script setup lang="ts">
const value = defineModel<string>({ required: true });
const title = defineModel<string>('title', { required: true });
</script>

默认 model 对应 modelValue 与 update:modelValue;命名 model 对应 title 与 update:title。其他名称的 props 和 emits 可以照常声明。defineModel 返回的 ref 则用于组件内部读写。这个展开关系见 Vue 官方文档。

defineModel 同时提供 prop 与更新事件

重复的情况是同时写了 defineModel<string>()、defineProps<{ modelValue: string }>() 和 defineEmits(['update:modelValue'])。读者需要在三个地方判断同一个值的必传、类型和更新行为;迁移过程中,其中一份声明很容易漏改。

重复声明不一定让编译失败

使用 @vue/compiler-sfc 3.5.44 编译上述重复声明,编译可以成功,产物通过 mergeModels 合并 props 与 emits。配套实验保留了可运行的编译入口,可用目标项目自己的版本再验一次。

项目禁止这种写法,是为了减少重复维护,不是为了修复一个必然发生的编译失败。

迁移时按 model 名逐个核对:移除同名 prop 和 update:* 声明,保留其他 props / emits,再检查父组件绑定和测试。不要整块删除已有 defineProps 或 defineEmits。

默认值属于谁

// 子组件
const count = defineModel<number>({ default: 1 });
// 父组件若只写 ref<number>(),初始值仍是 undefined

Vue 文档明确提醒这种默认值可能让父子初始状态不同。子组件有一个可显示的值,不代表父组件已经收到初始化更新。

如果组件本来就是受控输入,优先由父层初始化,再把 model 设为必传。例如父层 ref(1),子层 defineModel<number>({ required: true })。若产品允许未绑定时独立工作,子层默认值也可以成立,但要说明未绑定、传入 undefined、修改后发出事件分别是什么行为。默认值本身是 API 设计,不能一律禁止。

用调用方验证迁移

配套组件将初值放在父层,保留普通 label prop 和独立 submit 事件。编译实验故意包含重复声明,供比较产物;它不是建议复制的业务写法。

读取 https://shengsheng.fun/files/vue-define-model-contract-ownership/kits/define-model-contract/ 的 README.md 和 FILES.json,检查父子调用与编译实验;按当前 Vue 版本迁移 model 契约。

完整实现与接入示例见完整源码与接入方法弹窗。

除了类型检查,还应在父层断言:初次渲染值相同、子层修改发出对应更新、父层更新能回流,以及命名 model 不会误发默认事件。编译通过只能证明语法和编译路径成立。

@sheng/vue-no-define-model-duplicate-contract提示同名重复契约。它不替组件决定默认值归属,也不负责证明全部运行态同步行为。旧组件如果继续使用手写 prop / emit 且没有引入 defineModel,可以保持原有稳定写法。

总结

defineModel 已经声明了对应的 prop 和更新事件,迁移时应移除同名重复声明,保留其他契约。默认值则要按组件是否受控决定归属;编译成功和 lint 通过,都不能替代父子初值、更新事件与回流行为的验证。

defineModel 已经声明了什么:避免重复 v-model 契约和默认值分叉

打开原文件 FILES.json

defineModel 已经声明了什么:避免重复 v-model 契约和默认值分叉正在加载代码工作区...