Toolcoin
29.10M · 2026-03-04
defineModel 是 Vue 3.4 引入的语法糖。
它看起来只是让 v-model 更优雅:
const visible = defineModel<boolean>('visible')
但它背后做的事情,远不止简单的语法糖,甚至改变了组件的状态哲学。
在大部分 v-model 语义里,存在一个隐含规则:
比如对弹窗组件,如果父组件只传递了 visible 的 prop:
<MyDialog :visible="visible" />
我们会认为 MyDialog 的显示和隐藏完全由父组件控制
父组件的 visible 变量是控制 MyDialog 显示/隐藏的唯一数据源
这是一种非常清晰的“受控组件”边界
但是如果 MyDialog.vue 的 visible 由 defineModel 实现时,情况会有些不一样:
<script setup>
const visible = defineModel('visible')
</script>
<template>
<div>
<div>MyDialog 内的 visible:{{ visible }}</div>
<button @click="visible = !visible">MyDialog 内切换 visible</button>
</div>
</template>
如果父组件中还是
<MyDialog :visible="visible" />
请问子组件按钮点击时,visible 会变化吗?
答案是:会变化
代码链接
直接看 playground 生成的代码:
defineModel 除了生成对应的 props 和 emits,还通过 useModel 产生了 MyDialog 内的 visible 变量。
而在 useModel 里,会使用 customRef 创建一个本地变量。
在这个本地变量的设值逻辑里,是这样的(简化):
if (
!(
rawProps &&
// check if parent has passed v-model
(name in rawProps ||
camelizedName in rawProps ||
hyphenatedName in rawProps) &&
(`onUpdate:${name}` in rawProps ||
`onUpdate:${camelizedName}` in rawProps ||
`onUpdate:${hyphenatedName}` in rawProps)
)
) {
// no v-model, local update
localValue = value
trigger()
}
即 :visible="visible" 和 @update:visible="..." 任意一个不存在,就会更新本地数据。
翻译一下:
否则 —— 子组件会使用本地变量的数据
这意味着:
真正的数据源变成:
这是一种动态切换的数据源模型。
从功能角度看,它很强大。
对于“有内部状态”的组件,非常舒服。比如手风琴组件,使用方不需要提供变量保存手风琴的开关状态。
但对于大部分输入组件,它带来了新的权衡。
传统设计下:
现在:
如果你想让组件真正受控,你必须写:
<MyDialog
:visible="visible"
@update:visible="() => {}"
/>
用一个空器,强制关闭本地数据源。
这就产生了一个认知断层:
defineModel组件的状态模型,不再从接口上显式表达。
<MyDialog :visible="visible" />
由原本的只读受控语义,隐式拓展出了类似 init-visible 的初始值赋值语义。
而是受控,还是初始值赋值,取决于内部是否使用了 defineModel。
defineModel 带来的不只是语法糖。还把
它让 Vue 组件具备了“双数据源能力”。需要清醒认知它的能力边界。
两个想法:
defineModel,保持单一数据源