前言
- 在开发项目的时候,我们经常会使用到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>