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

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

单选框组件分为3部分

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

2可以单独使用,也可与1组合使用,3和1要组合使用

radio-group

结构

很简单相当于是一个父容器,并且提供了键盘上下左右选中的方法

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

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

script部分

1. 导入mixins

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

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

    // 接收组件名,事件名,参数
    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时用到

    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(执行相同)

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
        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>
      
      <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组件的属性(颜色)

你可能感兴趣的:(拜读及分析Element源码-radio组件篇)