一、背景与概念
在 Vue 3.3+ 中,<script setup> 引入了一个新的语法糖 ------ defineModel 。
它用于简化父组件与子组件之间的 v-model 双向绑定 定义。例如:
xml
<script setup>
const model = defineModel()
</script>
等价于传统写法:
css
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
而本文的核心源码来自于 Vue 编译阶段中对 defineModel 的处理逻辑,即:
- 函数
processDefineModel:编译器前端在解析 AST 时处理defineModel()的语义; - 函数
genModelProps:在代码生成阶段产出最终的props结构。
二、源码整体结构概览
完整源码定义了两个核心导出:
javascript
export function processDefineModel(ctx, node, declId?): boolean
export function genModelProps(ctx): string | undefined
两者协同作用:
| 函数 | 作用阶段 | 功能概要 |
|---|---|---|
processDefineModel |
语法分析(AST 处理) | 识别并转换 defineModel 调用 |
genModelProps |
代码生成 | 输出最终的 props 对象声明字符串 |
三、核心原理分析
1. processDefineModel:AST 层的编译处理
(1)判断节点类型
kotlin
if (!isCallOf(node, DEFINE_MODEL)) return false
ctx.hasDefineModelCall = true
isCallOf(node, DEFINE_MODEL)检查该节点是否为defineModel(...)调用。- 若不是,直接跳过。
- 若是,则标记上下文
ctx.hasDefineModelCall = true,后续用于生成阶段。
💡
ctx即ScriptCompileContext,存储脚本编译上下文(如偏移量、绑定元信息、字符串源码修改器等)。
(2)解析类型参数与参数
ini
const type = node.typeParameters?.params[0]
const arg0 = node.arguments[0] && unwrapTSNode(node.arguments[0])
- 若存在
<T>泛型参数,则type捕获类型节点; unwrapTSNode用于移除 TypeScript AST 包裹(如TSAsExpression)。
(3)提取 model 名称与选项
ini
const hasName = arg0 && arg0.type === 'StringLiteral'
if (hasName) {
modelName = arg0.value
options = node.arguments[1]
} else {
modelName = 'modelValue'
options = arg0
}
- 若首参为字符串,则作为自定义模型名;
- 否则默认名称为
"modelValue"; options可能为对象字面量,如{ type: String, required: true }。
(4)重复校验与字符串提取
javascript
if (ctx.modelDecls[modelName]) {
ctx.error(`duplicate model name ${JSON.stringify(modelName)}`, node)
}
let optionsString = options && ctx.getString(options)
- 检查同名 model 是否已定义;
- 若重复则报错;
- 通过
ctx.getString()提取源代码字符串片段。
(5)移除运行时选项并生成 runtimeOptionNodes
lua
if (options && options.type === 'ObjectExpression' && !options.properties.some(p => p.type === 'SpreadElement' || p.computed)) {
...
ctx.s.remove(...)
runtimeOptionNodes.push(p)
}
- 遍历选项对象;
- 移除如
get/set等仅在运行时需要的属性; - 记录剩余属性以便后续生成运行时校验代码。
这一步的核心目的是 避免编译时与运行时选项冲突 ,并保留必要的 prop 选项用于校验与提示。
(6)注册模型声明与绑定类型
css
ctx.modelDecls[modelName] = { type, options: optionsString, runtimeOptionNodes, identifier: declId?.name }
ctx.bindingMetadata[modelName] = BindingTypes.PROPS
- 在上下文
modelDecls中注册模型声明; - 设置绑定类型为
PROPS,表明该变量源自props。
(7)重写调用为 useModel
less
ctx.s.overwrite(start + node.callee.start!, start + node.callee.end!, ctx.helper('useModel'))
ctx.s.appendLeft(..., `__props, ...`)
编译器会把:
ini
const model = defineModel()
转译为:
ini
const model = useModel(__props, "modelValue")
这样在运行时即可自动与父组件的 v-model 建立绑定。
2. genModelProps:生成 props 声明代码
生成的结构形如:
css
{
modelValue: { type: String },
modelModifiers: {}
}
核心逻辑:
typescript
for (const [name, { type, options }] of Object.entries(ctx.modelDecls)) {
const runtimeTypes = inferRuntimeType(ctx, type)
const codegenOptions = ...
modelPropsDecl += `${JSON.stringify(name)}: ${decl},`
modelPropsDecl += `${modifierPropName}: {},`
}
- 类型推断:
inferRuntimeType(ctx, type) 会根据 TypeScript 类型推断出对应的运行时类型,如:
| TS 类型 | 推断结果 |
|---|---|
string |
'String' |
boolean |
'Boolean' |
Function |
'Function' |
- 合并选项:
根据 isProd 判断环境:
- 在开发模式下保留类型与校验;
- 在生产模式可省略以节省体积;
- 若包含
Boolean或Function,则保留类型声明。
- 最终输出结构:
每个 defineModel 最终生成两项:
- 模型本身的 prop;
- 对应的
Modifiers空对象,用于.trim、.number等修饰符支持。
四、实践示例
输入:
typescript
const count = defineModel<number>('count', { type: Number, required: true })
编译输出(简化):
javascript
const count = useModel(__props, "count")
export const __props = {
count: { type: Number, required: true },
countModifiers: {},
}
五、与其他宏的对比
| 宏名 | 功能 | 绑定方向 | 生成结果 |
|---|---|---|---|
defineProps |
定义 props | 父 → 子 | props |
defineEmits |
定义 emits | 子 → 父 | emit |
defineModel |
双向绑定语法糖 | 双向 | props + emit |
defineExpose |
暴露组件实例属性 | 子 → 父 | expose() |
可见 defineModel 结合了 props 和 emits 的特性,简化了双向绑定的模板定义。
六、拓展与潜在问题
(1)拓展方向
- 支持多模型定义:
defineModel('foo'),defineModel('bar') - 结合类型系统推断,生成更强的 IDE 自动提示
(2)潜在问题
- 若直接传入动态对象字面量(含计算属性),可能无法安全静态分析;
- 若模型名重复定义,会导致上下文报错;
- TypeScript 推断时若类型未知,会退化为
'null'。
七、总结
processDefineModel 与 genModelProps 是 Vue <script setup> 编译器中处理 defineModel 宏的两大核心环节。
它们体现了 Vue 编译器"静态分析 → 源码变换 → 运行时桥接"的思想:
- 在编译阶段静态提取类型与选项;
- 自动生成
useModel调用; - 动态构造
props结构以实现v-model语义。
本文部分内容借助 AI 辅助生成,并由作者整理审核。