日常开发记录-radio组件

1.radio组件

闲来无事,写写vue2的单选radio组件。

<template>
  <label>
    <input
      ref="radio"
      type="radio"
      class="q-radio__original"
      :value="label"
      :name="name"
      :disabled="disabled"
      v-model="currentValue"
      @change="handleChange"
    />
    <span class="q-radio__label">
      <slot>{{ label }}</slot>
    </span>
  </label>
</template>

<script>

  export default {
    name: 'MyRadio',
    props: {
      value: {},
      name: String,
      label: {},
      disabled: Boolean,
    },
    computed:{
      currentValue:{
        get() {
          return this.value
        },
        set(value) {
          this.$emit('input',value)
        }
      }
    },
    methods: {
      handleChange(event) {
        this.$emit('change', event.target.value)
      },
    },
  }
</script>

//父组件
     <input type="radio" id="contactChoice1" name="contact" value="email" />
      <label for="contactChoice1">电子邮件</label>
      <input type="radio" id="contactChoice2" name="contact" value="phone" />
      <label for="contactChoice2">电话</label>
      <input type="radio" id="contactChoice3" name="contact2" value="mail" />
      <label for="contactChoice3">邮件</label>
      
      <MyRadio v-model="currentValue" name="aaa" @change="handleChange" label="男"></MyRadio>
      <MyRadio v-model="currentValue" name="aaa" @change="handleChange" label="女"></MyRadio>
      <MyRadio v-model="currentValue" name="bbb" @change="handleChange" label="未知">未知</MyRadio>

实际效果:
在这里插入图片描述

问题就来了,我给了不同的name,但是却和原生的input标签效果不同。按理说男女和未知,应该互不影响的呢。

2.解除疑惑

  1. v-model 的本质
    v-model 本质上是一个语法糖,它由两个操作组成:
    将数据绑定到元素的 value 属性 (:value=“value”)
    监听元素的 input/change 事件 (@input=“value = $event”)
  2. 之前的代码分析
    在之前的代码中:
// App.vue
data() {
  return {
    currentValue: ''  // 单个字符串值
  }
}

// 模板中
<MyRadio v-model="currentValue" name="aaa" label="男"></MyRadio>
<MyRadio v-model="currentValue" name="aaa" label="女"></MyRadio>
<MyRadio v-model="currentValue" name="bbb" label="未知">未知</MyRadio>

问题在于:
所有的 MyRadio 组件都绑定到同一个 currentValue 变量
当任何一个 radio 被选中时,都会触发 v-model 的更新
由于共享同一个数据源,当一个 radio 更新了 currentValue,其他所有绑定到这个值的 radio 也会随之更新
3. 数据流动过程
让我们看看当点击一个 radio 时发生了什么:
用户点击 “男” 选项:

// MyRadio 组件内部
computed: {
  currentValue: {
    get() {
      return this.value  // 从父组件获取值
    },
    set(value) {
      this.$emit('input', value)  // 向父组件发送新值
    }
  }
}

触发事件链:

点击 radio → change 事件 → currentValue setter → $emit(‘input’) → 父组件 currentValue 更新 → 所有使用这个 currentValue 的组件都更新

由于所有 MyRadio 组件都监听着同一个 currentValue,当它变成 “男” 时:
name=“aaa” 的两个 radio (“男"和"女”) 会更新
name=“bbb” 的 radio (“未知”) 也会更新
它们都共享同一个值,所以只能有一个被选中
4. 为什么使用对象解决了问题
新的实现中:

data() {
  return {
    currentValue: {
      aaa: '',  // 专门存储 name="aaa" 的值
      bbb: ''   // 专门存储 name="bbb" 的值
    }
  }
}

现在:
每组 radio 都有自己独立的数据存储空间
当 name=“aaa” 的 radio 改变时,只会更新 currentValue.aaa
当 name=“bbb” 的 radio 改变时,只会更新 currentValue.bbb
不同 name 的 radio 组之间完全独立,互不影响
5. 原生 radio 的对比
原生 HTML radio 的行为:

<input type="radio" name="contact" value="email">  <!--1 -->
<input type="radio" name="contact" value="phone">  <!--1 -->
<input type="radio" name="contact2" value="mail">  <!--2 -->

原生 radio 通过 name 属性天然分组,同一个 name 的 radio 互斥,不同 name 的 radio 独立。但在 Vue 组件中,我们需要通过数据结构(对象)来模拟这种分组行为。
这就是为什么之前使用单个值会导致所有 radio 互相影响,而使用对象可以实现独立分组的原因。每个分组都有自己的数据空间,就像原生 radio 的 name 属性一样。

3.input标签又绑定了v-model,不是重复了吗?还是父组件的v-model绑定到那个元素上的呢

这是一个很好的问题!让我详细解析一下组件中的 v-model 工作原理。

  1. 组件的双重 v-model
    让我们看看当前的代码:
<!-- App.vue 父组件 -->
<MyRadio v-model="currentValue.aaa" name="aaa" label="男"></MyRadio>

<!-- MyRadio.vue 子组件 -->
<template>
  <label>
    <input
      type="radio"
      :value="label"
      :name="name"
      v-model="currentValue"  <!-- 这里确实看起来像重复了 -->
      @change="handleChange"
    />
    <span class="q-radio__label">
      <slot>{{ label }}</slot>
    </span>
  </label>
</template>

<script>
export default {
  computed: {
    currentValue: {
      get() {
        return this.value
      },
      set(value) {
        this.$emit('input', value)
      }
    }
  }
}
</script>
  1. v-model 的传递过程
    实际上这里涉及到两个层级的数据绑定:
    父组件的 v-model
   <MyRadio v-model="currentValue.aaa">

这等同于:

   <MyRadio 
     :value="currentValue.aaa"
     @input="val => currentValue.aaa = val"
   >

子组件的实现
子组件接收 value prop
通过计算属性 currentValue 代理这个值
当 input 元素变化时,触发 currentValue 的 setter
setter 向上触发 input 事件
3. 数据流动过程
让我们看看当用户点击 radio 时的完整流程:

1. 用户点击 radio input
   ↓
2. 同时触发两个事件流:

   流程A (v-model):                    流程B (@change):
   2A. 原生 input 的 v-model 触发      2B. 原生 input 的 change 事件触发
   ↓                                   ↓
   3A. 更新子组件的 currentValue       3B. 执行 handleChange 方法
   ↓                                   ↓
   4A. currentValue 的 setter 被调用   4B. 触发 $emit('input', event.target.value)
   ↓                                   
   5A. 触发 $emit('input', value)6. 父组件接收到 input 事件(可能收到两次!)
   ↓
7. 更新父组件的 currentValue.aaa

确实还会触发两次
4. 优化
实际上,当前的实现确实有些冗余。我们可以简化这个过程,移除子组件中的 v-model:

// ... existing code ...
    <input
      ref="radio"
      type="radio"
      class="q-radio__original"
      :value="label"
      :name="name"
      :checked="value === label"
      :disabled="disabled"
      @change="handleChange"
    />
// ... existing code ...
    props: {
      value: {
        required: true
      },
      name: {
        type: String,
        required: true
      },
      label: {
        required: true
      },
      disabled: Boolean,
    },
    methods: {
      handleChange(event) {
        this.$emit('input', this.label)
      },
    }
// ... existing code ...

主要改动:
移除了 currentValue 计算属性
使用 :checked=“value === label” 来控制选中状态
直接在 change 事件中发出 input 事件
这样的实现有以下优点:
更清晰的数据流:
父组件的值直接决定 radio 的选中状态
用户操作直接触发父组件的更新
没有中间的计算属性转换
更符合单向数据流:
Props 向下传递(value)
事件向上传递(input)
性能更好:
减少了计算属性的开销
减少了不必要的数据绑定
更容易理解:
直接使用原生 radio 的 checked 属性
清晰的事件处理流程
这样修改后,组件的行为保持不变,但实现更加简洁和高效。父组件的 v-model 仍然可以正常工作,因为我们保持了 Vue 组件 v-model 所需的 value prop 和 input 事件。

4.拓展

在现有代码上加一个

<template>
  <label>
    <input
      ref="radio"
      type="radio"
      class="q-radio__original"
      :value="label"
      :name="name"
      :checked="value === label"
      :disabled="disabled"
      @change="handleChange"
    />
    <span class="q-radio__label">
      <slot></slot>    <!===新加的>
      <template v-if="!$slots.default">{{label}}</template>
    </span>
  </label>
</template>

<script>

  export default {
    name: 'MyRadio',
    props: {
      value: {
       
      },
      name: String,
      label: {
     
      },
      disabled: Boolean,
    },
    methods: {
      handleChange(event) {
          this.$emit('input', this.label)
          this.$emit('change', this.label)
      },
    },
  }
</script>

这是一个很巧妙的插槽(slot)实现,让我们逐部分分析:

  1. 结构解析
    插槽内容
   <slot></slot>

默认插槽,允许用户自定义标签内容
后备内容

   <template v-if="!$slots.default">{{label}}</template>

当没有提供插槽内容时显示的后备内容
使用 prop 中的 label 值
2. 使用场景
场景一:使用插槽自定义内容

<q-radio v-model="radio" label="1">
  <span style="color: red">自定义文本</span>
</q-radio>

渲染结果:显示红色的"自定义文本"
场景二:直接使用 label

<el-radio v-model="radio" label="选项1"></el-radio>

渲染结果:显示"选项1"
3. 工作原理
插槽检测
s l o t s . d e f a u l t : V u e 提供的插槽检测机制如果有内容传入默认插槽, slots.default: Vue 提供的插槽检测机制 如果有内容传入默认插槽, slots.default:Vue提供的插槽检测机制如果有内容传入默认插槽,slots.default 就不为空
条件渲染
先尝试渲染插槽内容
如果插槽为空(!$slots.default 为 true),则渲染 label 值

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值