一、组件化与双向绑定的演变背景
1. 早期前端:手写 DOM 与状态管理痛点
在纯 JavaScript 时代,我们尝尝直接操作 DOM、管理全局变量。随着应用复杂度提升,出现了以下问题:
- 状态混乱:UI 状态(表单值、控件可见性)散落在各个回调里,难以维护。
- 模块化隔离不足:不同 UI 模块互相干扰,数据污染严重。
- 手动同步:每次数据改变,都要手动 querySelector → 更新属性 → 补齐事件监听器,十分臃肿。
2. MVVM 思想与双向绑定萌芽
为了解决手工同步的痛点,AngularJS(1.x)提出了 MVVM 架构,引入双向数据绑定,模板中写一个 ng-model,改变模型(Model)或视图(View)都能自动同步。这大大提升了开发效率,但也带来性能风险——脏检查(digest cycle)在大型应用中很难调优。
Vue 1.0 受到启发,保留双向绑定优势的同时,用基于 Object.defineProperty 的响应式系统来替代脏检查,性能与体验兼得。
二、Vue 2.x 中的 v-model
1. 前言:为什么需要 v-model 呢?
在传统的网页开发中,我们经常要手动将表单输入与 JavaScript 数据做同步:
- 监听输入框的 input 或 change 事件;
- 在回调中 setState 或直接赋值;
- 然后在渲染函数中把数据写回到 DOM。
这种手工绑定不仅代码冗长,而且容易出错,尤其是在多表单、复杂交互场景下,维护成本极高。
Vue 2.x 引入了 MVVM(Model–View–ViewModel)的理念,通过 数据响应式 + 模板指令,让模板到数据、数据到模板的同步都只需一句话:
<input v-model="foo" />
Vue 会在内部搞定:
- 初始化时把 foo 的值渲染到输入框;
- 用户输入时自动更新 foo;
- foo 变化时又反过来更新输入框。
从而极大地简化了表单开发,提升了开发效率。
2. 基本用法
举个 🌰
<div id="app">
<input v-model="message" placeholder="请输入文本">
<p>输入的是:{{ message }}</p>
</div>
new Vue({
el: '#app',
data: {
message: ''
}
});
- 渲染阶段:Vue 把 data.message 的值赋给 <input> 的 value 属性。
- 模版绑定:Vue 编译器把 v-model 转为 :value="message" + @input="message = $event.target.value"。
- 交互阶段:用户输入触发 input 事件,执行赋值,内部触发 数据响应式,重新渲染视图。
2. 在不同表单控件中的应用
- <input type="text">、<textarea>:监听 input 事件。
- <input type="checkbox">:在 value 与 checked 之间进行双向绑定;
- <input type="radio">:将同一组 radio 的 value 与数据做一一映射;
- <select>:多选时会将选中值合并为数组。
<!-- 单选 -->
<label><input type="radio" v-model="picked" value="A">A</label>
<label><input type="radio" v-model="picked" value="B">B</label>
<!-- 复选 -->
<label><input type="checkbox" v-model="checks" value="X">X</label>
<label><input type="checkbox" v-model="checks" value="Y">Y</label>
<!-- 下拉 -->
<select v-model="selected">
<option disabled value="">请选择</option>
<option>A</option>
<option>B</option>
</select>
3. v-model 修饰符
Vue 2.x 为 v-model 提供了三个常用修饰符:.lazy、.number、.trim,以及组合的 .sync 语法(实为 v-bind.sync 语法糖)。
.lazy
<input v-model.lazy="msg" />
- 默认:v-model 在 input 事件触发时同步。
- 懒惰模式:v-model.lazy 在 change 事件触发时才同步(失去焦点或按下回车)。
- 场景:避免每次按键都触发更新,如大型数据校验、实时搜索时可减少不必要的操作。
.number
<input v-model.number="quantity" type="number">
自动将用户输入的字符串转为数字,相当于:
inputHandler(e) {
this.msg = Number(e.target.value);
}
注意:Number('') 会转成 0,要防范空字符串带来的歧义。
.trim
<input v-model.trim="username" placeholder="用户名">
自动去除用户输入的首尾空白,相当于:
inputHandler(e) {
this.msg = e.target.value.trim();
}
.sync(.sync 语法糖)
其实是 v-bind:prop.sync="value" 的简写。
等价于:
<Child :foo="bar" @update:foo="val => bar = val" />
父组件在使用子组件绑定时:
<my-comp :title.sync="pageTitle" />
会自动把子组件的 this.$emit('update:title', newVal) 绑定到父组件,达成双向。
4. 自定义组件中的 v-model
当把 v-model 用在子组件时,Vue 会默认:
- prop:value
- 事件:input
比如:
<!-- 父组件 -->
<my-input v-model="text" />
等价于:
<my-input :value="text" @input="val => text = val" />
在子组件里,需要:
Vue.component('my-input', {
props: ['value'],
template: `<input :value="value" @input="$emit('input', $event.target.value)" />`
});
自定义 model 选项
Vue 2.x 支持在组件中用 model 选项定制 v-model 的 prop 和 event 名称:
Vue.component('my-cmp', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `<input type="checkbox" :checked="checked" @change="$emit('change', $event.target.checked)" />`
});
父组件写 <my-cmp v-model="flag" /> 时,Vue 会使用 prop=checked、event=change。
5. Vue 2.x v-model 的底层原理
指令系统(Directive)
v-model 在 Vue 2 中本质是一个 内置指令(model):
-
在编译阶段,模板编译器会识别 v-model 指令,生成对应的 AST 节点;
-
在渲染阶段,为元素注入 model 指令的钩子函数:bind、update、componentUpdated 等。
// 大致伪代码结构
model: {
bind(el, binding, vnode) {
// 初始化:setInitialValue(el, binding.value)
// 根据 el.tagName / type 决定事件类型,如 input、change
el.addEventListener(eventName, handler);
},
update(el, binding) {
// 当数据变化时,更新 el.value = binding.value
},
unbind(el, binding) {
// 清理事件监听器
el.removeEventListener(eventName, handler);
}
}
- bind 钩子:初始化时为元素添加对应的事件监听器,并设置初始值。
- update 钩子:响应式数据变化后,Vue 会调用指令的 update,保持 DOM 与数据同步。
AST 转换
以 <input v-model="foo" /> 为例,编译器处理过程:
-
解析模板 → AST:
{ tag: 'input', attrsList: [{ name: 'v-model', value: 'foo' }], // ... 其它节点信息 }
-
生成指令 → 为 AST 添加 model 指令:
node.directives = [{ name: 'model', rawName: 'v-model', value: 'foo', modifiers: {} }];
-
代码生成 → 在渲染函数中注入 withDirectives:
withDirectives( createElementVNode('input', { value: foo }), [[vModelText, foo]] )
运行时:指令 Hook 与响应式
1、初始化:directive.bind 会调用 el.value = foo,并用 el.addEventListener('input', handler)。
2、用户输入:handler 执行 foo = el.value,触发 Vue 的响应式赋值。
3、响应式更新:数据变化经过 Object.defineProperty 的 setter → Dep.notify() → watcher → 重新执行渲染函数 → 再次触发指令的 update,从新同步 el.value。
数据响应式核心
Vue 2.x 用 Object.defineProperty 实现响应式:
- 遍历 data,对每个属性调用 defineReactive(obj, key, val)。
- getter:收集依赖(Dep.target),即当前活跃的 watcher。
- setter:更新值并触发 dep.notify(),通知所有 watcher 更新视图。
v-model 的流程:
- 数据源:data.foo 被代理到组件实例,拥有 getter/setter。
- 视图端:directive.bind 通过 el.addEventListener 订阅 input。
- 数据变更:用户触发 input → setter 执行 → 通知渲染 watcher → 重新渲染 → directive.update 同步 DOM。
6. 源码分析
1、指令注册
// src/platforms/web/runtime/directives/model.js
export default {
bind(el, binding, vnode) {
setModel(el, binding, vnode);
},
update(el, binding, vnode) {
if (binding.value !== binding.oldValue) {
setModel(el, binding, vnode);
}
},
};
2、模型设置
function setModel(el, binding, vnode) {
const { value, modifiers } = binding;
const tag = el.tagName.toLowerCase();
if (tag === 'input') {
if (el.type === 'text' || el.type === 'password') {
el.value = value; // 初始值
el.addEventListener(
modifiers.lazy ? 'change' : 'input',
e => {
let newValue = e.target.value;
if (modifiers.trim) newValue = newValue.trim();
if (modifiers.number) newValue = Number(newValue);
vnode.context[binding.expression] = newValue;
}
);
}
// 省略 radio / checkbox 逻辑
} else if (tag === 'select') {
// select 多选/单选处理...
}
}
三、Vue 3 中 v-model 的重构
1. 为什么要重构 v-model 呢?
Vue 2 的困境
在 Vue 2.x 时代,v-model 已经是表单开发的王炸指令,但也存在一些局限:
- 默认只能单一绑定
- 组件上只能有一个 v-model,对应默认的 value/input。如果想对不同字段做双向绑定,就必须手动写 :foo + @update:foo 或使用 .sync,很不直观。
- 修饰符语法零散
- .lazy、.number、.trim 三个修饰符虽然能满足大部分需求,但并不能自定义,也不能应用到组件层面。
- TypeScript 支持不友好
- 在组件内部用 v-model 时,仍必须显式地在 props 中声明 value,在 emits 中声明 input,样板代码冗余。
Vue 3 的目标
Vue 3 团队希望在保留 v-model 开发便捷性的同时:
- 支持多模型绑定,让同一个组件能暴露多个 v-model,且语法统一。
- 修饰符更灵活,支持自定义绑定时机、预处理管道。
- 与 Composition API 深度融合,在 <script setup> 中减少样板、提升 TS 体验。
- 底层实现更模块化,让指令、编译器能更好地演进和扩展。
2. Vue 3 中 v-model 的新语法与用法
默认 v-model:modelValue + update:modelValue
在 Vue 3 中,单一 v-model 绑定变成:
<!-- 模板写法 -->
<ChildComponent v-model="foo" />
<!-- 编译后等价于 -->
<ChildComponent
:modelValue="foo"
@update:modelValue="val => foo = val"
/>
- Prop 名称:modelValue
- 事件名称:update:modelValue
这样避免了 Vue 2 的 value/input 语义冲突,同时与「单向数据流」的概念更契合:prop 向下传递,事件向上传递。
多模型绑定
Vue 3 支持在同一个组件上使用多次 v-model,并且可自定义绑定的字段名:
<MyComp
v-model:title="pageTitle"
v-model:visible="isDialogVisible"
/>
等价于:
<MyComp
:title="pageTitle"
@update:title="val => pageTitle = val"
:visible="isDialogVisible"
@update:visible="val => isDialogVisible = val"
/>
任意 prop 都可以变成双向绑定。
格式:v-model:arg="value" → :arg="value" + @update:arg="value = $event"
v-model 修饰符的变化
Vue 3 仍保留 .lazy、.number、.trim,但在组件层面也可通过 modelModifiers 传递到子组件上:
<!-- 在父组件 -->
<Child v-model.lazy.number.trim="foo" />
<!-- 会将 { lazy: true, number: true, trim: true } 作为 modelModifiers 传给子组件 -->
<Child
:modelValue="foo"
@update:modelValue="foo = $event"
:modelModifiers="{ lazy: true, number: true, trim: true }"
/>
子组件可在 v-bind="$attrs" 中接收 modelModifiers,或通过 defineProps({ modelModifiers: Object }) 拿到修饰符描述,自定义处理。
3. 底层编译器如何处理 v-model
AST 阶段指令转换
以 <input v-model="msg" /> 为例,Vue 3 编译器在解析模板阶段会:
- 生成 AST,节点包含 directive: { name: 'model', arg: null, exp: 'msg', modifiers: {} }
- 转换指令:将 v-model 转为两条指令:
-
v-bind:modelValue="msg"
-
v-on:update:modelValue="($event) => msg = $event"
-
-
最终生成渲染函数:
createVNode( 'input', { modelValue: msg, 'onUpdate:modelValue': $event => (msg = $event) } )
真正的编译产物中,已经看不到 v-model,只剩下标准的 bind 与 on。
运行时指令与组件支持
对于原生元素,Vue 3 还提供了专门的 vModelXXX 指令模块,如 vModelText、vModelCheckbox、vModelRadio、vModelSelect,它们统一挂在 withDirectives 中:
withDirectives(
createVNode('input', { type: 'text', modelValue: msg }),
[[vModelText, msg]]
)
- vModelText 内部:在 beforeMount 钩子设置 el.value = msg,mounted 钩子注册 input 事件。
- 组件:对于自定义组件,v-model 转为 modelValue + update:modelValue,由组件自己 emit 事件来触发父级更新。
4. 与 Composition API 的深度融合
<script setup> 中的写法
在 Vue 3 推荐的 <script setup> 语法里,v-model 依旧可以保留,只要配合 defineProps 与 defineEmits:
<template>
<input v-model="local" />
</template>
<script setup>
const props = defineProps({ modelValue: String })
const emit = defineEmits(['update:modelValue'])
const local = ref(props.modelValue)
watch(local, v => emit('update:modelValue', v))
</script>
父组件用 v-model 传进来,就完成了“父 → 子 → 父” 的单向数据流与事件流,清晰、安全。
而在纯模板层面,只需:
<!-- 父组件 -->
<CustomInput v-model="parentValue" />
<!-- 子组件 -->
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
<script setup>
defineProps({ modelValue: String });
defineEmits(['update:modelValue']);
</script>
useVModel 辅助函数
社区常用的 @vueuse/core 提供 useVModel,进一步简化:
<script setup>
import { useVModel } from '@vueuse/core'
const props = defineProps({
modelValue: String
})
const emit = defineEmits(['update:modelValue'])
const value = useVModel(props, 'modelValue', emit)
// value 是一个 ref,与父组件 v-model 完全同步
</script>
5. 底层实现原理
流程概览
- 编译模版阶段
- 解析到 v-model 指令
- 在 AST(抽象语法树)中添加相应的指令节点
- 将 v-model:foo="bar" 转换为标准的 v-bind:foo="bar" + v-on:update:foo="bar = $event"
- 渲染函数生成
- 把 AST 里的指令节点输出成 createVNode/h() 调用
- 在 vnode 上挂载 modelValue 属性和 onUpdate:modelValue 事件处理器
- 运行时执行
- 对于原生元素,用专门的 vModelXxx 指令模块(如 vModelText、vModelCheckbox)来完成 DOM → 数据 和 数据 → DOM 的双向绑定。
- 对于组件,直接以 prop/event 的形式读写,组件自己负责调用 emit('update:foo', newVal)。
编译器层面:指令到渲染函数的演变
1、AST 转换
当解析到模版中的一条 v-model 指令时,Vue 3 的编译器(@vue/compiler-core)会在 AST 节点上做如下两步转换:
<!-- 模板 -->
<Child v-model:title="pageTitle" />
- 添加绑定
- 在节点的 props 中添加一条 modelValue: pageTitle(对应 :title="pageTitle")。
- 添加事件
- 在节点的事件监听中添加一条 onUpdate:title: $event => pageTitle = $event(对应 @update:title="pageTitle = $event")。
2、代码生成
最终,渲染函数里那一行 vnode 创建会变成:
// 渲染函数伪代码
return createVNode(Child, {
title: pageTitle,
'onUpdate:title': $event => (pageTitle = $event)
});
或者如果是原生 <input v-model="msg" />,则会调用:
return withDirectives(
createVNode('input', { value: msg }),
[[vModelText, msg]]
);
这里 withDirectives 会把指令传给运行时去处理。
运行时层面:专用指令与事件流
Vue 3 在运行时提供了一组专用的 vModelXxx 指令函数,分别针对不同类型的表单控件:
指令 | 作用对象 | 对应包与模块 |
---|---|---|
vModelText | <input type="text">、<textarea> | @vue/runtime-dom |
vModelCheckbox | <input type="checkbox"> | @vue/runtime-dom |
vModelRadio | <input type="radio"> | @vue/runtime-dom |
vModelSelect | <select> | @vue/runtime-dom |
1、指令内部核心流程
以 vModelText 为例,伪代码运行时实现:
// vModelText 指令主要钩子
const vModelText = {
// 在 mount 时设置初始值并注册事件
mounted(el, binding, vnode) {
el.value = binding.value;
el.addEventListener('input', e => {
let newVal = e.target.value;
// 处理 trim、number 修饰符……
vnode.props['onUpdate:modelValue'](newVal);
});
},
// 在更新时,同步最新数据回 DOM
updated(el, binding) {
if (binding.value !== el.value) {
el.value = binding.value;
}
}
};
- mounted 阶段:将初始值写入 el.value,注册 input 事件,事件回调内调用 onUpdate:modelValue(即父级绑定的 $event => msg = $event)。
- updated 阶段:在响应式更新阶段,将最新的 binding.value(即 msg)同步回 DOM,确保父改动也能反映在输入框上。
2、事件流程图
父组件 data.foo
↓ (渲染阶段)
input.value = foo
↓ (用户输入)
input事件 ⇒ 拦截 newVal
↓
触发 onUpdate:modelValue(newVal)
↓
foo = newVal(响应式 setter)
↓ (再次渲染)
input.value = foo
这个“数据→视图→事件→数据”循环就是双向绑定的核心。
6. 扩展与自定义
自定义组件的 v-model
自定义组件只要 暴露 modelValue prop 和 update:modelValue 事件即可与 v-model 配合:
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
defineProps({ modelValue: String });
const emit = defineEmits(['update:modelValue']);
</script>
父级直接 <MyComp v-model="foo" />,毫无额外配置。
修饰符管道
Vue 3 将原生的 .trim / .number / .lazy 修饰符也统一在指令内部处理:
// 示例:input 事件处理器内
el.addEventListener(modifiers.lazy ? 'change' : 'input', e => {
let val = e.target.value;
if (modifiers.trim) val = val.trim();
if (modifiers.number) val = Number(val);
vnode.props['onUpdate:modelValue'](val);
});
这样在模板上写 <input v-model.number.trim="age" /> 就被自动组合成“数字转换 + 空白裁剪 + 延迟触发”的完整逻辑。
四、Vue 3.4 的 defineModel
1. 为什么需要 defineModel 呢?
1、从 Vue 2.x 到 Vue 3.x 的双向绑定演进
Vue 2.x 引入了 v-model,将表单输入的值与组件数据做双向绑定,语法糖极大简化了表单开发,但仍有痛点:只能同时支持一个 v-model,需要手写大量的 props 和 emits;多模型绑定依赖 .sync 或手动扩展,TS 场景下类型声明冗余。
Vue 3.0 ~ 3.3 在底层改为 modelValue + update:modelValue、支持多 v-model:arg,解决了事件/prop 名冲突、支持多个模型。但在 <script setup> 下,开发者仍需显式写:
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{'update:modelValue': (v: string) => void}>()
样板代码较多,多模型下成倍膨胀。
2、样板代码的维护成本
在大型组件库或业务组件中,常见场景:提示框、表单字段、分页器都需要双向绑定。需要为每个字段定义对应的 prop 和 emit:
defineProps({
visible: Boolean,
pageSize: Number,
currentIndex: Number,
});
defineEmits([
'update:visible',
'update:pageSize',
'update:currentIndex',
]);
当字段越来越多,或者要做版本迭代、字段重命名,维护成本高、易出错。
3、<script setup> + TypeScript 的机会
Vue 3 推荐使用 <script setup>,它本身简化了生命周期、props、emits 的写法。但对 v-model 支持依然需要手动声明:
<template>
<input v-model="props.modelValue" />
</template>
<script setup lang="ts">
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{
(e: 'update:modelValue', v: string): void
}>()
</script>
当要支持多个 v-model 时,还要多次重复,体验并不够理想。
4、团队一致性与 DX(开发者体验)追求
对于中大型团队和组件库,统一简单、少样板、类型安全的“双向绑定”方案是刚需。Vue 核心团队在 3.4 版推出 defineModel 编译宏,旨在一次性声明、自动生成对应的 props 和 emits,极大提升 <script setup> 下的 DX。
2. 基本使用
安装与开启
- Vue CLI:升级 @vue/compiler-sfc 至 3.4+ 版本。
- Vite:升级 @vitejs/plugin-vue 至支持 Vue 3.4 的版本。
- 一般不用额外配置,默认 <script setup> 就可使用 defineModel。
单模型绑定
<template>
<!-- 自动在 props 中注入 value,emits 注入 update:value -->
<input v-model="value" placeholder="请输入" />
</template>
<script setup lang="ts">
defineModel({
value: {
type: String,
default: ''
}
});
</script>
等同于:
const props = defineProps<{ value: string }>()
const emit = defineEmits<{
(e: 'update:value', v: string): void
}>()
const value = useVModel(props, 'value', emit)
多模型绑定:一次性声明多个字段
<template>
<h1>{{ title }}</h1>
<button @click="visible = !visible">{{ visible ? '隐藏' : '显示' }}</button>
<button @click="count++">Count: {{ count }}</button>
</template>
<script setup lang="ts">
defineModel({
title: { type: String, default: '' },
visible: { type: Boolean, default: false },
count: { type: Number, default: 0 },
});
</script>
自动在 props 中注入 { title, visible, count },在 emits 中注入 [ 'update:title','update:visible','update:count' ],并在 <script setup> 中提供同名 ref。
结合 TS 类型:接口或泛型声明
如果需要更精准地声明类型:
interface ModelProps {
name: string
age: number
}
defineModel<ModelProps>({
name: String,
age: Number,
});
defineModel 可接受泛型,TypeScript 会推断 props.name 是 string,props.age 是 number。
默认值与必填
defineModel({
count: { type: Number, required: true },
label: { type: String, default: '按钮' }
});
- required: true 强制父组件必须传入。
- default 在未传值时使用默认值。
3. 底层原理:编译宏如何工作?
什么是“编译宏”
宏:一种在编译阶段对源代码做特殊识别和变换的语法。
与运行时 API 的区别:宏调用在源码阶段展开,不会引入额外运行时代码,只在编译器中执行。
Vue 3.4 把 defineModel 实现为一个 SFC(单文件组件)编译宏:它在处理 <script setup> 时被识别,然后注入标准的 defineProps 与 defineEmits。
宏展开示例
开发者写:
<script setup lang="ts">
defineModel({
foo: String,
count: { type: Number, default: 0 }
});
</script>
在编译器处理时,等同于在顶层自动插入:
const __props = defineProps({
foo: String,
count: { type: Number, default: 0 }
});
const __emit = defineEmits(['update:foo','update:count']);
const foo = useVModel(__props, 'foo', __emit);
const count = useVModel(__props, 'count', __emit);
然后删除 defineModel 的原始调用,最终生成的渲染函数里只有上述标准调用,没有宏调用残留。
宏实现原理概览
defineModel 并非运行时代码,它是编译宏(Compiler Macro):
- 编译阶段识别:Vue 的模板编译器在遇到 defineModel({ ... }) 时会将其展开。
- 注入 defineProps / defineEmits:自动生成与每个 key 对应的 defineProps 和 defineEmits 调用。
- 生成本地 ref:在 <script setup> 内返回一个同名的 ref,其 .value 与父组件 v-model 值双向同步。
- 抹除宏调用:最终打包产物中,defineModel 调用被移除,只保留标准 JS。
因此,defineModel 在开发时提供极佳 DX(开发体验),但不会给运行时增加负担。
为何无运行时开销
- 宏展开的代码和手写 defineProps / defineEmits 是等价的,运行时代码没有额外分支或 API。
- 宏调用在打包阶段被移除,最终 bundle 里只剩标准 API 调用。
- 不会增加执行路径长度或闭包开销。
4. 与传统 v-model 对比
维度 | Vue 3.3 及更早 v-model | Vue 3.4 defineModel |
---|---|---|
声明样板 | 手动写 defineProps + defineEmits | 一次写 defineModel 即可 |
多模型 | 模板写多次 v-model:arg,props/emits 需匹配 | 宏中一并声明,自动注入 props & emits |
类型推导 | 需自己定义泛型 | 宏支持泛型输入,IDE 自动推断 |
底层成本 | 只有标准 API,无额外开销 | 宏在编译阶段展开,运行时无差异 |
可读性 | props/emits 分散,零碎 | 一处集中声明,后续使用更简洁 |
修饰符支持
Vue 3.4 保留 .trim、.number、.lazy,也可以在宏声明中传递 modelModifiers 到子组件:
<MyComp v-model.trim.lazy="foo" />
父级会传入 modelModifiers={ trim: true, lazy: true },子组件可在 defineProps({ modelModifiers: Object }) 中接收,并据此自定义指令逻辑。
自定义事件 / prop 映射
如果要做类似 Vue 2.x 的 model: { prop: 'checked', event: 'change' },在 Vue 3.4 里可以配合自定义组件直接:
<script setup>
defineModel({
checked: Boolean
}, { // 可选第二参数:事件/prop 映射
propName: 'checked',
eventName: 'change'
});
</script>
注:此语法目前需依赖 RFC 或社区插件支持,核心思想是在宏中传入映射选项。
5. 注意事项
1、仅限 <script setup>
defineModel 宏只能在 <script setup> 中使用,普通 <script> 或组合式 setup() 中无法生效。
2、编译阶段工具链要求
需升级 @vue/compiler-sfc—@vitejs/plugin-vue 至 3.4+,旧版本不识别宏。
3、无法在运行时动态声明
宏展开时机在编译,运行时无法条件化调用。它只适合静态声明。
4、调试困难
由于宏调用最终被删除,打包后无法在运行时代码中看到 defineProps、defineEmits,需在源码或产物源码上进行追踪。