前端笔记 - 【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;
      }[];
    • 每个按钮至少应该有label和value两个属性;

    • 子组件中定义一个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>
相关推荐
Autumn_ing3 小时前
Design.md生成UI怎么验收?举例运营后台的颜色、状态和数据核对
前端·ui·ai·设计系统·paico·ai生成ui·design.md
RobinDevNotes4 小时前
一套代码发布到十几个平台:Cocos入门速览
typescript·c#·游戏开发
keyipatience4 小时前
JavaScript学习(1)书写位置,变量、数据类型、运算符
开发语言·前端·javascript·css·学习
朱涛的自习室4 小时前
我给 Gemini 4 和 DeepSeek 搞了套「末位淘汰」,AI 干活会更猛吗?(Day 3)
android·前端·后端
java1234_小锋6 小时前
【技术专题】HTML5 网页技术 - HTML5简介 & HTML5 文档结构
前端·html·html5
鶴哥只手遮天7 小时前
Vue 3 核心源码深度剖析系列(v3.5.44)-第 1 章 项目全景:monorepo 架构与工程化体系
前端·vue.js·开源
流星。。方7 小时前
小团队官网SEO落地:前端语义化与静态生成的工程实践
前端·状态模式
用户530429765797 小时前
盘点危化品搬运诊断的6类工具与自处理边界判断
前端
curtain7 小时前
从古法编程到 Agent:我入行这两年,AI 改变了什么
前端·openai
zyfts7 小时前
10分钟自制Codex钉钉通知Skill:AI干完活自动钉钉提醒你
前端