Vue 2 与 Vue 3 中 v-model 的对比以及源码分析 案例实践

第一章:核心概念 - 什么是 v-model
v-model 是 Vue 提供的一个语法糖,用于在表单输入元素或自定义组件上实现双向数据绑定。
-
它的本质:
v-model会根据所使用的元素类型,自动扩展为不同的属性绑定和事件监听。 -
它的目的:简化手动连接值和事件监听器的过程,让开发者更专注于数据逻辑而非 DOM 操作。
基本工作原理:
-
数据 -> 视图:将数据绑定到元素的
value(或checked,selected) 属性上。 -
视图 -> 数据:监听元素的
input(或change) 事件,当事件触发时,将最新的值更新到数据中。
第二章:Vue 2 中的 v-model
2.1 在原生表单元素上的使用
在原生输入元素上,Vue 2 的 v-model 行为非常直观,会根据标签类型选择正确的属性和事件。
<input v-model="message" type="text">
<p>Message is: {{ message }}</p>
<!-- 等价于 -->
<input :value="message" @input="message = $event.target.value">
<p>Message is: {{ message }}</p>
<!-- 复选框 -->
<input v-model="isChecked" type="checkbox">
<!-- 等价于 -->
<input :checked="isChecked" @change="isChecked = $event.target.checked">
<!-- 单选框 -->
<input v-model="picked" type="radio" value="one">
<!-- 等价于 -->
<input :checked="picked === 'one'" @change="picked = $event.target.value">
<!-- 下拉选择框 -->
<select v-model="selected">
<option value="a">A</option>
</select>
<!-- 等价于 -->
<select :value="selected" @change="selected = $event.target.value">
<option value="a">A</option>
</select>
2.2 在自定义组件上的使用
在 Vue 2 中,一个组件上的 v-model 默认会利用一个名为 value 的 prop 和一个名为 input 的事件。
父组件 (Parent.vue)
<template>
<ChildComponent v-model="pageTitle" />
</template>
<script>
export default {
data() {
return {
pageTitle: 'Hello World'
}
}
}
</script>
子组件 (ChildComponent.vue)
<template>
<input
type="text"
:value="value" <!-- 接收名为 value 的 prop -->
@input="$emit('input', $event.target.value)" <!-- 发射名为 input 的事件 -->
>
</template>
<script>
export default {
props: ['value'] // 必须声明 value prop
}
</script>
上面的代码等价于:
<ChildComponent :value="pageTitle" @input="pageTitle = $event" />
2.3 Vue 2 的 .sync 修饰符
有时,需要对一个 prop 进行“双向绑定”,但又不希望用 v-model 的 value 和 input 约定。为此,Vue 2 引入了 .sync 修饰符。
父组件
<ChildComponent :title.sync="pageTitle" />
<!-- 等价于 -->
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
子组件
需要通过发射一个 update:myPropName 的事件来更新父组件的数据。
this.$emit('update:title', newTitle)
Vue 2 的局限:
-
一个组件只能有一个
v-model:因为默认只能绑定到value这个 prop 上。 -
命名冲突:如果组件需要一个真正的
valueprop 用于其他目的,就会与v-model的约定产生冲突。 -
不够灵活:
v-model和.sync是两套不同的机制,容易混淆。
第三章:Vue 3 中的 v-model
Vue 3 对 v-model 进行了重大改进,旨在解决 Vue 2 中的局限性,提供更大的灵活性和一致性。
3.1 在原生表单元素上的使用
在原生元素上,Vue 3 的 v-model 用法与 Vue 2 完全一致,没有破坏性变化。
<input v-model="message" type="text">
<textarea v-model="message"></textarea>
<input v-model="checked" type="checkbox">
<select v-model="selected">
<option>A</option>
</select>
3.2 在自定义组件上的使用 (重大变化)
Vue 3 中,组件上的 v-model 默认不再使用 value prop 和 input 事件。
新的默认约定:
-
Prop:
modelValue -
事件:
update:modelValue
父组件 (Parent.vue)
<template>
<ChildComponent v-model="pageTitle" />
</template>
<script>
export default {
data() {
return {
pageTitle: 'Hello World'
}
}
}
</script>
子组件 (ChildComponent.vue)
<template>
<input
type="text"
:value="modelValue" <!-- 接收名为 modelValue 的 prop -->
@input="$emit('update:modelValue', $event.target.value)" <!-- 发射 update:modelValue 事件 -->
>
</template>
<script>
export default {
props: ['modelValue'], // 必须声明 modelValue prop
emits: ['update:modelValue'] // 显式声明发射的事件更好
}
</script>
上面的代码等价于:
<ChildComponent :modelValue="pageTitle" @update:modelValue="pageTitle = $event" />
3.3 Vue 3 的强大功能:多个 v-model
这是 Vue 3 解决 Vue 2 核心局限性的杀手锏。你现在可以在一个组件上绑定多个 v-model。
父组件
<UserName
v-model:first-name="firstName"
v-model:last-name="lastName"
/>
<!-- 等价于 -->
<UserName
:first-name="firstName"
@update:first-name="firstName = $event"
:last-name="lastName"
@update:last-name="lastName = $event"
/>
子组件 (UserName.vue)
<template>
<input
type="text"
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
>
<input
type="text"
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
>
</template>
<script>
export default {
props: {
firstName: String,
lastName: String
},
emits: ['update:firstName', 'update:lastName']
}
</script>
3.4 自定义 v-model 修饰符
Vue 3 允许你为自定义组件的 v-model 创建修饰符(如 v-model.trim 用于原生输入框)。
假设想创建一个 capitalize 修饰符,它自动将输入的首字母大写。
父组件
<MyComponent v-model.capitalize="myText" />
子组件 (MyComponent.vue)
<template>
<input
type="text"
:value="modelValue"
@input="emitValue($event.target.value)"
>
</template>
<script>
export default {
props: {
modelValue: String,
modelModifiers: { // 默认的修饰符对象,名为 modelModifiers
default: () => ({}) // 默认是一个空对象
}
},
emits: ['update:modelValue'],
methods: {
emitValue(value) {
let formattedValue = value;
// 检查 modifiers 对象中是否有 capitalize 属性且为 true
if (this.modelModifiers.capitalize) {
formattedValue = value.charAt(0).toUpperCase() + value.slice(1);
}
// 将格式化后的值发射出去
this.$emit('update:modelValue', formattedValue);
}
}
}
</script>
对于带参数的 v-model (如 v-model:firstName),修饰符对象的名称为 arg + "Modifiers":
<!-- 父组件 -->
<UserName v-model:first-name.capitalize="firstName" />
<!-- 子组件 -->
<script>
export default {
props: {
firstName: String,
firstNameModifiers: { // firstName + Modifiers
default: () => ({})
}
},
// ...
}
</script>
第四章:对比总结与迁移指南
| 特性 | Vue 2 | Vue 3 | 说明 |
|---|---|---|---|
| 默认 Prop | value | modelValue | Vue 3 使用了新的默认 prop 名。 |
| 默认事件 | input | update:modelValue | Vue 3 使用了新的事件名。 |
| 多个 v-model | 不支持。需使用 .sync 修饰符。 | 原生支持。v-model:arg。 | Vue 3 彻底解决了单组件多绑定的问题。 |
| .sync 修饰符 | 存在且常用。 | 已废弃。其功能被 v-model:arg 替代。 | 在 Vue 3 中,v-model:title 等同于 Vue 2 的 :title.sync。 |
| 自定义修饰符 | 可通过 model 选项配置,但较复杂。 | 通过 modelModifiers 或 argModifiers prop 轻松访问。 | Vue 3 的 API 设计更加直观和强大。 |
Vue 2 到 Vue 3 的迁移策略
-
将代码中的
.sync部分替换为v-model::-
:title.sync="pageTitle"->v-model:title="pageTitle"
-
-
将所有自定义组件的
v-model进行调整:-
Prop: 将
value改为modelValue。 -
事件: 将
this.$emit('input', payload)改为this.$emit('update:modelValue', payload)。
-
-
对于需要兼容 Vue 2 的组件,可以使用 Vue 3 的兼容配置(但通常建议为 Vue 3 创建新组件):
// 在 Vue 3 组件中,如果想同时支持 Vue 2 的语法 export default { props: ['modelValue', 'value'], // 同时接收两种 prop computed: { // 内部使用一个计算属性来统一数据源 internalValue: { get() { return this.modelValue ?? this.value; }, set(value) { // 优先发射 Vue 3 的事件 this.$emit('update:modelValue', value); // 同时兼容发射 Vue 2 的事件 this.$emit('input', value); } } } }
第五章 Vue 2 与 Vue 3 中 v-model 的源码级对比分析
实现思路
-
创建对比展示界面,左侧Vue 2,右侧Vue 3
-
实现基础用法、组件用法和修饰符用法的示例
-
添加源码解析部分展示关键实现代码
-
使用可视化方式展示v-model的数据流
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue 2 vs Vue 3 v-model 源码分析</title>
<script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
<script src="https://unpkg.com/vue@3.2.31/dist/vue.global.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
h1 {
color: #2c3e50;
margin-bottom: 10px;
}
.subtitle {
color: #7f8c8d;
font-size: 1.2em;
}
.comparison {
display: flex;
gap: 20px;
margin-bottom: 30px;
}
@media (max-width: 900px) {
.comparison {
flex-direction: column;
}
}
.vue2-section, .vue3-section {
flex: 1;
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.section-header {
background: #42b983;
color: white;
padding: 15px;
text-align: center;
font-size: 1.4em;
font-weight: bold;
}
.vue2-section .section-header {
background: #42b983;
}
.vue3-section .section-header {
background: #3498db;
}
.content {
padding: 20px;
}
.example {
margin-bottom: 25px;
padding: 15px;
border-radius: 8px;
background: #f8f9fa;
}
h3 {
margin-bottom: 15px;
color: #2c3e50;
border-bottom: 2px solid #eee;
padding-bottom: 8px;
}
.code-block {
background: #2d2d2d;
color: #f8f8f2;
padding: 15px;
border-radius: 5px;
overflow-x: auto;
margin: 15px 0;
font-family: 'Fira Code', monospace;
font-size: 0.9em;
}
.demo {
padding: 15px;
background: white;
border-radius: 8px;
margin: 15px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
input, button {
padding: 10px;
margin: 5px;
border: 1px solid #ddd;
border-radius: 4px;
width: 100%;
}
button {
background: #42b983;
color: white;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background: #359f74;
}
.source-analysis {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
margin-bottom: 30px;
}
.conclusion {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
text-align: center;
}
.highlight {
color: #e96900;
font-weight: bold;
}
.data-flow {
display: flex;
justify-content: space-around;
align-items: center;
margin: 20px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.arrow {
font-size: 24px;
color: #42b983;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Vue 2 与 Vue 3 中 v-model 的源码分析</h1>
<p class="subtitle">从实现原理到使用方式的全面对比</p>
</header>
<div class="comparison">
<div class="vue2-section">
<div class="section-header">Vue 2</div>
<div class="content">
<div class="example">
<h3>基础用法</h3>
<div class="code-block">
<input v-model="message" placeholder="编辑我">
<p>消息是: {{ message }}</p>
</div>
<div class="demo">
<div id="vue2-app">
<input v-model="message" placeholder="编辑我">
<p>消息是: {{ message }}</p>
</div>
</div>
</div>
<div class="example">
<h3>组件上的使用</h3>
<div class="code-block">
<custom-input v-model="searchText"></custom-input>
<p>搜索内容: {{ searchText }}</p>
</div>
<div class="demo">
<div id="vue2-component">
<custom-input v-model="searchText"></custom-input>
<p>搜索内容: {{ searchText }}</p>
</div>
</div>
</div>
<div class="example">
<h3>修饰符</h3>
<div class="code-block">
<input v-model.trim="trimmedText" placeholder="输入前后有空格">
<p>修剪后: "{{ trimmedText }}"</p>
</div>
<div class="demo">
<div id="vue2-modifiers">
<input v-model.trim="trimmedText" placeholder="输入前后有空格">
<p>修剪后: "{{ trimmedText }}"</p>
</div>
</div>
</div>
</div>
</div>
<div class="vue3-section">
<div class="section-header">Vue 3</div>
<div class="content">
<div class="example">
<h3>基础用法</h3>
<div class="code-block">
<input v-model="message" placeholder="编辑我">
<p>消息是: {{ message }}</p>
</div>
<div class="demo">
<div id="vue3-app">
<input v-model="message" placeholder="编辑我">
<p>消息是: {{ message }}</p>
</div>
</div>
</div>
<div class="example">
<h3>组件上的使用</h3>
<div class="code-block">
<custom-input v-model="searchText"></custom-input>
<p>搜索内容: {{ searchText }}</p>
</div>
<div class="demo">
<div id="vue3-component">
<custom-input v-model="searchText"></custom-input>
<p>搜索内容: {{ searchText }}</p>
</div>
</div>
</div>
<div class="example">
<h3>多个 v-model</h3>
<div class="code-block">
<user-name
v-model:first-name="firstName"
v-model:last-name="lastName"
></user-name>
<p>全名: {{ firstName }} {{ lastName }}</p>
</div>
<div class="demo">
<div id="vue3-multiple">
<user-name
v-model:first-name="firstName"
v-model:last-name="lastName"
></user-name>
<p>全名: {{ firstName }} {{ lastName }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="source-analysis">
<h3>源码实现解析</h3>
<div class="example">
<h4>Vue 2 v-model 实现</h4>
<div class="code-block">
// 简化的Vue 2 v-model实现
function transformModel(options, data) {
const prop = (options.model && options.model.prop) || 'value';
const event = (options.model && options.model.event) || 'input';
// 在data.props中添加prop
(data.props || (data.props = {}))[prop] = data.model.value;
// 在data.on中添加事件监听
(data.on || (data.on = {}))[event] = function(value) {
this.$emit(event, value);
};
}
</div>
<p>Vue 2 的 v-model 在编译阶段被转换为 value prop 和 input 事件。</p>
</div>
<div class="example">
<h4>Vue 3 v-model 实现</h4>
<div class="code-block">
// 简化的Vue 3 v-model实现
export const vModelText = {
created(el, { value, modifiers: { lazy, trim, number } }, vnode) {
// 获取数据设置函数
el._assign = getModelAssigner(vnode);
// 添加事件监听
const event = lazy ? 'change' : 'input';
el.addEventListener(event, e => {
let value = e.target.value;
if (trim) {
value = value.trim();
}
if (number) {
value = toNumber(value);
}
el._assign(value);
});
// 初始化值
if (value != null) {
el.value = value;
}
},
beforeUpdate(el, { value, oldValue }) {
if (value !== oldValue) {
el.value = value;
}
}
};
</div>
<p>Vue 3 的 v-model 使用指令实现,支持更多内置功能,并且默认使用 modelValue prop 和 update:modelValue 事件。</p>
</div>
<div class="data-flow">
<div>数据</div>
<div class="arrow">⇩</div>
<div>v-model 指令</div>
<div class="arrow">⇩</div>
<div>UI 组件</div>
<div class="arrow">⇩</div>
<div>用户输入</div>
<div class="arrow">⇩</div>
<div>更新数据</div>
</div>
</div>
<div class="conclusion">
<h3>总结</h3>
<p>Vue 2 的 v-model 主要是语法糖,将 value prop 和 input 事件绑定在一起。</p>
<p>Vue 3 的 v-model 进行了重大改进,支持多个 v-model 绑定,默认使用 modelValue 和 update:modelValue,</p>
<p>并且通过指令实现提供了更好的灵活性和功能扩展性。</p>
</div>
</div>
<!-- Vue 2 实现 -->
<script>
// Vue 2 基础示例
new Vue({
el: '#vue2-app',
data: {
message: 'Hello Vue 2!'
}
});
// Vue 2 自定义组件
Vue.component('custom-input', {
props: ['value'],
template: `
<div>
<input
:value="value"
@input="$emit('input', $event.target.value)"
placeholder="输入搜索内容"
>
</div>
`
});
new Vue({
el: '#vue2-component',
data: {
searchText: 'Vue 2 搜索'
}
});
// Vue 2 修饰符示例
new Vue({
el: '#vue2-modifiers',
data: {
trimmedText: ''
}
});
</script>
<!-- Vue 3 实现 -->
<script>
// Vue 3 基础示例
Vue.createApp({
data() {
return {
message: 'Hello Vue 3!'
}
}
}).mount('#vue3-app');
// Vue 3 自定义组件
const app = Vue.createApp({});
app.component('custom-input', {
props: ['modelValue'],
emits: ['update:modelValue'],
template: `
<div>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
placeholder="输入搜索内容"
>
</div>
`
});
app.mount('#vue3-component');
// Vue 3 多个v-model示例
const multipleApp = Vue.createApp({
data() {
return {
firstName: '张',
lastName: '三'
}
}
});
multipleApp.component('user-name', {
props: {
firstName: String,
lastName: String
},
emits: ['update:firstName', 'update:lastName'],
template: `
<div>
<input
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
placeholder="姓"
>
<input
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
placeholder="名"
>
</div>
`
});
multipleApp.mount('#vue3-multiple');
</script>
</body>
</html>
关键特性说明
-
Vue 2 v-model:
-
基于value属性和input事件
-
在组件上使用需要手动实现value prop和input事件
-
通过编译阶段转换实现
-
-
Vue 3 v-model:
-
基于modelValue属性和update:modelValue事件
-
支持多个v-model绑定
-
通过指令实现,提供更好的灵活性和扩展性
-
内置.trim、.number等修饰符处理
-
-
源码分析:
-
展示了简化版的Vue 2和Vue 3 v-model实现
-
Vue 2通过编译时转换,Vue 3通过指令实现
-
可视化数据流展示了v-model的工作原理
-
结论
Vue 3 的 v-model 是对 Vue 2 版本的一次全面升级。它通过引入 modelValue/update:modelValue 的默认约定、支持多个 v-model 绑定以及提供更强大的自定义修饰符功能,解决了 Vue 2 中的主要痛点,让 API 变得更加一致、灵活和强大。虽然带来了一些 breaking changes,但迁移路径清晰,且最终能带来更简洁、更可维护的代码。
更多推荐
所有评论(0)