拜读及分析Element源码-radio组件篇

element-ui 单选框radio组件源码分析,也是很经常使用的一个javascript

单选框组件分为3部分html

  1. radio-group: 单选组,适用于多个互斥的选项中选择的场景
  2. radio: 单选
  3. radio-button: 按钮样式的单选

2能够单独使用,也可与1组合使用,3和1要组合使用vue

radio-group

结构

很简单至关因而一个父容器,而且提供了键盘上下左右选中的方法java

<div class="el-radio-group" role="radiogroup" @keydown="handleKeydown" >
    <slot></slot>
  </div>
复制代码

slot接收的内容就是radio或radio-button了node

script部分

1. 导入mixins

import Emitter from 'element-ui/src/mixins/emitter';
复制代码

这是其实就是用到emitter.js里的dispatch 方法(向上找到指定组件并发布指定事件及传递值)ios

// 接收组件名,事件名,参数
    dispatch(componentName, eventName, params) {
      var parent = this.$parent || this.$root;
      var name = parent.$options.componentName;

      // 寻找父级,若是父级不是符合的组件名,则循环向上查找
      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent;
        if (parent) {
          name = parent.$options.componentName;
        }
      }
      // 找到符合组件名称的父级后,发布其事件。
      if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params));

      }
复制代码

在watch中监听value时用到element-ui

watch: {
      // 监听选中值,向上找到from-item组件发布el.form.change(应该是用于表单验证)
      value(value) {
        this.dispatch('ElFormItem', 'el.form.change', [this.value]);
      }
    }
复制代码

2.声明 冻结上下左右的keyCode组成的对象

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

Object.freeze() 方法能够冻结一个对象,冻结指的是不能向这个对象添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性。该方法返回被冻结的对象。数组

3.若form-item组件注入属性影响size(默认为空)

inject: {
      elFormItem: {
        default: ''
      }
    }
复制代码

size在computed里并发

// 最终大小
    computed: {
      _elFormItemSize() {
        return (this.elFormItem || {}).elFormItemSize;
      },
      radioGroupSize() {
        return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
      }
    }
复制代码

4.生命周期及watch

created() {
      // 触发radio组件发布的handleChange事件拿到选中值,发布change事件暴露选中值
      this.$on('handleChange', value => {
        this.$emit('change', value);
      });
    },
    mounted() {
      // 当radioGroup没有默认选项时,第一个能够选中Tab导航
      // 不知为什么要这样作
      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;
      }
    }
复制代码

5.keyDown事件

handleKeydown(e) { // 左右上下按键 能够在radio组内切换不一样选项
        const target = e.target;
        // radio || label
        const className = target.nodeName === 'INPUT' ? '[type=radio]' : '[role=radio]';
        const radios = this.$el.querySelectorAll(className);
        const length = radios.length;
        const index = [].indexOf.call(radios, target);
        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;
        }
      }
复制代码

switch case语句没有break默认向下执行,因此上左 和 下右 分别只写了一个执行函数和break(执行相同)app

radio

结构

1.外层label,控制总体样式

<label class="el-radio" :class="[ border && radioSize ? 'el-radio--' + radioSize : '', { 'is-disabled': isDisabled }, { 'is-focus': focus }, { 'is-bordered': border }, { 'is-checked': model === label } ]" role="radio" :aria-checked="model === label" :aria-disabled="isDisabled" :tabindex="tabIndex" @keydown.space.stop.prevent="model = isDisabled ? model : label" >
  ...
  </label>
复制代码
  • role,aria-checked,aria-disabled三个属性是无障碍页面应用的属性(读屏软件会用到)参考
  • tabindex: 属性规定元素的 tab 键控制次序 ,0为按照顺序,-1为不受tab控制
  • @keydown.space:空格keydown事件(可查阅vue官网按键修饰符)

2.内层第一个span由span和不可见的input(模拟radio)组成(筛选框)

<!-- 单选框 -->
    <span class="el-radio__input" :class="{ 'is-disabled': isDisabled, 'is-checked': model === label }" >
      <span class="el-radio__inner"></span>
    <!-- 不可见input模拟radio -->
      <input class="el-radio__original" :value="label" type="radio" aria-hidden="true" v-model="model" @focus="focus = true" @blur="focus = false" @change="handleChange" :name="name" :disabled="isDisabled" tabindex="-1" >
    </span>
复制代码
  • aria-hidden:也是无障碍页面应用的属性(读屏软件会用到),为true时自动读屏软件会自动跳过,毕竟这是一个隐藏元素

3.内层第二个span显示(筛选框对应的内容)

<!-- 单选文字 -->
    <!-- 阻止冒泡 -->
    <span class="el-radio__label" @keydown.stop>
      <!-- 接收到插槽,显示插槽内容 -->
      <slot></slot>
      <!-- 没有接收到插槽,显示label -->
      <template v-if="!$slots.default">{{label}}</template>
    </span>
复制代码
  • $slots.default :接收匿名插槽内容

script部份

1.引入mixins

同上 用到的是mixins中的dispatch方法

// 用到mixins中的dispatch方法,向上寻找对应的组件并发布事件
import Emitter from 'element-ui/src/mixins/emitter';
复制代码

运用在input的change事件中

handleChange() {
    this.$nextTick(() => {
        // 发布change事件暴露model
        this.$emit('change', this.model);
        // 若是被radio-group组件嵌套,向上找到radio-group组件发布handleChange事件暴露model
        this.isGroup && this.dispatch('ElRadioGroup', 'handleChange', this.model);
    });
}
复制代码
  • $nextTick: 将回调延迟到下次 DOM 更新循环以后执行

2.provide和 inject

// form注入
    inject: {
      elForm: {
        default: ''
      },
      elFormItem: {
        default: ''
      }
    }
复制代码

同上,接收form组件注入属性,影响size及disabled。 (computed中能够看到)

3.computed

  • 是否被radio-group包裹

    // 向上找radio-group组件 有则true无则false
    isGroup() {
        let parent = this.$parent;
        while (parent) {
            if (parent.$options.componentName !== 'ElRadioGroup') {
                parent = parent.$parent;
            } else {
                this._radioGroup = parent;
                return true;
            }
        }
        return false;
    }
    复制代码
  • 实现v-model

    // 实现v-model
          model: {
            // 取值
            get() {
              // radio-group的value或value
              return this.isGroup ? this._radioGroup.value : this.value;
            },
            // 赋值
            set(val) {
              // 被radio-group组件包裹 radio-group组件发布input事件数组形式暴露值
              if (this.isGroup) {
                this.dispatch('ElRadioGroup', 'input', [val]);
              } else {
                // 没有被radio-group组件包裹,直接发布input事件暴露值
                this.$emit('input', val);
              }
            }
          }
    复制代码
  • 控制size,disabled,tabIndex

    _elFormItemSize() {
            return (this.elFormItem || {}).elFormItemSize;
          },
          radioSize() {
            // props的size及form注入的size及全局配置对象($ELEMENT,此对象由引入时Vue.use()传入的默认空对象)的size
            const temRadioSize = this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
            // 被radio-group组件包裹优先radioGroupSize
            return this.isGroup
              ? this._radioGroup.radioGroupSize || temRadioSize
              : temRadioSize;
          },
          isDisabled() {
            // 被radio-group组件包裹,radioGroup的disabled || props的disabled || form注入的disabled,
            // 未被radio-group组件包裹则少第一个条件
            return this.isGroup
              ? this._radioGroup.disabled || this.disabled || (this.elForm || {}).disabled
              : this.disabled || (this.elForm || {}).disabled;
          },
          // 控制tab是否能够选中
          tabIndex() {
            // 当tabindex=0时,该元素能够用tab键获取焦点,且访问的顺序是按照元素在文档中的顺序来focus
            // 当tabindex=-1时,该元素用tab键获取不到焦点,可是能够经过js获取,这样就便于咱们经过js设置上下左右键的响应事件来focus,在widget内部能够用到。
            // 当tabindex>=1时,该元素能够用tab键获取焦点,并且优先级大于tabindex=0;不过在tabindex>=1时,数字越小,越先定位到。
            return !this.isDisabled ? (this.isGroup ? (this.model === this.label ? 0 : -1) : 0) : -1;
          }
    
    复制代码

radio-button

结构

与radio相似,label是button的样式,少了一个单选框的结构(span),input模拟radio而且不可见,另外一个依旧是显示对应单选框内容的span

<label class="el-radio-button" :class="[ size ? 'el-radio-button--' + size : '', { 'is-active': value === label }, { 'is-disabled': isDisabled }, { 'is-focus': focus } ]" role="radio" :aria-checked="value === label" :aria-disabled="isDisabled" :tabindex="tabIndex" @keydown.space.stop.prevent="value = isDisabled ? value : label" >
    <input class="el-radio-button__orig-radio" :value="label" type="radio" v-model="value" :name="name" @change="handleChange" :disabled="isDisabled" tabindex="-1" @focus="focus = true" @blur="focus = false" >
    <span class="el-radio-button__inner" :style="value === label ? activeStyle : null" @keydown.stop>
      <slot></slot>
      <template v-if="!$slots.default">{{label}}</template>
    </span>
  </label>

复制代码

script部分

逻辑与radio基本上同样,来看下有区别的地方

选中时的填充色和边框色

computed: {
        // radio-group组件实例
      _radioGroup() {
        let parent = this.$parent;
        // 向上寻找radio-group组件 有就返回radio-group组件实例 没有返回false
        while (parent) {
          if (parent.$options.componentName !== 'ElRadioGroup') {
            parent = parent.$parent;
          } else {
            return parent;
          }
        }
        return false;
      },
      activeStyle() {
        // 选中时的填充色和边框色
        return {
          backgroundColor: this._radioGroup.fill || '',
          borderColor: this._radioGroup.fill || '',
          boxShadow: this._radioGroup.fill ? `-1px 0 0 0 ${this._radioGroup.fill}` : '',
          color: this._radioGroup.textColor || ''
        };
      }
复制代码
  • fill:是radio-group组件的属性(颜色)
相关文章
相关标签/搜索