vue v-model 双向绑定
回顾从 vue2 到 vue3 v-model 双向绑定的写法变化
场景
v-model
双向绑定,用于处理表单输入绑定,类似于 react 中的受控组件。
// React 受控组件
function App() {
const [text, setText] = useState("");
return (
<>
<h3>{text}</h3>
<input
value={text}
onInput={(e) => {
setText(e.target.value);
}}
></input>
</>
);
}
vue 的 v-model 本质与 react 受控组件是一样的,只是加了一个语法糖封装。
vue2 表单 v-model
<template>
<div>
<h2>FullName: {{ fullName }}</h2>
<h3>Email: {{ email }}</h3>
<input v-model="firstName" />
<input :value="lastName" @input="(e) => (lastName = e.target.value)" />
<input v-model.trim="email" placeholder="your email here" />
</div>
</template>
<script>
export default {
name: "HelloWorld",
data() {
return {
firstName: "",
lastName: "",
email: "",
};
},
computed: {
fullName() {
return this.firstName + " " + this.lastName;
},
},
};
</script>
这个例子中,firstName 使用 v-model 的基础写法,lastName 是还原 v-model 的“本来面目”。
需要注意的是,这里对 input 标签,绑定的是 value
属性和 input
事件,不同的 input 标签类型,对应的属性和事件不同,详见官方文档。
email 数据添加了修饰符,可以做一些额外的处理
vue2 父子组件 v-model
下面这个案例展示对于自定义组件,如何使用 v-model。
在组件间使用 v-model,一个隐含的场景是,数据是由父组件提供的,子组件可能会修改数据,然后通知父组件更新数据。
不管是 vue 还是 react,都是单向数据流的设计,子组件不应该直接修改父组件给过来的数据,而是通知父组件,让父组件处理,完成所谓的双向绑定。
PS 如果数据本身就是子组件产生的,那直接通过事件告知父组件即可,这种场景没有双向绑定,也就不需要 v-model。
// Foo 组件,子组件
<template>
<div>
<!-- <input :value="value" @input="(e) => this.$emit('input', e.target.value)" /> -->
<input
:value="firstName"
@input="(e) => this.$emit('updateFristName', e.target.value)"
/>
<input
:value="lastName"
@input="(e) => this.$emit('update:lastName', e.target.value)"
/>
<input
:value="email"
@input="(e) => this.$emit('update:email', e.target.value.trim())"
placeholder="your email here"
/>
<p>{{ firstName }} {{ lastName }} {{ email }}</p>
</div>
</template>
<script>
export default {
name: "FooItem",
model: {
prop: "firstName",
event: "updateFristName",
},
props: {
// value: String,
firstName: String,
lastName: String,
email: {
type: String,
default: "https://www.cnblogs.com/jasongrass",
},
},
data() {
return {};
},
};
</script>
这里子组件中是没有任何 v-model 这个指令的,因为 v-model 有两个功能,一个是提供数据,一个是修改数据(在事件回调中),而子组件是不能修改父组件提供的数据的,会破坏单向数据流。
所以这里子组件只是通过 props 接受数据,需要修改数据时,只触发事件,具体的事件处理和数据的实际修改,在父组件中完成。
具体写法上,上面的子组件代码中,涉及到了三种写法。
子组件 1. 默认写法
在上面代码中被注释的部分,即默认的数据名称是 value
,默认的事件名称是 input
。
<input :value="value" @input="(e) => this.$emit('input', e.target.value)" />
子组件 2. 修改默认写法
默认写法有两个问题,一是不够语义化,在数据比较多的时候,value 具体的业务含义会很不直观,影响代码可读性;二是在其它场景下,可能不能满足需求,如使用单选框、复选框等不同的表单元素时。
此时就可以自定义,如上面的 firstName,默认的 v-model 双向绑定属性名称,变成了 firstName, 事件变成了 updateFristName。
model: {
prop: "firstName",
event: "updateFristName",
}
<input
:value="firstName"
@input="(e) => this.$emit('updateFristName', e.target.value)"
/>
子组件 3. 多个数据的双向绑定
这里就是 lastName 和 email 两个属性,不考虑事件触发,其实这就是两个普通的属性。
特殊之处在于,这里在期望数据改变时,触发 update:myPropName
事件,以通知父组件修改相关的数据。
<input
:value="lastName"
@input="(e) => this.$emit('update:lastName', e.target.value)"
/>
// FooContainer 组件,父组件
<template>
<div>
<h2>FullName: {{ fullName }}</h2>
<h3>Email: {{ email }}</h3>
<!-- :lastName.sync="lastName" -->
<FooItem
v-model="firstName"
:lastName="lastName"
@update:lastName="
(e) => {
lastName = e;
}
"
:email.sync="email"
></FooItem>
</div>
</template>
<script>
import FooItem from "./Foo.vue";
export default {
name: "FooContainer",
components: {
FooItem,
},
data() {
return {
firstName: "",
lastName: "",
email: "",
};
},
computed: {
fullName() {
return this.firstName + " " + this.lastName;
},
},
};
</script>
<style scoped></style>
父组件 1. 默认写法
如上面的 firstName,如果需要将父组件中的 firstName 数据,作为子组件的默认 v-model 数据绑定,直接写 v-model="firstName"
。
这样就会实现与子组件默认 model 的双向绑定
父组件 2. 修改默认写法
修改默认写法,是针对子组件而言的。对于父组件,只要是绑定子组件的 model(因为只有一个),写法就是 v-model="firstName"
父组件 3. 多个数据的双向绑定
如这里的 lastName 和 email 数据,多个数据的绑定,可以对 v-bind 使用 .sync
修饰符。
本质上就是以下写法的语法糖
<FooItem
:lastName="lastName"
@update:lastName="
(e) => {
lastName = e;
}
"
></FooItem>
vue3 v-model 的变化
主要变化体现在自定义组件的 v-model 上,vue2 中一个组件只有一个 model 定义,其它的是通过 v-bind 的 .sync 修饰符来实现的。
在语法上容易混淆 v-model 和 v-bind 的用法,不是很直观。
以下是对变化的总体概述:
- 非兼容:用于自定义组件时,v-model prop 和事件默认名称已更改:
prop:value -> modelValue;
事件:input -> update:modelValue; - 非兼容:v-bind 的 .sync 修饰符和组件的 model 选项已移除,可在 v-model 上加一个参数代替;
- 新增:现在可以在同一个组件上使用多个 v-model 绑定;
- 新增:现在可以自定义 v-model 修饰符。
vue3 表单 v-model
这部分没有什么变化,详见文档:表单输入绑定 | Vue.js
<template>
<div>
<div>
<h2>FullName: {{ fullName }}</h2>
<h3>Email: {{ email }}</h3>
<input v-model="firstName" />
<input :value="lastName" @input="(e) => (lastName = e.target.value)" />
<input v-model.trim="email" placeholder="your email here" />
</div>
</div>
</template>
<script setup>
import { ref, reactive, toRefs, computed } from "vue";
const info = reactive({
firstName: "",
lastName: "",
email: "",
});
const { firstName, lastName, email } = toRefs(info);
const fullName = computed(() => {
return firstName.value + " " + lastName.value;
});
</script>
vue3 父子组件 v-model
在 vue 3.4 版本之后,使用了 defineModel 宏,处理 v-model 双向绑定写法上就简单多了。
// Foo 组件,子组件
<template>
<div>
<input :value="model" @input="(e) => (model = e.target.value)" />
<input v-model="model" />
<input :value="lastName" @input="(e) => (lastName = e.target.value)" />
<input v-model="lastName" />
<input :value="email" @input="updateEmail" placeholder="your email here" />
<p>{{ model }} {{ lastName }} {{ email }}</p>
</div>
</template>
<script setup>
const model = defineModel();
const lastName = defineModel("lastName");
const [email, emailModifiers] = defineModel("email");
const updateEmail = (e) => {
const inputValue = e.target.value;
if (emailModifiers.upper) {
console.log(inputValue);
email.value = inputValue ? inputValue.toUpperCase() : "";
} else {
email.value = inputValue;
}
};
</script>
<style lang="less" scoped></style>
子组件 1. 默认写法
model 定义
const model = defineModel();
model 使用
<input v-model="model" />
默认写法就是在使用 defineModel 时,不指定 model 的名称,则内部默认名称是 modelValue
, 对应的更新事件名称是 update:modelValue
, 但这两个默认名称,都不需要体现在代码中。
代码中直接使用 defineModel 的返回值,可以自定义命名,如这里是 model,它是一个 ref, 可以直接读取或修改,如果是修改,则底层会自动调用 update:modelValue
事件,通知父组件处理。
注意,这里在子组件中,可以直接使用 v-model,而不是必须写成 <input :value="model" @input="(e) => (model = e.target.value)" />
这样手动绑定 value 和 触发事件 的方式。因为这里 v-model 绑定的是一个 ref 代理,内部在修改数据时,没有真实修改数据,而是触发事件。
在 vue3.4 之前,不支持这样写的时候,可以自定义一个计算属性,将 input 标签的 value 绑定到这个计算属性中, 计算属性的 get 方法中返回 model, 计算属性的 set 方法中,触发 update:modelValue 事件。但这样还是需要手动添加并封装一个计算属性。
代码上省心很多,但这里仍然遵守数据单向流的设计原则(虽然看起来像是直接在修改数据),如果父组件不对事件做处理(当然,通常父组件对事件的处理,也是被自动封装在了 v-model 指令中),则子组件对数据的“修改”,也是无效的。
子组件 2&3. 修改默认写法 和 多个 v-model
在使用 defineModel 之后,不管是默认写法,还是定义多个 v-model,都进行了风格上的统一。直接使用 defineModel 定义即可。
const lastName = defineModel("lastName");
子组件,处理自定义修饰符
<script setup>
const [email, emailModifiers] = defineModel("email");
const updateEmail = (e) => {
const inputValue = e.target.value;
if (emailModifiers.upper) {
console.log(inputValue);
email.value = inputValue ? inputValue.toUpperCase() : "";
} else {
email.value = inputValue;
}
};
</script>
// FooContainer 组件,父组件
<template>
<div>
<h2>FullName: {{ fullName }}</h2>
<h3>Email: {{ email }}</h3>
<Foo
v-model="fristName"
v-model:lastName="lastName"
v-model:email.upper="email"
></Foo>
</div>
</template>
<script setup>
import { computed, ref } from "vue";
import Foo from "./Foo.vue";
const fristName = ref("");
const lastName = ref("");
const email = ref("");
const fullName = computed(() => {
return fristName.value + " " + lastName.value;
});
</script>
父组件的写法也简单直接了很多,对于默认 model, 直接使用 v-model="fristName"
这样的方式绑定,对于其它命名的 model, 使用 v-model:lastName="lastName"
进行绑定。
v-model 内部自动处理了监听子组件对应事件,并修改对应数据的操作。
总结
vue 3.4 之后,对 v-model 进行了很多优化,引入 defineModel 统一了 vue2 各种 model 的写法,方便地支持了多个 v-model。
但仍然需要注意,本质上 v-model 还是没有改变单向数据流这个设计原则,只是实现细节被封装起来了,在开发中需要有这个意识。
参考文档
Vue2
表单输入绑定 — Vue.js
组件 v-model | Vue.js
自定义组件的 v-model & .sync 修饰符 — Vue.js
Vue3
v-model | Vue 3 迁移指南
表单输入绑定 | Vue.js
组件 v-model | Vue.js