回顾从 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;
- 非兼容: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
事件。但这样还是需要手动添加并封装一个计算属性。
代码上省心很多,但这里仍然遵守数据单向流的设计原则(虽然看起来像是直接在修改数据),如果父组件不对事件做处理(当然,通常父组件对事件的处理,也是被自动封装在了 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
原文链接: https://blog.jgrass.cc/posts/vue-v-model-bind/
本作品采用 「署名 4.0 国际」 许可协议进行许可,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。