v-model 是 Vue 中最具代表性的双向绑定语法糖,它在运行时能自动管理表单输入与数据之间的同步。而在编译阶段,Vue 的模板编译器(@vue/compiler-dom)通过 transformModel 函数将 v-model 转换为运行时可识别的指令表达式。
本文我们将深入剖析源码:
python
import {
type DirectiveTransform,
ElementTypes,
NodeTypes,
transformModel as baseTransform,
findDir,
findProp,
hasDynamicKeyVBind,
isStaticArgOf,
} from '@vue/compiler-core'
import { DOMErrorCodes, createDOMCompilerError } from '../errors'
import {
V_MODEL_CHECKBOX,
V_MODEL_DYNAMIC,
V_MODEL_RADIO,
V_MODEL_SELECT,
V_MODEL_TEXT,
} from '../runtimeHelpers'
一、概念:transformModel 的角色定位
在 Vue 编译过程中,指令(如 v-model、v-html、v-bind)都会被编译器的 transform 阶段 转换成适合运行时的结构。
transformModel 是 DOM 编译器中专门处理 v-model 的指令转换函数,目标是:
- 判断绑定的元素类型(如
<input>、<select>)。 - 检查错误用法(如
v-model绑定到文件输入)。 - 注入运行时辅助函数(如
V_MODEL_TEXT、V_MODEL_CHECKBOX等)。
二、原理:编译时如何决定不同的绑定逻辑
1️⃣ 调用基础转换逻辑
ini
const baseResult = baseTransform(dir, node, context)
- 这里调用了核心模块
@vue/compiler-core的通用版本transformModel。 - 它会生成一个基础结果对象
{ props, needRuntime },为后续 DOM 特有的逻辑扩展打底。
2️⃣ 组件与普通元素的区分
ini
if (!baseResult.props.length || node.tagType === ElementTypes.COMPONENT) {
return baseResult
}
解释:
- 如果
v-model是在组件上(例如<MyInput v-model="x" />),编译器不会做额外 DOM 层级转换,只保留基础属性。- 普通元素则继续深入检查类型。
3️⃣ 检查非法参数使用
lua
if (dir.arg) {
context.onError(
createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_ARG_ON_ELEMENT,
dir.arg.loc,
),
)
}
v-model:foo="x"这种语法仅对组件有效,对原生元素无意义。
4️⃣ 检查重复绑定 value
scss
function checkDuplicatedValue() {
const value = findDir(node, 'bind')
if (value && isStaticArgOf(value.arg, 'value')) {
context.onError(
createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_UNNECESSARY_VALUE,
value.loc,
),
)
}
}
当用户在使用
v-model的同时又写了:value时,可能造成冲突或冗余,因此编译器在开发模式下会发出警告。
5️⃣ 识别元素类型并选择合适的运行时助手
arduino
const { tag } = node
const isCustomElement = context.isCustomElement(tag)
Vue 会为不同类型的元素绑定不同的指令运行时函数:
| 元素类型 | 对应运行时辅助符号 | 功能说明 |
|---|---|---|
<input type="text"> |
V_MODEL_TEXT |
文本输入双向绑定 |
<input type="checkbox"> |
V_MODEL_CHECKBOX |
多选框绑定 |
<input type="radio"> |
V_MODEL_RADIO |
单选绑定 |
<select> |
V_MODEL_SELECT |
下拉选择绑定 |
自定义组件或动态 :type |
V_MODEL_DYNAMIC |
动态类型运行时绑定 |
源码逻辑如下:
go
let directiveToUse = V_MODEL_TEXT
let isInvalidType = false
if (tag === 'input' || isCustomElement) {
const type = findProp(node, `type`)
if (type) {
if (type.type === NodeTypes.DIRECTIVE) {
directiveToUse = V_MODEL_DYNAMIC
} else if (type.value) {
switch (type.value.content) {
case 'radio':
directiveToUse = V_MODEL_RADIO
break
case 'checkbox':
directiveToUse = V_MODEL_CHECKBOX
break
case 'file':
isInvalidType = true
context.onError(
createDOMCompilerError(
DOMErrorCodes.X_V_MODEL_ON_FILE_INPUT_ELEMENT,
dir.loc,
),
)
break
default:
__DEV__ && checkDuplicatedValue()
}
}
} else if (hasDynamicKeyVBind(node)) {
directiveToUse = V_MODEL_DYNAMIC
} else {
__DEV__ && checkDuplicatedValue()
}
} else if (tag === 'select') {
directiveToUse = V_MODEL_SELECT
} else {
__DEV__ && checkDuplicatedValue()
}
6️⃣ 注入运行时指令引用
ini
if (!isInvalidType) {
baseResult.needRuntime = context.helper(directiveToUse)
}
此时,
baseResult.needRuntime会携带一个对运行时resolveDirective()的引用,使生成的渲染函数能在运行时调用正确的指令处理逻辑。
7️⃣ 移除编译期无用的 modelValue 属性
ini
baseResult.props = baseResult.props.filter(
p => !(p.key.type === NodeTypes.SIMPLE_EXPRESSION && p.key.content === 'modelValue')
)
原因:原生元素的
v-model不需要显式的modelValue传入,它会在运行时通过binding.value自动管理,因此删除以减少代码体积。
三、对比:@vue/compiler-core 与 @vue/compiler-dom
-
@vue/compiler-core:负责通用的 AST 构建与基础转换(组件/指令通用逻辑)。 -
@vue/compiler-dom:在此基础上为浏览器平台添加 DOM 特有的行为,例如:- 检查
<input type="file">这种非法绑定。 - 区分
checkbox、radio的运行时指令。 - 针对开发模式(
__DEV__)进行额外校验。
- 检查
四、实践:transformModel 实际输出示例
当编译如下模板:
ini
<input v-model="msg" type="text">
编译结果(简化版)大致为:
css
{
props: [],
needRuntime: helper(V_MODEL_TEXT)
}
渲染函数中会生成:
lua
withDirectives(
createElementVNode("input", null, null, 512 /* NEED_PATCH */),
[[vModelText, msg]]
)
最终由 vModelText 在运行时处理 input 的输入/输出同步。
五、拓展:动态输入类型的特殊处理
例如:
ini
<input :type="inputType" v-model="value">
此时编译器会检测到 type 是一个动态绑定(NodeTypes.DIRECTIVE),
自动切换到:
ini
directiveToUse = V_MODEL_DYNAMIC
运行时则会在输入类型变化时动态切换不同的监听逻辑。
六、潜在问题与边界
- 文件输入限制
v-model不支持<input type="file">,必须使用事件监听手动处理上传。 - 重复绑定冲突
若同时使用:value与v-model,可能导致值不一致问题。 - 自定义元素兼容性
对于 Web Components,需自定义isCustomElement逻辑,保证v-model的行为一致。
七、总结
transformModel 是 Vue 模板编译器中将 "语法糖" 翻译为 "运行时逻辑" 的关键节点。
它体现了 Vue 的一个核心设计哲学------在编译阶段智能决策,在运行时高效执行。
在理解了这段代码后,你不仅能掌握 v-model 的编译机制,还能更好地理解 Vue 模板编译的抽象层次:
从语法到 AST,从 AST 到渲染函数,再从渲染函数到最终 DOM 更新。
本文部分内容借助 AI 辅助生成,并由作者整理审核。