各位靓仔靓女,晚上好!我是你们的老朋友,今天咱们来聊聊 Vue 3 源码里边那个磨人的小妖精——v-model
。
v-model
这玩意儿,用起来简单,双向绑定,数据咻咻咻地就同步了,但你真要深究它背后的实现,尤其是它在不同组件和原生元素上的差异,嘿嘿,那可就有点意思了。别慌,今晚我就把这层窗户纸给捅破,保证让你看得明明白白。
开场白:v-model
的“双面人生”
首先,v-model
这东西,它不是一个简单的语法糖。它会根据你绑定的对象,自动选择不同的实现方式。简单来说,它有“双面人生”:
-
绑定到原生 HTML 元素: 比如
<input>
,<textarea>
,<select>
等等。这时候,v-model
会监听元素的input
或change
事件(具体哪个事件取决于元素类型),并更新绑定的数据。 -
绑定到自定义组件: 这种情况下,
v-model
实际上是一个语法糖,它展开后相当于传递一个modelValue
prop,并监听一个update:modelValue
事件。
所以,我们要分别从这两个角度来看 v-model
的实现。
第一幕:原生 HTML 元素的 v-model
咱们先来看 v-model
是怎么跟原生 HTML 元素眉来眼去的。这里,我用最常见的 <input type="text">
元素来举例。
<template>
<input type="text" v-model="message">
<p>Message: {{ message }}</p>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const message = ref('');
return {
message
}
}
}
</script>
这段代码里,v-model
绑定了 message
这个响应式数据。当你修改输入框里的内容时,message
的值会同步更新,反之亦然。
幕后黑手:@vue/compiler-dom
要理解 v-model
的实现,我们需要稍微了解一下 Vue 的编译器。当 Vue 编译这段模板时,v-model
指令会被转换成一系列的属性和事件监听器。
对于 <input type="text">
来说,v-model="message"
实际上会被编译成类似下面的代码:
<input type="text" :value="message" @input="message = $event.target.value">
是不是有点豁然开朗的感觉?
:value="message"
:这部分负责将message
的值绑定到输入框的value
属性上,实现了数据的单向绑定(从数据到视图)。@input="message = $event.target.value"
:这部分监听了输入框的input
事件。当输入框的内容发生改变时,事件处理函数会获取新的值($event.target.value
),并更新message
的值,实现了数据的反向绑定(从视图到数据)。
更深层次的挖掘:patchProps
当然,这只是一个简化的版本。Vue 内部的实现要复杂得多。真正负责更新 DOM 属性的是 patchProps
函数,它是 Virtual DOM 更新算法的核心部分。
patchProps
会比较新旧 VNode 的属性,然后根据差异来更新 DOM 元素。对于绑定了 v-model
的元素,patchProps
会特别处理 value
属性和相应的事件监听器。
不同类型的原生元素:适配是关键
v-model
的实现并非对所有原生元素都一视同仁。它会根据元素的类型进行适配。
比如:
- 对于
<input type="checkbox">
,v-model
会监听change
事件,并且绑定的是checked
属性。 - 对于
<select>
,v-model
也会监听change
事件,并且绑定的是value
属性(或者selected
属性,如果使用了multiple
属性)。 - 对于
<textarea>
,v-model
的行为和<input type="text">
类似,都是监听input
事件并绑定value
属性。
为了更好地理解,我们用表格的形式来总结一下:
元素类型 | 监听事件 | 绑定的属性 |
---|---|---|
<input type="text"> |
input |
value |
<input type="checkbox"> |
change |
checked |
<input type="radio"> |
change |
checked |
<select> |
change |
value / selected |
<textarea> |
input |
value |
这种适配的目的是为了让 v-model
能够无缝地工作在各种原生 HTML 元素上,提供一致的开发体验。
第二幕:自定义组件的 v-model
接下来,我们来看看 v-model
是怎么和自定义组件打成一片的。
// MyInput.vue (自定义组件)
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
</template>
<script>
export default {
props: {
modelValue: String // 接收父组件传递的 modelValue
},
emits: ['update:modelValue'] // 声明组件会触发 update:modelValue 事件
}
</script>
// ParentComponent.vue (父组件)
<template>
<MyInput v-model="message" />
<p>Message: {{ message }}</p>
</template>
<script>
import { ref } from 'vue';
import MyInput from './MyInput.vue';
export default {
components: {
MyInput
},
setup() {
const message = ref('');
return {
message
}
}
}
</script>
在这个例子中,MyInput
是一个自定义组件,它接收一个 modelValue
prop,并触发一个 update:modelValue
事件。父组件使用 v-model
将 message
数据绑定到 MyInput
组件上。
语法糖的真相:modelValue
和 update:modelValue
对于自定义组件,v-model="message"
实际上会被编译成类似下面的代码:
<MyInput :modelValue="message" @update:modelValue="message = $event" />
看到了吗?v-model
实际上是以下两个步骤的简写:
- 传递
modelValue
prop: 将父组件的数据(message
)通过modelValue
prop 传递给子组件。 - 监听
update:modelValue
事件: 监听子组件触发的update:modelValue
事件,并在事件处理函数中更新父组件的数据(message
)。
modelModifiers
:v-model
的高级玩法
Vue 3 还引入了一个 modelModifiers
的概念,允许你自定义 v-model
的行为。
// MyInput.vue
<template>
<input
:value="modelValue"
@input="handleChange"
>
</template>
<script>
export default {
props: {
modelValue: String,
modelModifiers: { // 接收修饰符
default: () => ({})
}
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const handleChange = (event) => {
let value = event.target.value;
if (props.modelModifiers.trim) { // 如果有 .trim 修饰符
value = value.trim();
}
if (props.modelModifiers.uppercase) { // 如果有 .uppercase 修饰符
value = value.toUpperCase();
}
emit('update:modelValue', value);
};
return {
handleChange
};
}
}
</script>
// ParentComponent.vue
<template>
<MyInput v-model.trim.uppercase="message" />
<p>Message: {{ message }}</p>
</template>
<script>
import { ref } from 'vue';
import MyInput from './MyInput.vue';
export default {
components: {
MyInput
},
setup() {
const message = ref('');
return {
message
}
}
}
</script>
在这个例子中,我们在父组件中使用 v-model.trim.uppercase
,表示我们希望对输入的值进行 trim
和 uppercase
处理。子组件通过 modelModifiers
prop 接收这些修饰符,并在 handleChange
函数中应用它们。
modelModifiers
的编译结果:
v-model.trim.uppercase="message"
会被编译成类似下面的代码:
<MyInput :modelValue="message" @update:modelValue="message = $event" :modelModifiers="{ trim: true, uppercase: true }" />
可以看到,modelModifiers
实际上是一个对象,包含了所有修饰符的信息。
第三幕:深入源码(简略版)
虽然我们不可能在这里把 Vue 3 的源码全部看完,但我们可以稍微深入一下,看看 v-model
的核心逻辑在哪里。
compiler-dom
模块: 这个模块负责将模板编译成渲染函数。在编译过程中,v-model
指令会被转换成相应的属性和事件监听器。runtime-dom
模块: 这个模块负责操作 DOM。patchProps
函数是其中一个关键函数,它负责更新 DOM 元素的属性,包括value
、checked
等等。runtime-core
模块: 这个模块包含了 Vue 的核心逻辑,包括响应式系统、组件生命周期等等。v-model
的实现也依赖于这个模块提供的功能。
总结:v-model
的本质
总而言之,v-model
的本质是:
- 双向数据绑定: 它简化了数据和视图之间的同步过程。
- 语法糖: 它隐藏了底层的实现细节,让开发者可以更方便地使用。
- 适配器模式: 它根据不同的元素类型和组件类型,采用不同的实现方式,提供了统一的开发体验。
v-model
原理图
graph LR
A[用户输入] --> B{原生HTML元素?}
B -- 是 --> C[input/change事件]
B -- 否 --> D[自定义组件]
C --> E[更新value/checked属性]
E --> F[更新数据]
D --> G[触发update:modelValue事件]
G --> F
F --> A
一些小技巧和注意事项:
- 避免无限循环: 在使用
v-model
时,要小心避免无限循环。比如,在update:modelValue
事件处理函数中,如果直接修改了modelValue
的值,可能会导致无限循环。 - 使用计算属性: 如果需要对
v-model
绑定的值进行复杂的处理,可以使用计算属性。 - 自定义
v-model
: 虽然 Vue 提供了默认的v-model
实现,但你也可以自定义v-model
的行为。这需要你深入了解 Vue 的编译器和渲染机制。
结尾:v-model
的未来
v-model
是 Vue 中一个非常重要的特性,它极大地简化了开发过程。随着 Vue 的不断发展,v-model
也在不断进化。我相信,在未来,v-model
会变得更加强大和灵活,为开发者带来更好的体验。
好了,今天的讲座就到这里。希望大家有所收获! 咱们下期再见!