前言
- 在开发项目的时候,我们经常会使用到
v-model
指令,有时候,会使用v-model
指令去封装一些组件; - 面试的时候,面试官也会问我们
v-model
的语法糖; - 下面就来说说
v-model
的语法糖,以及怎么封装组件;- 结合下面的例子更容易理解和记忆;
需求
- 封装的组件名称:
RadioBtn.vue
;
- 我们单独封装一个单选按钮组件;
- Vant中虽然有单选按钮组件,但是和我们需要的组件还是有差别的;
- 有多少个按钮我们不确定;
- 该组件使用的地方不止一处;
需求 ------ 分析
- 有多少个按钮我们不确定;
-
所以需要在
App.vue
中准备一个配置项,用来存放按钮; -
这个配置项的类型是:
tstype OptionType = { // 要渲染在页面上的文本 label: string; // 传给后端的标识符 value: number | string; }[];
-
每个按钮至少应该有
label
和value
两个属性; -
子组件中定义一个props,来接收父组件传递的配置项options;
-
- 子组件选中之后,需要将选中的值传递给父组件;
- 需要定义一个事件,将值(也就是当前选中项对应的value)传递给父组件;
- 选中之后有高亮效果;
需求 ------ 基本模板代码
父组件:
App.vue
;
html
<!-- Vue3版本 -->
<script setup lang="ts">
import RadioBtn from '@/components/RadioBtn.vue';
</script>
<!-- Vue2版本 -->
<script>
import RadioBtn from '@/components/RadioBtn.vue';
export default {
components: { RadioBtn },
data() {
return {}
}
}
</script>
<template>
<div class="container">
<RadioBtn />
<hr />
<div class="value">
<span class="label">子组件传递的值:</span>
<span class="value">1</span>
</div>
</div>
</template>
<!-- 如果不下载 sass 模块,可以将下面的 lang="scss" 去掉 或者 改成 lang="less",改写一下样式 -->
<!-- npm i sass -->
<style scoped lang="scss">
.container {
box-sizing: border-box;
width: 400px;
height: 400px;
margin: 100px auto;
padding: 30px;
border: 1px solid #ccc;
border: thick double #32a1ce;
}
div.value {
display: flex;
justify-content: space-between;
align-items: center;
width: 200px;
height: 40px;
margin: 0 auto;
padding: 0 10px;
border: 1px solid #333;
border-radius: 6px;
.value {
flex: 1;
text-align: left;
}
}
</style>
子组件:
RadioBtn.vue
;
html
<!-- Vue3版本 -->
<script setup lang="ts"></script>
<!-- Vue2版本 -->
<script>
export default {
data() {
return {}
}
}
</script>
<template>
<div class="radio-btn">
<a class="item" href="javascript:;">男</a>
<a class="item" href="javascript:;">女</a>
</div>
</template>
<!-- 如果不下载 sass 模块,可以将下面的 lang="scss" 去掉 或者 改成 lang="less",改写一下样式 -->
<!-- npm i sass -->
<style lang="scss" scoped>
a {
text-decoration: none;
}
.radio-btn {
display: flex;
flex-wrap: wrap;
.item {
height: 32px;
min-width: 60px;
line-height: 30px;
padding: 0 14px;
text-align: center;
border: 1px solid #f6f7f9;
background-color: #f6f7f9;
margin-right: 10px;
box-sizing: border-box;
color: #3c3e42;
margin-bottom: 10px;
border-radius: 4px;
transition: all 0.3s;
&.active {
border-color: #16c2a3;
background-color: #eaf8f6;
}
}
}
</style>
- 页面基本展示:
一、v-model 简介
1.1 v-model 原理
- 在
Vue
中,我们可以使用v-bind
实现单向数据的动态绑定,也就是通过 父组件 向 子组件 传递数据 , 但是反过来,子组件 是 绝对不可以 修改 父组件 传递的数据 ,这就是我们经常说的 单向数据流; - 而
v-model
就实现了数据的双向绑定,实际上v-model
就是通过 Vue 提供的 事件机制 实现了 数据 的 双向绑定 ;- 子组件 通过
$emit(自定义事件, 给父组件传参)
触发事件; - 父组件 使用
v-on
来监听对应的事件并修改相应的数据;
- 子组件 通过
1.2 不同的表单项绑定不同的事件
- 由于HTML中的表单项的属性不一定都是
value
,所以也就不一定能触发input
事件; - 因此Vue为这些不同属性的元素做了单独的适配:我们最常使用的就是
radio、checkbox
这两个,都是通过绑定change
事件触发的;
简单来说:
v-model
是通过 绑定事件 和 监听事件 实现的;
二、Vue3
2.1 v-model 语法糖
默认解析成:
- 属性 :
:modelValue
;- 事件 :
@update:modelValue
;
2.1.1 使用 :modelValue 和 @update:modelValue 封装组件
-
App.vue
- 父组件:html<script setup lang="ts"> import { ref } from 'vue'; import RadioBtn from '@/components/RadioBtn.vue'; const options = [ { label: '男', value: 1 }, { label: '女', value: 0 } ]; // 性别的默认值 const gender = ref<number | string>(1); const updateGender = (val: number | string) => { gender.value = val; }; </script> <template> <div class="container"> <RadioBtn :options="options" :modelValue="gender" @update:modelValue="updateGender" /> <hr /> <div class="value"> <span class="label">子组件传递的值:</span> <span class="value">{{ gender }}</span> </div> </div> </template>
-
RadioBtn.vue
- 子组件:html<script setup lang="ts"> defineProps<{ options: { label: string; value: number | string; }[]; modelValue: number | string; }>(); defineEmits<{ (e: 'update:modelValue', value: number | string): void; }>(); </script> <template> <div class="radio-btn"> <a class="item" :class="{ active: item.value == modelValue }" href="javascript:;" v-for="item in options" :key="item.label" @click="$emit('update:modelValue', item.value)"> {{ item.label }} </a> </div> </template>
2.2.2 使用 v-model 封装组件
-
子组件不用改变,只需对父组件进行稍微的改动即可;
-
App.vue
- 父组件:html<script setup lang="ts"> import { ref } from 'vue'; import RadioBtn from '@/components/RadioBtn.vue'; const options = [ { label: '男', value: 1 }, { label: '女', value: 0 } ]; const gender = ref<number | string>(1); </script> <template> <div class="container"> <!-- 🚀 --> <RadioBtn :options="options" v-model="gender" /> <!-- 🚀 --> <hr /> <div class="value"> <span class="label">子组件传递的值:</span> <span class="value">{{ gender }}</span> </div> </div> </template>
2.2 v-model 的另一种形式 v-model:attr
说明:
attr
➡ 自定义的 属性 / 变量 名
默认解析成:属性 :
:attr
;事件 :
update:attr
;
2.2.1 使用 :attr 和 :update:attr 封装组件
-
父组件 -
App.vue
:html<script setup lang="ts"> import { ref } from 'vue'; import RadioBtn from '@/components/RadioBtn.vue'; const options = [ { label: '男', value: 1 }, { label: '女', value: 0 } ]; const gender = ref<number | string>(1); const updateGender = (val: number | string) => { gender.value = val; }; </script> <template> <div class="container"> <RadioBtn :options="options" :gender="gender" @update:gender="updateGender" /> <hr /> <div class="value"> <span class="label">子组件传递的值:</span> <span class="value">{{ gender }}</span> </div> </div> </template>
-
子组件 -
RadioBtn.vue
:html<script setup lang="ts"> defineProps<{ options: { label: string; value: number | string; }[]; gender: number | string; }>(); defineEmits<{ (e: 'update:gender', value: number | string): void; }>(); </script> <template> <div class="radio-btn"> <a class="item" :class="{ active: item.value === gender }" href="javascript:;" v-for="item in options" :key="item.label" @click="$emit('update:gender', item.value)"> {{ item.label }} </a> </div> </template>
2.2.2 使用 v-model:attr 封装组件
- 子组件不动,对父组件进行稍微的改动;
- 父组件 -
App.vue
:
html
<script setup lang="ts">
import { ref } from 'vue';
import RadioBtn from '@/components/RadioBtn.vue';
const options = [
{ label: '男', value: 1 },
{ label: '女', value: 0 }
];
const gender = ref<number | string>(1);
</script>
<template>
<div class="container">
<RadioBtn :options="options" v-model:gender="gender" />
<hr />
<div class="value">
<span class="label">子组件传递的值:</span>
<span class="value">{{ gender }}</span>
</div>
</div>
</template>
三、Vue2
3.1 v-model 语法糖
- 默认解析成:
- 输入框 :
- 属性 :
:value
;
- 事件 :
@input
;
- 属性 :
- 单选按钮
- 属性 :
:checked
;
- 事件 :
@change
;
- 属性 :
- 输入框 :
3.1.1 使用 :value 与 @input 封装组件
父组件:
html
<script>
import RadioBtn from '@/components/RadioBtn.vue'
export default {
components: { RadioBtn },
data () {
return {
options: [
{ label: '男', value: 1 },
{ label: '女', value: 0 }
],
gender: ''
}
},
methods: {
updateGender (val) {
this.gender = val
}
}
}
</script>
<template>
<div class="container">
<RadioBtn :options="options" :value="gender" @input="updateGender" />
<hr />
<div class="value">
<span class="label">子组件传递的值:</span>
<span class="value">{{ gender }}</span>
</div>
</div>
</template>
<style scoped lang="less">
.container {
box-sizing: border-box;
width: 400px;
height: 400px;
margin: 100px auto;
padding: 30px;
border: 1px solid #ccc;
border: thick double #32a1ce;
}
div.value {
display: flex;
justify-content: space-between;
align-items: center;
width: 200px;
height: 40px;
margin: 0 auto;
padding: 0 10px;
border: 1px solid #333;
border-radius: 6px;
.value {
flex: 1;
text-align: left;
}
}
</style>
子组件:
html
<script>
export default {
props: {
options: {
type: Array,
required: true
},
value: {
required: true
}
},
data () {
return {}
}
}
</script>
<template>
<div class="radio-btn">
<a
class="item"
:class="{ active: value === item.value }"
href="javascript:;"
v-for="item in options"
:key="item.value"
@click="$emit('input', item.value)"
>
{{ item.label }}
</a>
</div>
</template>
<!-- 如果不下载 sass 模块,可以将下面的 lang="scss" 去掉 或者 改成 lang="less",改写一下样式 -->
<!-- npm i sass -->
<style lang="less" scoped>
a {
text-decoration: none;
}
.radio-btn {
display: flex;
flex-wrap: wrap;
.item {
height: 32px;
min-width: 60px;
line-height: 30px;
padding: 0 14px;
text-align: center;
border: 1px solid #f6f7f9;
background-color: #f6f7f9;
margin-right: 10px;
box-sizing: border-box;
color: #3c3e42;
margin-bottom: 10px;
border-radius: 4px;
transition: all 0.3s;
&.active {
border-color: #16c2a3;
background-color: #eaf8f6;
}
}
}
</style>
- 效果展示:
3.1.2 使用 v-model 封装组件
- 子组件不用改;
- 只需对父组件进行稍微修改即可;
父组件:
html
<template>
<div class="container">
<!-- <RadioBtn :options="options" :value="gender" @input="updateGender" /> -->
<!-- 🚀 --> <RadioBtn :options="options" v-model="gender" /> <!-- 🚀 -->
<hr />
<div class="value">
<span class="label">子组件传递的值:</span>
<span class="value">{{ gender }}</span>
</div>
</div>
</template>
3.2 attr.sync 语法糖
说明:
attr
➡ 自定义的 属性 / 变量 名
- 默认解析成:
- 属性 :
:attr
;
- 事件 :
@update:attr
;
- 属性 :
父组件:
html
<template>
<div class="container">
<!-- <RadioBtn :options="options" :value="gender" @input="updateGender" /> -->
<!-- <RadioBtn :options="options" v-model="gender" /> -->
<!-- 🚀 --> <RadioBtn :options="options" :gender.sync="gender" /> <!-- 🚀 -->
<hr />
<div class="value">
<span class="label">子组件传递的值:</span>
<span class="value">{{ gender }}</span>
</div>
</div>
</template>
子组件:
html
<script>
export default {
props: {
options: {
type: Array,
required: true
},
// 🚀
gender: {
required: true
}
// 🚀
},
data () {
return {}
}
}
</script>
<template>
<div class="radio-btn">
<!-- 🚀 -->
<a
class="item"
:class="{ active: gender === item.value }"
href="javascript:;"
v-for="item in options"
:key="item.value"
@click="$emit('update:gender', item.value)">
{{ item.label }}
</a>
<!-- 🚀 -->
</div>
</template>