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

前言

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

需求

  • 封装的组件名称:
    • RadioBtn.vue
  • 我们单独封装一个单选按钮组件;
    • Vant中虽然有单选按钮组件,但是和我们需要的组件还是有差别的;
  • 有多少个按钮我们不确定;
    • 该组件使用的地方不止一处;

需求 ------ 分析

  1. 有多少个按钮我们不确定;
    • 所以需要在App.vue中准备一个配置项,用来存放按钮;

    • 这个配置项的类型是:

      ts 复制代码
      type OptionType = {
          // 要渲染在页面上的文本
          label: string;
          // 传给后端的标识符
          value: number | string;
      }[];
    • 每个按钮至少应该有labelvalue两个属性;

    • 子组件中定义一个props,来接收父组件传递的配置项options;

  2. 子组件选中之后,需要将选中的值传递给父组件;
    • 需要定义一个事件,将值(也就是当前选中项对应的value)传递给父组件;
  3. 选中之后有高亮效果;

需求 ------ 基本模板代码

父组件

  • 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>
相关推荐
喵叔哟5 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
man20171 小时前
【2024最新】基于springboot+vue的闲一品交易平台lw+ppt
vue.js·spring boot·后端
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js