Vue Vue2与Vue3的v-model

v-model 是 Vue 中使用频率特别高的一个指令,而 Vue3 中的 v-model 有了很大的变化,本文将详细讲述一下 Vue2 和 Vue3 中的 v-model 的区别。


Vue2 中的 v-model

如果对 Vue2 中的语法很熟悉,这部分可以不看。
首先来回顾一下 Vue2 中的 v-model,它主要用于表单元素和自定义组件上。v-model本质上是一个语法糖,会对用户的输入做一些特殊处理以达到更新数据,而所谓的处理其实就是给使用的元素默认绑定属性和事件。

当 v-model 使用在表单元素上时,会根据元素的不同而采用不同的处理:

  1. <input type="text">文本 和 <textarea>上使用时,会默认给元素绑定名为 value 的 prop 和名为 input 的事件;
  2. <input type="checkbox">复选框 和 <input type="radio">单选框 上使用时,会默认绑定名为 checked 的 prop 和名为 change 的事件;
  3. <select>选择框 上使用时,则绑定名为 value 的 prop 和名为 change 的事件。

这些是 Vue 默认帮我们处理的,可以直接使用。但是你也会发现一些第三方组件也可以使用 v-model ,比如 Element 中的 Input 组件。这是因为这些组件自己实现了 v-model,原理其实就是上面说到的绑定属性和事件。
我们可以尝试实现一下 v-model,来开发一个简单的输入组件,就叫 MyInput 吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- MyInput 组件代码 -->

<template>
<div>
<input type="text" :value="value" @input="$emit('input',$event.target.value)">
</div>
</template>

<script>
export default {
props: {
value: String, // 默认接收一个名为 value 的 prop
}
}
</script>

上面代码就实现了组件的 v-model 功能,当在这个组件上使用 v-model 时:

1
<my-input v-model="msg"></my-input>

其实就等同于:

1
<my-input :value="msg" @input="msg = $event">

Vue 还提供了 model 选项,用于将属性或事件名称改为其他名称,比如上面的 MyInput 组件,我们改一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
<input
type="text"
:value="title"
@input="$emit('change', $event.target.value)"
/>
</div>
</template>

<script>
export default {
model: {
prop: "title", // 将默认的 prop 名 value 改为 title
event: "change", // 将默认的事件名 input 改为 change
},
props: {
title: String, // 注意 template 代码中也要修改为 title
},
};
</script>

此时使用组件:

1
2
3
4
<my-input v-model="msg"></my-input>

// 等同于
<my-input :title="msg" @change="msg = $event"></my-input>

使用 .sync 修饰符

Vue 提供一个 .sync 的修饰符,效果跟 v-model 一样,也是便于子组件数据更改后自动更新父组件相关数据。实现 .sync 的方式与实现 v-model 异曲同工,区别就是抛出的事件名需要是 update:myPropName 的结构。
还是拿上面的 MyInput 说明,我们还是传入一个 title 的 prop,同时组件内部抛出 update:title 事件,代码如下:

1
2
// MyInput 组件中,修改抛出的事件名为 update:title
<input type="text" :value="title" @input="$emit('update:title', $event.target.value)" />

此时如果使用这个组件,正常应该是这样:

1
<my-input :title="msg" @update:title="msg = $event"></my-input>

但此时可以使用 .sync 修饰符来简化:

1
<my-input :title.sync="msg"></my-input>

可以看到 .sync 和 v-model 所能达到的效果是一样的,用什么就看你什么场景,一般表单组件上都是用 v-model。

Vue3 中的 v-model

上面说了那么多,为的就是接下来区别出 Vue3 中 v-model 带来的变化,主要变化有以下几处:

修改默认 prop 名和事件名

当用在自定义组件上时,v-model 默认绑定的 prop 名从 value 变为 modelValue,而事件名也从默认的input 改为 update:modelValue 。在 Vue3 中编写上面那个 MyInput 组件时,就需要这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- MyInput 组件代码 Vue3 版 -->

<template>
<div>
<input
type="text"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)" // 事件名改为 update:modelValue
/>
</div>
</template>

<script>
export default {
props: {
modelValue: String, // 默认 prop 从 value 改为 modelValue
},
};
</script>

使用组件时:

1
2
3
4
<my-input v-model="msg"></my-input>

// 等同于
<my-input :modelValue="msg" @update:modelValue="msg = $event"></my-input>

废除 model 选项和 .sync 修饰符

Vue3 中移除了 model 选项,这样就不可以在组件内修改默认 prop 名了。现在有一种更简单的方式,就是直接在 v-model 后面传递要修改的 prop 名:

1
2
3
4
5
// 要修改默认 prop 名,只需在 v-model 后面接上 :propName,例如修改为 title
<my-input v-model:title="msg"></my-input>

// 等同于
<my-input :title="msg" @update:title="msg = $event"></my-input>

注意组件内部也要修改 props:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
</div>
</template>

<script>
export default {
// 此时这里不需要 model 选项来修改了
props: {
title: String, // 修改为 title,注意 template 中也要修改
},
};
</script>

同时,.sync 修饰符也被移除了,如果你尝试使用它,会报这样的错误:

‘.sync’ modifier on ‘v-bind’ directive is deprecated. Use ‘v-model:propName’ instead

错误提示中说明了,可以使用 v-model:propName 的方式来替代 .sync,因为本质上效果是一样的。

使用多个 v-model

Vue3 中支持使用多个 v-model,属于新增功能,我很喜欢这个功能,使得组件数据更新更灵活。例如有这样一个表单子组件,用户输入的多个数据都需要更新到父组件中显示,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!--  表单子组件 Form -->

<template>
<div class="form">

<label for="name">姓名</label>
<input id="name" type="text" :value="name" @input="$emit('update:name',$event.target.value)">

<label for="address">地址</label>
<input id="address" type="text" :value="address" @input="$emit('update:address',$event.target.value)">

</div>
</template>

<script>
export default {
props:{
name: String,
address: String
}
}
</script>

父组件使用这个组件时:

1
2
3
4
5
<child-component v-model:name="name" v-model:address="address"></child-component>

// 将用户输入数据更新到父组件中显示
<p>{{name}}</p>
<p>{{address}}</p>

自定义 v-model 修饰符

在 Vue2 中的 v-model 上,我们用过 .trim、.lazy 和 .number这三个内置修饰符,而 Vue3 则在这个基础上增加了自定义修饰符,即开发者可以自定义修饰符,以按需处理绑定值。
当我们在 v-model 后面加上自定义修饰符后,会通过名为 modelModifiers 的 prop 传递给子组件,子组件拿到这个修饰符名后,根据条件修改绑定值。我们来看一个例子,自定义一个修饰符 capitalize,用于将输入字符串的首字母大写。
假设自定义组件还是叫 MyInput,使用 v-model 时加上自定义修饰符 capitalize:

1
<my-input v-model.capitalize="msg"></my-input>

由于不是内置修饰符,所以需要我们自己在组件内部处理修饰符逻辑,编写组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!-- MyInput 组件 -->

<template>
<div>
<input type="text" :value="modelValue" @input="emitValue" />
</div>
</template>

<script>
export default {
props: {
modelValue: String,
modelModifiers: { // 自定义修饰符会默认传入这个 prop 中
type: Object,
default: () => ({}),
},
},
mounted() {
// 当组件 v-model 后面加上了自定义修饰符,组件内部会在 modelModifiers 上获取到修饰符状态
console.log(this.modelModifiers); // {capitalize: true}
},
methods: {
emitValue(e) {
let value = e.target.value;
// 如果使用了自定义修饰符,即状态为 true,就处理值
if (this.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1);
}
// emit value
this.$emit("update:modelValue", value);
},
},
};
</script>

这样就完成了一个将输入字符串首字母大写的v-model修饰符。

如果是 v-model 带上了参数,同时使用了自定义修饰符,比如这样:

1
<my-input v-model:title.capitalize="msg"></my-input>

那么传入组件内部的 prop 就不再是 modelModifiers 了,而是 titleModifiers。它的格式是 arg + ‘Modifiers’。此时这个组件应该这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!-- MyInput 组件 -->

<template>
<div>
<input type="text" :value="title" @input="emitValue" />
</div>
</template>

<script>
export default {
props: {
title: String, // modelValue -> title
titleModifiers: { // modelModifiers -> titleModifiers
type: Object,
default: () => ({}),
},
},
mounted() {
console.log(this.titleModifiers); // {capitalize: true}
},
methods: {
emitValue(e) {
let value = e.target.value;

// 如果使用了自定义修饰符,就处理值
if (this.titleModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1);
}
// emit value
this.$emit("update:title", value);
},
},
};
</script>

参考文章

v-model 在 Vue2 和 Vue3 中的区别