Vue3 中 v-model 的改动与扩展
家喻户晓的 v-model 不仅是开发中常用的指令,也是面试必问的经典。这里在回顾这个语法糖的同时,主要和大家聊聊 vue3 和 vue2 的 v-model 区别,以及如何在 TSX 中扩展 v-model。
v-model = v-bind + v-on
抛开“双向绑定”不谈,父子组件的通讯无非是传值的 v-bind 和 改值的 v-on,大家都很熟悉:
// 父组件:(1)传了个 title;(2)注册了个 update:modelValue 事件
<Son
v-bind:modelValue="title"
v-on:update:modelValue="title = $event"
>
// 子组件:(1)接收 modelValue;(2)调用 emit 修改
{
props: ['modelValue'],
emits: ['update:modelValue']
methdos: {
change() {
this.$emit('update:modelValue', 'haha')
}
}
}
上例中,子组件在调用自身的 change 事件时(比如子组件将 change 事件挂载到内部原生的 input 元素),就会触发 update:modelValue
,父组件就会从中拿到子组件传递过来的值,去修改父组件自身的数据。
为了方便起见,Vue 提供了指令供父组件直接使用,于是最常见的 v-model
就诞生了:
<Son v-model="title">
值得一提的是,这里的事件名 update:modelValue
虽说看着有些奇怪,但在代码背后,这正是模板编译器对 v-model
的一种冗长的等价展开,你可以理解为是一种默认的约定。这也是为什么在 Vue2 中 v-model
只能绑定一个值的原因,因为它只有一个叫 modelValue
的值。
既然如此,那除了 v-model
外,如何再实现一种自定义的“双向绑定”,能让我同时绑定多个值呢?
Vue2 中的 .sync 修饰符
.sync 修饰符以前存在于 vue1.0 版本里,但是在在 2.0 中移除了 .sync 。但是在 2.0 发布之后的实际应用中,我们发现 .sync 还是有其适用之处,比如在开发可复用的组件库时。我们需要做的只是让子组件改变父组件状态的代码更容易被区分。从 2.3.0 起我们重新引入了 .sync 修饰符,但是这次它只是作为一个编译时的语法糖存在。它会被扩展为一个自动更新父组件属性的 v-on 监听器。
.sync
使我们能够实现自定义的 v-model:
// 不使用 .sync
<Son
v-bind:title="title"
v-on:update:title="title = $event"
/>
// 使用 .sync
<Son :title.sync="title"/>
请注意,子组件内部依然要做两件事:1.接收 prop, 2.调用 emit 触发更新
// 子组件依然要自己实现更新逻辑
<script>
export default {
props: ['title'],
emits: ['update:title']
}
</script>
<template>
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
</template>
不难看出,无论是 v-model
还是 .sync
,其本质就是父子组件通讯,只是父组件的使用时写起来方便了些。当然,子组件内部的实现依然不能少。
既然 v-model
和 .sync
两者是一个东西,那能不能整到一起去呢?帮人办法到底,送佛送到西嘛!
v-model 的指令参数
Vue3 对 v-model
和 .sync
进行了合并,将 sync
化成了 v-model
的指令参数。
这好吗?这很好。
我们就可以写多个 v-model 了,反正都是组件通信,怎么舒服怎么来,名字也可以随便取,就很哇撒:
// 父组件绑定多个 model
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>
// 子组件实现:
</script>
export default {
props: {
firstName: String,
lastName: String
},
emits: ['update:firstName', 'update:lastName']
}
</script>
<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>
TSX 中编写 v-model
有时,我们需要自己使用 TSX 封装组件,但是 jsx 语法并不支持这个指令,在模板编译过程中,我们必须自己提供这些 props。举个封装的小栗子,大家一眼就能看明白了:
- 封装一个 element-plus 中的 radio 组件:
import { defineComponent, PropType } from "vue";
export default defineComponent({
props: {
modalValue: {
type: [Boolean, String, Number],
require: true
},
radioType: {
type: String as PropType<"radio" | "button">,
default: "radio"
},
groups: {
type: Array as PropType<Array<any>>,
require: true
},
},
emits: ["update:modalValue"],
setup(props, ctx) {
return () => (
<el-radio-group
modalValue={props.modalValue}
onUpdate:modalValue={value => ctx.emit("update:modalValue", value)}
>
{props.groups.map(({ slots, ...rProps }) => {
return props.radioType === "button" ? (
<el-radio-button {...rProps} v-slots={slots} />
) : (
<el-radio {...rProps} v-slots={slots} />
);
})}
</el-radio-group>
);
}
});
- 父组件使用:
<script setup lang="ts">
import { ref } from "vue";
import RadioGroup from "./components/RadioGroup.vue"
const groups = [
{ label: "昨天", slots: { default: () => "昨天" }},
{ label: "今天", slots: { default: () => "今天" }},
{ label: "明天", slots: { default: () => "明天" }}
];
const radio = ref();
</script>
<template>
<RadioGroup
v-model="radio"
radio-type="button"
:groups="groups"
/>
</template>
上述例子中,element-plus 组件库提供了 model-value / v-model
两种形式给我们绑定选中项的值,所以我们使用 modelValue
和 onUpdate:modelValue
为 v-model
指令进行了扩展,让其他父组件在调用时,可以直接使用 v-model
指令,而无需再注册事件了。
参考资料
转载自:https://juejin.cn/post/7234834284690358331