Appearance
v-model:双向绑定的实现原理与演进
v-model 是 Vue 中用于实现双向数据绑定的指令。它本质上是一个“语法糖”,简化了父子组件之间的数据传递和事件监听。
本节我们将深入 v-model 的编译和运行时源码,分析它的工作原理,以及它从 Vue 2 的 .sync 到 Vue 3.4 defineModel 的演进过程。
v-model 的本质:一个 Prop + 一个 Event
v-model 的核心是“一个 prop + 一个 event”的约定。
默认
v-model:v-model="myState"等价于::modelValue="myState" @update:modelValue="myState = $event"带参数的
v-model(取代了 Vue 2 的.sync):v-model:title="myTitle"等价于::title="myTitle" @update:title="myTitle = $event"
Vue 3 废弃了 .sync 修饰符,统一使用 v-model,并支持在同一个组件上使用多个 v-model,增强了灵活性。
编译时(transformModel):转换指令
当编译器在模板中遇到 v-model 指令时,它的工作是将 v-model 转换成等价的 prop 和 event。
这个工作由 transformModel 插件(compiler-core/src/transforms/vModel.ts)完成。
typescript
// core/packages/compiler-core/src/transforms/vModel.ts
export const transformModel: DirectiveTransform = (dir, node, context) => {
const { exp, arg } = dir // 'exp' 是绑定的值 'myTitle','arg' 是参数 'title'
if (!exp) {
// 必须有 v-model="expression"
}
// 1. 【确定 Prop 名称】
// 如果是 v-model:title,propName 是 'title'
// 如果是 v-model,propName 是 'modelValue'
const propName = arg ? arg : createSimpleExpression('modelValue', true)
// 2. 【确定 Event 名称】
// v-model:title -> 'onUpdate:title'
// v-model -> 'onUpdate:modelValue'
const eventName = arg
? isStaticExp(arg)
? `onUpdate:${camelize(arg.content)}`
: createCompoundExpression(['"onUpdate:" + ', arg])
: `onUpdate:modelValue`
// 3. 【创建赋值表达式】
// 生成 $event => (myTitle = $event) 对应的 AST 节点
const assignmentExp = createCompoundExpression([
`$event => ((`, exp, `) = $event)`
])
// 4. 【返回 props】
// 将 v-model 转换成两个 prop
const props = [
// :title="myTitle"
createObjectProperty(propName, dir.exp!),
// @update:title="myTitle = $event"
createObjectProperty(eventName, assignmentExp),
]
// ... (处理 .lazy, .trim, .number 等修饰符,
// 它们会被编译成 "modelModifiers" prop 传给子组件)
return createTransformProps(props)
}transformModel 是理解 v-model 的第一步。它在编译时就将 v-model 转换成了组件通用的 props 和 events 接口。
子组件(setup):实现 v-model 接口的演进
transformModel 在父组件完成了转换。那么在子组件中,我们如何“实现”这个 prop 和 event 的约定呢?
传统方式:props + emit
在 Vue 3.4 之前,我们必须手动实现这个约定:
vue
<script setup>
const props = defineProps({
modelValue: String // 1. 接收 :modelValue prop
})
const emit = defineEmits([
'update:modelValue' // 2. 声明 'update:modelValue' event
])
function onInput(e) {
// 3. 在输入时,emit 事件,将新值传回父组件
emit('update:modelValue', e.target.value)
}
</script>
<template>
<input :value="modelValue" @input="onInput" />
</template>这个 props + emit 的“样板代码”虽然清晰,但比较繁琐。
现代方式:defineModel (Vue 3.4+)
defineModel 是一个编译器宏,它自动帮我们生成了上述所有的“样板代码”。
vue
<script setup>
// 这一行代码,自动完成了:
// 1. defineProps({ modelValue: ... })
// 2. defineEmits(['update:modelValue'])
// 3. 返回一个 ref,它 get 时读取 prop,set 时 emit 事件
const modelValue = defineModel()
</script>
<template>
<input v-model="modelValue" />
</template>defineModel 是 v-model 演进的最新成果,它极大地简化了子组件的实现。它还支持多个 v-model 和修饰符:
javascript
// 对应 v-model:title.capitalize="myTitle"
const [title, modifiers] = defineModel('title', {
set(value) {
// 自动处理修饰符
if (modifiers.capitalize) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
return value
}
})特例:v-model 用于原生 DOM 元素
当 v-model 用在组件上时,它被转换为 prop/event。但当它用在原生元素(如 <input>, <select>)上时,编译器会进行不同的处理。
为什么? 因为原生 DOM 元素的 v-model 逻辑更复杂:
<input type="text">:需要处理输入法(IME)的composition事件。<input type="checkbox">:需要处理数组(true-value/false-value)的push/splice。<input type="radio">:需要处理值的匹配。.lazy:需要将input事件切换为change事件。.trim,.number:需要转换值。
transformModel 会检测 node.tagType。如果它是原生 ELEMENT,compiler-dom 会介入:
typescript
// core/packages/compiler-dom/src/transforms/vModel.ts
export const transformModel: DirectiveTransform = (dir, node, context) => {
// 1. 先获取组件 v-model 的基础 props (modelValue, onUpdate:modelValue)
const baseResult = baseTransform(dir, node, context)
// 2. 如果是组件,到此为止,返回 prop/event
if (node.tagType === ElementTypes.COMPONENT) {
return baseResult
}
// 3. 【关键】如果是 <input>, <select>, <textarea>
if (tag === 'input' || tag === 'select' || tag === 'textarea') {
let directiveToUse = V_MODEL_TEXT // 默认
// 4. 根据 <input> 的 type 切换运行时指令
if (tag === 'input') {
const type = findProp(node, 'type')
if (type.value.content === 'checkbox') {
directiveToUse = V_MODEL_CHECKBOX
} else if (type.value.content === 'radio') {
directiveToUse = V_MODEL_RADIO
}
}
// 5. 【注入】告诉 codegen,需要 "v-model-text" 这个运行时指令
baseResult.needRuntime = context.helper(directiveToUse)
// 6. 【移除】移除 :modelValue prop,
// 因为 vModelText 指令会接管 value 的设置
baseResult.props = baseResult.props.filter(...)
return baseResult
}
}运行时:vModelText 等指令的实现
编译器最终生成的代码,不会有 :modelValue,而是: _withDirectives(_createElementVNode("input"), [ [_vModelText, _ctx.msg] ])
vModelText(runtime-dom/src/directives/vModel.ts)是一个运行时指令,它在 created 和 beforeUpdate 钩子中,手动完成了 DOM 的双向绑定:
typescript
// core/packages/runtime-dom/src/directives/vModel.ts
export const vModelText: ModelDirective = {
// 元素创建时
created(el, { modifiers: { lazy, trim } }, vnode) {
// 1. 获取父组件的 "setter" (即 $event => (foo = $event))
const assign = getModelAssigner(vnode)
// 2. 【处理 .lazy】决定监听 'input' 还是 'change'
const event = lazy ? 'change' : 'input'
addEventListener(el, event, e => {
// 3. 【处理 IME】如果正在中文输入,直接返回
if ((e.target as any).composing) return
let domValue = el.value
// 4. 【处理 .trim】
if (trim) domValue = domValue.trim()
// ... (处理 .number) ...
// 5. 【执行更新】调用 setter,更新父组件状态
assign(domValue)
})
// 6. 监听 IME 事件
if (!lazy) {
addEventListener(el, 'compositionstart', onCompositionStart)
addEventListener(el, 'compositionend', onCompositionEnd)
}
},
// 数据更新时
mounted(el, { value }) {
// 7. 【数据 -> 视图】将父组件的值设置到 DOM 上
el.value = value == null ? '' : value
},
// 视图更新时
beforeUpdate(el, { value }, vnode) {
// 8. 确保 DOM 上的值与状态同步
if (el.value !== newValue) {
el.value = newValue
}
},
}vModelCheckbox, vModelRadio, vModelSelect 也是类似的运行时指令,它们各自处理了更复杂的数组、Set 和 multiple 逻辑。
总结
v-model 的实现展现了 Vue“编译时优化,运行时灵活”的特点:
- 核心约定:
v-model始终是prop+event的简写。Vue 3 统一了.sync,使其支持多绑定。 - 路径一:组件
- 编译时:
transformModel将v-model转换为:modelValue和@update:modelValue。 - 子组件实现:
defineModel宏(Vue 3.4+)是实现此约定的最佳实践,它自动生成了props和emits的样板代码。
- 编译时:
- 路径二:原生 DOM
- 编译时:
transformModel(DOM 版)拦截v-model,将其转换为运行时指令(如vModelText)。 - 运行时:
vModelText指令通过其生命周期钩子,手动处理 DOM 事件(如input/change/IME)、应用修饰符(.lazy/.trim),并调用setter更新父组件状态。
- 编译时:
