Element-ui 之 Radio-Group 组件的源码分析

一. Radio-Group 组件提供的属性和事件

1.1 属性

以下是 Radio-Group 组件提供的属性:

参数 说明 类型 可选值 默认值
value / v-model 绑定值 string / number / boolean -- --
size 单选框组尺寸,仅对按钮形式的 Radio 或带有边框的 Radio 有效 string medium / small / mini --
disabled 是否禁用 boolean -- false
text-color 按钮形式的 Radio 激活时的文本颜色 string -- #ffffff
fill 按钮形式的 Radio 激活时的填充色和边框色 string -- #409EFF

1.2 事件

事件名称 说明 回调参数
input 绑定值变化时触发的事件 选中的 Radio label 值

二. 按照步骤去实现 Radio-Group 组件的属性和事件

  1. 注册组件。
  2. 组件的基本 template 部分。
  3. 引入组件的部分。
  4. Radio 组件里面增加是否是 Radio-group 中的 Radio 的判断。
  5. value/v-model 的处理。
  6. disabled 属性的处理。
  7. size 属性的处理。
  8. filltext-color 属性的处理。
  9. 键盘事件上、下、左、右的处理。
  10. 屏幕阅读器相关 tabIndex 的处理。

三. Radio-Group 组件属性和事件的具体实现

3.1 注册组件

新建一个 radio-Group.vue 文件,设置组件的 namecomponentName 都为 ElRadioGroup。 在 main.js 文件里面引入这个组件,并且使用 Vue.component 来全局注册这个组件。

3.2 组件的基本 template 部分

采用 is 属性来动态渲染组件的标签,并且用插槽接收父组件传过来的标签内内容。

html 复制代码
<component
    :is="_elTag"
    class="el-radio-group"
    role="radiogroup"
  >
    <slot></slot>
</component>

计算属性 _elTag 首先判断 $vnode.data 是否存在 tag 标签,如果不存在或者 tagcomponent 则默认为 div 标签,否则取 $vnode.data 中的 tag 属性。

js 复制代码
computed: {
  _elTag() {
    let tag = (this.$vnode.data || {}).tag;
    if (!tag || tag === 'component') tag = 'div';
    return tag;
  },
},

3.3 在 Radio 组件里面增加是否是 Radio-group 中的 Radio 的判断

radio.vue 文件中增加计算属性 isGroup,判断 radio 是否被包含在 radio-group 组件中,如果被包含在 radio-group 组件中,isGroup 返回 true,否则返回 false

js 复制代码
computed: {
  isGroup() {
    // parent 变量保存组件的父级元素信息
    let parent = this.$parent;
    while (parent) {
      if (parent.$options.componentName !== 'ElRadioGroup') {
        // 如果父级的 componentName 不是 ElRadioGroup 则继续向上查找
        parent = parent.$parent;
      } else {
        // 如果父级的 componentName 是 ElRadioGroup
        // 用变量 _radioGroup 保存父级信息,并且返回 true
        this._radioGroup = parent;
        return true;
      }
    }
    // 查找不到则返回 false
    return false;
  },
}

3.4 value/v-model 的处理

实现步骤:

  1. radio-group 组件使用 v-model 绑定值,组件内部默认使用 value 接收值。
js 复制代码
props: {
  value: {}
},
  1. radio.vue 文件中的计算属性 modelgetter 方法中,先判断 isGroup 是否为 true,如果为 true,获取变量 _radioGroupvalue,否则获取 radio 组件的 valueradio-group 组件的 value 优先级高于 radio 组件的 value
js 复制代码
model: {
  get() {
    return this.isGroup ? this._radioGroup.value : this.value;
  },
  ...
},
  1. radio.vue 文件中引入 Emitter,用来向上查找 radio-group 组件,并在 modelgetter 方法中判断 isGroup 是否为 true,如果是 true,则查找到 ElRadioGroupinput 事件,并调用。
js 复制代码
model: {
  ...
  set(val) {
    if (this.isGroup) {
      this.dispatch('ElRadioGroup', 'input', [val]);
    } else {
      this.$emit('input', val);
    }
    ...
  }
},
  1. 改变选中时让 radio-group 组件监听到 change 事件,在 radio 组件的 handleChange 方法中增加如果是单选按钮组,则向上查找 radio-group 组件的自定义事件 handleChange,并触发该事件。
js 复制代码
methods: {
  handleChange() {
    this.$nextTick(() => {
      ...
      this.isGroup && this.dispatch('ElRadioGroup', 'handleChange', this.model);
    });
  }
}
  1. radio-group 组件中增加监听自定义事件 handleChange,并且在 handleChange 事件中触发父组件的 change 事件。
js 复制代码
created() {
  this.$on('handleChange', value => {
    this.$emit('change', value);
  });
},

3.5 disabled 属性的处理

实现步骤:

  1. props 监听父组件传入的 disabled 属性。
js 复制代码
props: {
  ...
  disabled: Boolean
}
  1. 修改 radio.vue 文件中的 isDisabled 计算属性,判断 isGroup 是否为 true,如果为 true,优先取 radio-groupdisabled 属性,然后再取 radiodisabled 属性。
js 复制代码
computed: {
  ...
  isDisabled() {
    return this.isGroup
      ? this._radioGroup.disabled || this.disabled
      : this.disabled;
  },
}

3.6 size 属性的处理

实现步骤:

  1. props 监听父组件传入的 size 属性。
js 复制代码
props: {
  ...
  size: String,
}
  1. 设置计算属性 radioGroupSize 监听 size 的改变。
js 复制代码
computed: {
  ...
  radioGroupSize() {
    return this.size;
  }
}
  1. 修改 radio.vue 文件中的 radioSize 计算属性,isGroup 是否为 true,如果为 true,优先取 radio-groupradioGroupSize 属性,然后再取 radiosize 属性。
js 复制代码
computed: {
  radioSize() {
    return this.isGroup
      ? this._radioGroup.radioGroupSize || this.size
      : this.size;
  },
}

3.7 fill 和 text-color 属性的处理

props 监听父组件传入的 filltext-color,由于 filltext-colorradio-button 组件中用到的,所以这里先接收一下传入的属性,讲到 radio-button 组件时再去说明这部分。

js 复制代码
props: {
  fill: String,
  textColor: String,
}

3.8 键盘事件上、下、左、右的处理

Chrome 浏览器会自带这个功能,写这部分功能时可以用 safari 浏览器调试。

实现步骤:

  1. 设置监听的键盘事件对象。

Object.freeze() 方法用于冻结一个对象,冻结的对象不能被新增、删除、修改属性,并且不能修改已有属性的可枚举性、可配置性、可写性。并且该对象的原型也不能被修改。

Object.freeze() 是浅冻结,对于对象里面的对象是无法冻结的。

html 复制代码
const keyCode = Object.freeze({
  LEFT: 37,
  UP: 38,
  RIGHT: 39,
  DOWN: 40
});
  1. radio-group 组件上监听 keydown 事件。
html 复制代码
<component
  ...
  @keydown="handleKeydown"
  >
  <slot></slot>
</component>
  1. handleKeydown() 方法处理键盘的上下左右。
js 复制代码
methods: {
  handleKeydown(e) {
    // 用 target 变量保存事件触发的元素
    const target = e.target;
    // 判断当前触发的是 input 元素还是 label 元素,用元素的属性来获取元素
    const className = target.nodeName === 'INPUT' ? '[type=radio]' : '[role=radio]';
    // 获取到当前组件模板根标签下的所有同类型元素,this.$el 指向当前组件模板的根标签
    const radios = this.$el.querySelectorAll(className);
    // 获取元素的整体长度
    const length = radios.length;
    // 获取触发的元素的索引值
    const index = [].indexOf.call(radios, target);
    // 获取到当前组件模板根标签下的全部 role=radio 的元素
    const roleRadios = this.$el.querySelectorAll('[role=radio]');
    switch (e.keyCode) {
      case keyCode.LEFT:
      case keyCode.UP:
        e.stopPropagation();
        e.preventDefault();
        // 按下左或上,则让前一个元素点击并聚焦,如果当前是第一个元素,则让最后一个元素点击并聚焦
        if (index === 0) {
          roleRadios[length - 1].click();
          roleRadios[length - 1].focus();
        } else {
          roleRadios[index - 1].click();
          roleRadios[index - 1].focus();
        }
        break;
      case keyCode.RIGHT:
      case keyCode.DOWN:
        // 按下右或下,则让下一个元素点击并聚焦,如果当前是最后一个元素,则让第一个元素点击并聚焦
        if (index === (length - 1)) {
          // 阻止冒泡
          e.stopPropagation();
          // 阻止默认事件
          e.preventDefault();
          roleRadios[0].click();
          roleRadios[0].focus();
        } else {
          roleRadios[index + 1].click();
          roleRadios[index + 1].focus();
        }
        break;
      default:
        break;
    }
  }
},

3.9 屏幕阅读器相关 tabIndex 的处理

实现步骤:

  1. radio.vue 文件中的计算属性 tabIndex,增加判断如果是按钮组,且该按钮的 value 值与按钮组传入的 value 值不一致,则不能聚焦。表示按钮组在有选中值的情况下,不能聚焦其余选项。
js 复制代码
tabIndex() {
  return (this.isDisabled || (this.isGroup && this.model !== this.label)) ? -1 : 0;
}
  1. 如果 radio-group 按钮组没有默认的选项时,将第一个 radiotabIndex 设置为0,代表元素是可聚焦的。表示按钮组在没有默认的选项时,可以将第一个选项设置为可聚焦。
js 复制代码
mounted() {
  const radios = this.$el.querySelectorAll('[type=radio]');
  const firstLabel = this.$el.querySelectorAll('[role=radio]')[0];
  if (![].some.call(radios, radio => radio.checked) && firstLabel) {
    firstLabel.tabIndex = 0;
  }
},

四. 整体代码详解

包含 Radio-Group 与 Form 组件相互作用的部分。

html 复制代码
<template>
  <component
    :is="_elTag"
    class="el-radio-group"
    role="radiogroup"
    @keydown="handleKeydown"
  >
    <slot></slot>
  </component>
</template>
<script>
import Emitter from 'element-ui/src/mixins/emitter';

const keyCode = Object.freeze({
  LEFT: 37,
  UP: 38,
  RIGHT: 39,
  DOWN: 40
});

export default {
  name: 'ElRadioGroup',
  
  mixins: [Emitter],
  
  inject: {
    elFormItem: {
      default: ''
    }
  },
  
  componentName: 'ElRadioGroup',
  
  props: {
    value: {},
    size: String,
    disabled: Boolean
  },
  
  computed: {
    // 计算包裹 radio-group 组件的标签
    _elTag() {
      let tag = (this.$vnode.data || {}).tag;
      if (!tag || tag === 'component') tag = 'div';
      return tag;
    },
    // 获取 Form-Item 组件的 elFormItemSize 变量
    _elFormItemSize() {
      return (this.elFormItem || {}).elFormItemSize;
    },
    // Radio-Group 组件的尺寸计算
    radioGroupSize() {
      // 优先级:group-size > Form-Item > Form
      return this.size || this._elFormItemSize;
    }
  },
  
  created() {
    this.$on('handleChange', value => {
      this.$emit('change', value);
    });
  },
  
  mounted() {
    // 如果 radio-group 按钮组没有默认的选项时,将第一个 radio 的 tabIndex 设置为0。代表元素是可聚焦的
    const radios = this.$el.querySelectorAll('[type=radio]');
    const firstLabel = this.$el.querySelectorAll('[role=radio]')[0];
    if (![].some.call(radios, radio => radio.checked) && firstLabel) {
      firstLabel.tabIndex = 0;
    }
  },
  
  methods: {
    handleKeydown(e) {
      // 用 target 变量保存事件触发的元素
      const target = e.target;
      // 判断当前触发的是 input 元素还是 label 元素,用元素的属性来获取元素
      const className = target.nodeName === 'INPUT' ? '[type=radio]' : '[role=radio]';
      // 获取到当前组件模板根标签下的所有同类型元素,this.$el 指向当前组件模板的根标签
      const radios = this.$el.querySelectorAll(className);
      // 获取元素的整体长度
      const length = radios.length;
      // 获取触发的元素的索引值
      const index = [].indexOf.call(radios, target);
      // 获取到当前组件模板根标签下的全部 role=radio 的元素
      const roleRadios = this.$el.querySelectorAll('[role=radio]');
      switch (e.keyCode) {
        case keyCode.LEFT:
        case keyCode.UP:
          e.stopPropagation();
          e.preventDefault();
          // 按下左或上,则让前一个元素点击并聚焦,如果当前是第一个元素,则让最后一个元素点击并聚焦
          if (index === 0) {
            roleRadios[length - 1].click();
            roleRadios[length - 1].focus();
          } else {
            roleRadios[index - 1].click();
            roleRadios[index - 1].focus();
          }
          break;
        case keyCode.RIGHT:
        case keyCode.DOWN:
          // 按下右或下,则让下一个元素点击并聚焦,如果当前是最后一个元素,则让第一个元素点击并聚焦
          if (index === (length - 1)) {
            // 阻止冒泡
            e.stopPropagation();
            // 阻止默认事件
            e.preventDefault();
            roleRadios[0].click();
            roleRadios[0].focus();
          } else {
            roleRadios[index + 1].click();
            roleRadios[index + 1].focus();
          }
          break;
        default:
          break;
      }
    }
  },
  watch: {
    value(value) {
      // 监听 value 值的变化,当 value 值改变时,向 Form-Item 组件派发 el.form.change 自定义事件
      this.dispatch('ElFormItem', 'el.form.change', [value]);
    }
  }
}
</script>
相关推荐
q***38513 小时前
SpringBoot + vue 管理系统
vue.js·spring boot·后端
喵个咪3 小时前
go-kratos-admin 快速上手指南:从环境搭建到启动服务(Windows/macOS/Linux 通用)
vue.js·go
用户841794814563 小时前
vxe-gantt table 甘特图如何设置任务视图每一行的背景色
vue.js
小章鱼学前端4 小时前
2025 年最新 Fabric.js 实战:一个完整可上线的图片选区标注组件(含全部源码).
前端·vue.js
涔溪4 小时前
实现将 Vue3 项目作为子应用,通过无界(Wujie)微前端框架接入到 Vue2 主应用中(Vue2 为主应用,Vue3 为子应用)
vue.js·前端框架·wujie
源码技术栈7 小时前
什么是云门诊系统、云诊所系统?
java·vue.js·spring boot·源码·门诊·云门诊
lcc1877 小时前
Vue3 ref函数和reactive函数
前端·vue.js
艾小码7 小时前
还在为组件通信头疼?defineExpose让你彻底告别传值烦恼
前端·javascript·vue.js
带只拖鞋去流浪7 小时前
迎接2026,重新认识Vue CLI (v5.x)
前端·vue.js·webpack
Coder-coco7 小时前
游戏助手|游戏攻略|基于SprinBoot+vue的游戏攻略系统小程序(源码+数据库+文档)
java·vue.js·spring boot·游戏·小程序·论文·游戏助手