前端笔记 - 【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>
相关推荐
jerrywus1 天前
为什么每个程序员都应该试试 cmux:AI 加持的终端效率革命
前端·人工智能·claude
codeniu1 天前
@logicflow/vue-node-registry 在 Vite 中无法解析的踩坑记录与解决方案
前端·javascript
孟祥_成都1 天前
AI 术语满天飞?90% 的人只懂名词,不懂为什么!
前端·人工智能
Lupino1 天前
被 React “玩弄”的 24 小时:为了修一个不存在的 Bug,我给大模型送了顿火锅钱
前端·react.js
米丘1 天前
了解 Javascript 模块化,更好地掌握 Vite 、Webpack、Rollup 等打包工具
前端
Heo1 天前
深入 React19 Diff 算法
前端·javascript·面试
滕青山1 天前
个人所得税计算器 在线工具核心JS实现
前端·javascript·vue.js
小怪点点1 天前
手写promise
前端·promise
国思RDIF框架1 天前
RDIFramework.NET Web 敏捷开发框架 V6.3 发布 (.NET8+、Framework 双引擎)
前端
Mintopia1 天前
如何在有限的时间里,活出几倍的人生
前端