前端笔记 - 【Vue3 + Vue2】 - 如何使用 v-model语法糖(Vue3/Vue2) 封装组件?(附有详细案例)

484 阅读4分钟

前言

  • 在开发项目的时候,我们经常会使用到v-model指令,有时候,会使用v-model指令去封装一些组件;
  • 面试的时候,面试官也会问我们v-model的语法糖;
  • 下面就来说说v-model的语法糖,以及怎么封装组件;
    • 结合下面的例子更容易理解和记忆;

需求

  • 封装的组件名称:
    • RadioBtn.vue
  • 我们单独封装一个单选按钮组件;
  • 有多少个按钮我们不确定;
    • 该组件使用的地方不止一处;
    • image.png

需求 —— 分析

  1. 有多少个按钮我们不确定;
    • 所以需要在App.vue中准备一个配置项,用来存放按钮;
    • 这个配置项的类型是:
      type OptionType = {
          // 要渲染在页面上的文本
          label: string;
          // 传给后端的标识符
          value: number | string;
      }[];
      
    • 每个按钮至少应该有labelvalue两个属性;
    • 子组件中定义一个props,来接收父组件传递的配置项options;
  2. 子组件选中之后,需要将选中的值传递给父组件;
    • 需要定义一个事件,将值(也就是当前选中项对应的value)传递给父组件;
  3. 选中之后有高亮效果;

需求 —— 基本模板代码

  • 父组件

    • App.vue
    <!-- 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
    <!-- 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>
    
  • 页面基本展示: image.png

一、v-model 简介

1.1 v-model 原理

  • Vue 中,我们可以使用 v-bind 实现单向数据的动态绑定,也就是通过 父组件 向 子组件 传递数据, 但是反过来,子组件 是 绝对不可以 修改 父组件 传递的数据,这就是我们经常说的 单向数据流
  • v-model 就实现了数据的双向绑定,实际上 v-model 就是通过 Vue 提供的 事件机制 实现了 数据 的 双向绑定
    1. 父组件 使用 v-on 来监听对应的事件并修改相应的数据;
    2. 子组件 通过 $emit(自定义事件, 给父组件传参) 触发事件;

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 - 父组件:

    • 给子组件传递的数据属性名 === modelValue
    • 父组件中给子组件标签绑定的自定义事件 === @update:modelValue
    <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 - 子组件:

    • 子组件在点击按钮的时候使用 emit() 触发自定义事件,并将当前按钮对应的 value 传递给父组件;
      • script 中使用 defineEmits() 的返回值去触发自定义事件;
      • template 中使用 $emit() 触发自定义事件;
    <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 封装组件

  • 子组件不用改变,只需将父组件中的 :modelValue@update:modelValue 改写成 v-model 即可;
  • App.vue - 父组件:
    <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

    <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

    <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
<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 封装组件

  • 父组件

    <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>
    
  • 子组件

    <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>
    
  • 效果展示: v-model语法糖.gif

3.1.2 使用 v-model 封装组件

  • 子组件不用改;
  • 只需对父组件进行稍微修改即可;
  • 父组件

    <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
  • 父组件

    <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>
    
  • 子组件

    <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>