airi 项目 Vue 组件 v-model 双向绑定实战:useVModel 与 defineModel 的取舍与用法详解
2026/9/10 16:13:20 网站建设 项目流程

airi 项目 Vue 组件 v-model 双向绑定实战:useVModel 与 defineModel 的取舍与用法详解

【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi

useVModel是 VueUse 提供的 v-model 快捷封装,把「props + emit」这一对松散概念收敛为一个可直接读写的 ref,是组件库与复杂交互组件中双向绑定的经典实现。本文以 .agents/skills/vueuse-functions/references/useVModel.md 为骨架,结合 airi 仓库(packages/stage-uipackages/stage-pagespackages/ui等)的真实组件源码,讲解 useVModel 的完整用法、六大选项的底层行为,以及它与defineModel在什么场景下应该二选一,帮助你写出更健壮的受控组件。

一、useVModel 是什么:props + emit → ref

在 Vue 中实现v-model需要同时处理两件事:通过props.modelValue读取父级传入的值,通过emit('update:modelValue', newValue)把变更回传给父级。这套「读 + 写」的配对逻辑在每个组件里重复出现,而useVModel正是它的简写形式——把propsemit和属性名绑定在一起,返回一个可读写的 ref:读取ref.value等价于读props,赋值ref.value = newValue等价于触发对应的事件。

它的类型签名可以用一句话概括:

props + emit -> ref

默认约定:属性名为modelValue,事件名为update:modelValue,这与 Vue 内置的v-model约定完全一致,因此useVModel的返回值可以直接接在模板的v-model上,形成组件间的无缝衔接。

二、基础用法:Composition API 与 Options API

Composition API(<script setup>

import { useVModel } from '@vueuse/core' const props = defineProps<{ modelValue: string }>() const emit = defineEmits(['update:modelValue']) const data = useVModel(props, 'modelValue', emit)

之后在模板中可直接使用:

<input v-model="data" />

data.value的读取对应props.modelValue,写入data.value = 'foo'则触发emit('update:modelValue', 'foo')

Options API(setup()函数)

useVModel 同样适用于不使用<script setup>的写法,第二个参数可以指定任意属性名(不局限于modelValue):

import { useVModel } from '@vueuse/core' export default { setup(props, { emit }) { const data = useVModel(props, 'data', emit) console.log(data.value) // props.data data.value = 'foo' // emit('update:data', 'foo') }, }

这里体现了 useVModel 的通用性:它并不关心属性名是否叫modelValue,只要传入的keyemit的能力匹配,就能为任意 prop 建立双向通道。

三、六大选项详解:从被动同步到发射前校验

useVModel的第四参数options提供了六个可配置项,理解它们各自的触发条件与默认值,是正确使用的关键。完整类型声明可查阅文档中的UseVModelOptions<T, Passive>接口,下面逐一展开。

1. passive(被动模式)

  • 类型:boolean,默认值false
  • 作用:决定返回值是 computed ref 还是本地 ref

默认情况下,useVModel返回的是一个computed refWritableComputedRef):读操作直接代理到 prop,写操作直接 emit。这是一种「直通」语义,无额外中间层,性能最优。

而在passive: true模式下,它会创建一个本地 ref,并通过watch与 prop 保持同步:

const data = useVModel(props, 'modelValue', emit, { passive: true })

被动模式的核心收益是允许深层响应式追踪。computed 模式对嵌套对象内部属性的变更(如obj.a.b = 1)不会产生预期的同步效果,而本地 ref + watch 的组合可以做到。

2. deep(深层监听)

  • 类型:boolean,默认值false
  • 适用前提:仅在passive: true时生效

当绑定的是嵌套对象或数组,且需要在内部属性变更时也能同步,需要同时开启passivedeep

const data = useVModel(props, 'modelValue', emit, { passive: true, deep: true, })

这是defineModel尚不支持的 edge case 之一——文档明确提示,类似deep: true的场景需要回退到 useVModel。

3. clone(克隆 prop 值)

  • 类型:boolean | CloneFn<T>,默认值false
  • 作用:避免直接修改父级传入的原始对象

当值为对象/数组时,子组件内部的修改可能意外污染父组件的状态。设置clone: true会用JSON.parse(JSON.stringify(value))做深拷贝;也可以传入自定义克隆函数以支持DateMap等 JSON 无法表达的类型:

const data = useVModel(props, 'modelValue', emit, { clone: true, // or provide custom clone function // clone: (val) => structuredClone(val), })

注意structuredClone是浏览器原生 API,相比 JSON 序列化能正确处理更多数据类型,代价是对运行环境有要求。

4. defaultValue(默认值)

  • 类型:T,默认值undefined
  • 作用:当 prop 为undefined时,为返回的 ref 提供兜底值
const data = useVModel(props, 'modelValue', emit, { defaultValue: 'default', })

这在「父组件未传值、但子组件内部需要有一个可用初值」的场景非常实用,例如表单输入框的默认占位值。

5. eventName(自定义事件名)

  • 类型:string,默认值undefined(使用update:propName
  • 作用:覆盖默认的update:propName事件名
const data = useVModel(props, 'value', emit, { eventName: 'change', })

当组件需要兼容旧的事件协议(例如历史上使用change而非update:value)时,无需改动父组件,即可用该选项适配。

6. shouldEmit(发射前校验)

  • 类型:(v: T) => boolean,默认值undefined
  • 作用:在触发 emit 之前做校验,返回false则阻止发射
const data = useVModel(props, 'modelValue', emit, { shouldEmit: (value) => { // Only emit if value is valid return value.length > 0 }, })

这是文档中标注为「可用于表单校验」的钩子:它拦截的是「向外发射」这个动作,适合实现「非法输入不打扰父组件」的防御式设计——例如空字符串、超出范围的值等都不再向上冒泡。

选项速查表

选项类型默认值生效条件核心作用
passivebooleanfalse始终返回 computed ref 或 watch 同步的本地 ref
eventNamestringundefined始终覆盖 emit 事件名
deepbooleanfalsepassive: true深层监听嵌套对象/数组变化
defaultValueTundefined始终prop 为undefined时提供兜底值
cloneboolean \| CloneFn<T>false始终克隆 prop,避免污染父级原始对象
shouldEmit(v: T) => booleanundefined始终emit 前校验,返回false阻止发射

四、useVModel 与 defineModel:什么时候该用哪个

文档给出的官方态度非常明确:推荐优先使用 Vue 的defineModel,但仍存在defineModel覆盖不到的 edge case,需要回退到 useVModel:

  • TSX / 非 SFC 环境defineModel<script setup>的编译宏,只能在 SFC 中工作;而在 TSX 渲染函数或纯 Options API 组件中,defineModel不可用,useVModel 是唯一选择。
  • deep: true深层监听:当绑定对象需要监听嵌套属性变化时,defineModel原生不支持该能力,需借助 useVModel 的passive + deep组合。
  • cloneshouldEmit等高级选项:useVModel 提供的这组细粒度选项,让它在需要防御性拷贝、发射前校验的复杂组件中更具表达力。

在 airi 仓库中可以观察到这种分工的实际落地。packages/stage-uipackages/stage-pagespackages/ui等包中,共有59 个组件文件采用defineModel作为默认双向绑定方案,与文档「鼓励优先使用 defineModel」的指引一致;同时@vueuse/core作为依赖出现在 20 余个包的 package.json 中,VueUse 生态(含 useVModel)始终是可用后备方案。

五、仓库实战:airi 中的 v-model 组件设计模式

模式一:布尔开关——最简 defineModel

check-bar.vue 是设置面板中的勾选项,只用一行完成双向绑定声明:

const model = defineModel<boolean>()

模板中直接把 ref 接回原生控件:

<input v-model="model" :aria-checked="model" type="checkbox" hidden>

模式二:带默认值的受控输入

provider-api-key-input.vue 是 AI 服务商 API Key 输入框,展示了defineModel对「默认值」的处理:

const modelValue = defineModel<string>({ required: false, default: '' })

随后将modelValue透传给更底层的FieldInput

<FieldInput v-model="modelValue" :label="label || t('settings.pages.providers.common.fields.field.api-key.label')" :placeholder="placeholder" :required="required" type="password" />

这是典型的「受控组件组合」:上层组件把 v-model 向下透传,值的变化会沿FieldInput -> provider-api-key-input -> 父级配置表单逐层冒泡,最终写入配置 store。

模式三:多命名模型

context-flow-filters.vue 展示了defineModel的命名模型能力——上下文流调试面板同时暴露多个筛选状态:

const directionFilter = defineModel<'all' | FlowDirection>('directionFilter', { required: true }) const showIncoming = defineModel<boolean>('showIncoming', { required: true }) const showOutgoing = defineModel<boolean>('showOutgoing', { required: true }) const showServer = defineModel<boolean>('showServer', { required: true }) const showBroadcast = defineModel<boolean>('showBroadcast', { required: true }) const showChat = defineModel<boolean>('showChat', { required: true }) const showDevtools = defineModel<boolean>('showDevtools', { required: true }) const maxEntries = defineModel<string>('maxEntries', { required: true })

父组件通过<ContextFlowFilters v-model:direction-filter="..." v-model:show-incoming="..." />分别绑定各筛选项,避免了把一堆布尔值塞进单个modelValue对象的脏做法。对应到 useVModel 体系,这套能力由姊妹函数useVModels(批量处理多个 prop)提供,两条路线在仓库中均可按需选用。

模式四:泛型受控组件

input.vue 是packages/ui基础组件库中的通用输入框,用泛型约束模型类型:

const modelValue = defineModel<T>({ required: false })

这种写法让基础组件在stringnumber等类型间保持类型安全,是组件库层面 v-model 设计的推荐范式。

六、使用建议与最佳实践

综合文档与 airi 仓库的落地经验,可以总结出如下选型与使用建议:

  1. 默认走defineModel:SFC 内、无特殊需求的组件,优先使用编译宏,代码最简洁(仓库中 59 个组件的实践已证明这一点)。
  2. 以下场景切回useVModel:TSX 渲染、Options API 组件、需要deep: true深层监听、需要clone隔离父级对象、需要shouldEmit做发射前表单校验。
  3. 组合选项要满足前提deep必须搭配passive: true才生效;clonetrue时注意 JSON 序列化对DateMap、函数等类型的丢失,需要时改用structuredClone或自定义函数。
  4. 把校验放在边界shouldEmit适合拦截非法值上抛,但「值本身是否合法」的最终裁决仍应交给父组件,保持单向数据流的清晰。
  5. 命名模型保持克制:当组件需要暴露 3 个以上模型时,优先评估是拆分子组件还是使用命名模型(v-model:xxx/useVModels),避免modelValue携带过大的聚合对象。

七、进一步探索

  • 本文主题的完整选项定义与类型声明,见 useVModel.md;
  • 批量处理多个 v-model 的姊妹方案:同目录下的 useVModels.md;
  • 仓库中的真实应用:packages/stage-ui/src/components/scenarios/(设置项与弹窗)、packages/stage-pages/src/pages/devtools/context-flow/(多命名模型)、packages/ui/src/components/form/(基础受控组件);
  • 依赖声明:@vueuse/core见 packages/stage-ui/package.json 与 packages/ui/package.json。

【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询