Vue

深入Vue之解析ElementUI组件--radio

2019-10-08  本文已影响0人  Lia代码猪崽

一、github地址

https://github.com/ElemeFE/element/blob/dev/packages/radio/src/radio.vue

二、文档地址

https://element.eleme.cn/#/zh-CN/component/radio

三、解析过程(很多注释在源代码是不合法的,这里只是为了更直观的展示)

<template>
  <label
    class="el-radio"
    :class="[  // 注解1
      border && radioSize ? 'el-radio--' + radioSize : '',
      { 'is-disabled': isDisabled },
      { 'is-focus': focus },
      { 'is-bordered': border },
      { 'is-checked': model === label }
    ]"
    role="radio"  // 注解2
    :aria-checked="model === label"  // 注解2
    :aria-disabled="isDisabled"  // 注解2
    :tabindex="tabIndex"  // 注解2
    @keydown.space.stop.prevent="model = isDisabled ? model : label"  // 注解3
  >
    <span class="el-radio__input"  // 注解4
      :class="{
        'is-disabled': isDisabled,
        'is-checked': model === label
      }"
    >
      <span class="el-radio__inner"></span>
      <input
        ref="radio"
        class="el-radio__original"
        :value="label"
        type="radio"
        aria-hidden="true"
        v-model="model"  // 注解1.5
        @focus="focus = true"
        @blur="focus = false"
        @change="handleChange"  // 注解5
        :name="name"
        :disabled="isDisabled"
        tabindex="-1"
      >
    </span>
    <span class="el-radio__label" @keydown.stop>  // 注解6
      <slot></slot>
      <template v-if="!$slots.default">{{label}}</template>
    </span>
  </label>
</template>
<script>
  import Emitter from 'element-ui/src/mixins/emitter';
  export default {
    name: 'ElRadio',
    mixins: [Emitter],
    inject: {
      elForm: {
        default: ''
      },
      elFormItem: {
        default: ''
      }
    },
    componentName: 'ElRadio',
    props: {
      value: {},
      label: {},
      disabled: Boolean,
      name: String,
      border: Boolean,
      size: String
    },
    data() {
      return {
        focus: false
      };
    },
    computed: {
      isGroup() {  // 注解1.1
        let parent = this.$parent;
        while (parent) {
          if (parent.$options.componentName !== 'ElRadioGroup') {
            parent = parent.$parent;
          } else {
            this._radioGroup = parent;
            return true;
          }
        }
        return false;
      },
      model: {  // 注解1.5
        get() {
          return this.isGroup ? this._radioGroup.value : this.value;
        },
        set(val) {
          if (this.isGroup) {
            this.dispatch('ElRadioGroup', 'input', [val]);
          } else {
            this.$emit('input', val);
          }
          this.$refs.radio && (this.$refs.radio.checked = this.model === this.label);
        }
      },
      _elFormItemSize() {  // 注解1.1
        return (this.elFormItem || {}).elFormItemSize;
      },
      radioSize() {  // 注解1.1
        const temRadioSize = this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
        return this.isGroup
          ? this._radioGroup.radioGroupSize || temRadioSize
          : temRadioSize;
      },
      isDisabled() {  // 注解1.2
        return this.isGroup
          ? this._radioGroup.disabled || this.disabled || (this.elForm || {}).disabled
          : this.disabled || (this.elForm || {}).disabled;
      },
      tabIndex() {  // 注解2
        return (this.isDisabled || (this.isGroup && this.model !== this.label)) ? -1 : 0;
      }
    },
    methods: {
      handleChange() { // 注解5
        this.$nextTick(() => {
          this.$emit('change', this.model);  // 注解1.5
          this.isGroup && this.dispatch('ElRadioGroup', 'handleChange', this.model);
        });
      }
    }
  };
</script>
1. 动态class样式
  1. 调用组件时候有没有传来border,且radioSize的返回值是否为true,则有类'el-radio--' + radioSize
 // 有this.elFormItem,则返回this.elFormItem.elFormItemSize;否则返回{}.elFormItemSize,即返回undefined
_elFormItemSize() {
  return (this.elFormItem || {}).elFormItemSize; 
},
// 判断是否为radio-group
isGroup() {
    let parent = this.$parent;  // 获取父节点的DOM元素
    while (parent) {
        if (parent.$options.componentName !== 'ElRadioGroup') {
            parent = parent.$parent;  // 改变parent,退出while循环,然后返回false
        } else {
            this._radioGroup = parent;
            return true;  // 已找到radio-group父节点,赋值给了this._radioGroup,返回true,代表是在group里
        }
    }
    return false;
},
 radioSize() {
  const temRadioSize = this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;  // 获取到size
  return this.isGroup
    ? this._radioGroup.radioGroupSize || temRadioSize
          : temRadioSize;    // 如果是在group里,则返回this._radioGroup.radioGroupSize || temRadioSize;否则返回temRadioSize
},
  1. 判断isDisabled返回的值,为true则有类is-disabled
{ 'is-disabled': isDisabled },
// 是否为radio-group,是则返回this._radioGroup.disabled || this.disabled || (this.elForm || {}).disabled;否则返回this.disabled || (this.elForm || {}).disabled
isDisabled() {
    return this.isGroup 
      ? this._radioGroup.disabled || this.disabled || (this.elForm || {}).disabled
         : this.disabled || (this.elForm || {}).disabled;
},
  1. focus是否为true,则有类is-focus
{ 'is-focus': focus },
  1. 调用组件时候有传来border,就有类is-bordered
{ 'is-bordered': border },
  1. model(下面会详讲)是否与label(调用时父组件传来的label值)相等,就有类is-checked
{ 'is-checked': model === label }

model具体,有用到知识点computed的读取和设置

model: {
        get() {
          return this.isGroup ? this._radioGroup.value : this.value;  // 获取:是否在radio-group里,是返回this._radioGroup.value;否则返回this.value
        },
        set(val) {  // 更新
          if (this.isGroup) {  // 如果在radio-group里,
            this.dispatch('ElRadioGroup', 'input', [val]);  // 下面详讲
          } else {
            this.$emit('input', val);  // 如果不是,则直接触发调用父组件input事件,传递val过去
          }
          this.$refs.radio && (this.$refs.radio.checked = this.model === this.label);  
        }
},

Vue组件通信 dispatch,查找所有父级,直到找到要找到的父组件,并在身上触发指定的事件。

// dispatch(componentName, eventName, params) {}
// @param { componentName } 组件名称
// @param { eventName } 事件名
// @param { params } 参数
this.dispatch('ElRadioGroup', 'input', [val]);
2. HTML5中的aria与role

这些都是HTML5针对html tag增加的属性,一般是为不方便的人士提供的功能,比如屏幕阅读器。
role的作用是描述一个非标准的tag的实际作用。比如用divbutton,那么设置divrole="button",辅助工具就可以认出这实际上是个button
aria的意思是Accessible Rich Internet Applicationaria-*的作用就是描述这个tag在可视化的情境中的具体信息。

role="radio"  // 这实际上是个单选radio
:aria-checked="model === label"  // 当前是否被选中
:aria-disabled="isDisabled"  // 当前是否被禁用

2.aria-label只有加在可被tab到的元素上,读屏才会读出其中的内容。可令tabindex0可读,-1不可读:

:tabindex="tabIndex"
// 如果是禁用状态,返回 -1
// 如果是在单选框组里(radio-group),且不是单选选中的,返回-1
// 其余返回0
tabIndex() {  
  return (this.isDisabled || (this.isGroup && this.model !== this.label)) ? -1 : 0;
}
3. @keydown.space.stop.prevent
// 当按下键盘的空格键时,如果是禁用状态,不改变model的值;否则model为label的值;
// 停止冒泡、阻止默认行为
@keydown.space.stop.prevent="model = isDisabled ? model : label"  
4. 单选框圆点

这一部分的全部HTML代码如下,待会儿会进行拆解:

<span class="el-radio__input" 
  :class="{
    'is-disabled': isDisabled, 
    'is-checked': model === label
  }"
>
  <span class="el-radio__inner"></span>
  <input
        ref="radio"
        class="el-radio__original"
        :value="label"
        type="radio"
        aria-hidden="true"
        v-model="model"  // 注解1.5
        @focus="focus = true"
        @blur="focus = false"
        @change="handleChange"
        :name="name"
        :disabled="isDisabled"
        tabindex="-1"
      >
</span>
未选中
选中
  1. 外层,定位居中,判断是否为禁用和选中
<span class="el-radio__input" 
  :class="{
    'is-disabled': isDisabled,  // 如果为禁用状态则有'is-disabled'类样式
    'is-checked': model === label  // 如果为当前选中则有'is-checked'类样式
  }"
>
...
</span>
.el-radio__input {
    white-space: nowrap;
    cursor: pointer;
    outline: none;
    display: inline-block;
    line-height: 1;
    position: relative;
    vertical-align: middle;
}
  1. 内层1,实际上显示的样式
<span class="el-radio__inner"></span>
.el-radio__inner {
    border: 1px solid #dcdfe6;
    border-radius: 100%;
    width: 14px;
    height: 14px;
    background-color: #fff;
    position: relative;
    cursor: pointer;
    display: inline-block;
    box-sizing: border-box;
}
.el-radio__inner:after {
    width: 4px;
    height: 4px;
    border-radius: 100%;
    background-color: #fff;
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%,-50%) scale(0);
    transition: transform .15s ease-in;
}

/*禁用样式*/
.el-radio__input.is-disabled .el-radio__inner {
    background-color: #f5f7fa;
    border-color: #e4e7ed;
    cursor: not-allowed;
}
/*选中样式*/
.el-radio__input.is-checked .el-radio__inner {
    border-color: #409eff;
    background: #409eff;
}
.el-radio__input.is-checked .el-radio__inner:after {
    transform: translate(-50%,-50%) scale(1); 
}
  1. 内层2,因为太丑被隐藏了,但是实际上有大大的作用
<input
        ref="radio"  // 可通过this.$refs.radio获取到dom节点
        class="el-radio__original"  // 样式
        :value="label"  // 绑定值为父组件调用传来的label
        type="radio"  // 单选
        aria-hidden="true"  // 阅读器模式下隐藏
        v-model="model"  // 注解1.5
        @focus="focus = true" 
        @blur="focus = false"
        @change="handleChange"  // 注解5
        :name="name"  // 绑定值为父组件调用传来的name
        :disabled="isDisabled"  // 根据isDisabled的值来定义是否禁用
        tabindex="-1"  // 阅读器模式下隐藏
>
.el-radio__original {
    opacity: 0;    // 透明度为0,依旧要占个位
    outline: none;
    position: absolute;
    z-index: -1;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    margin: 0;
}
5. $nextTick
handleChange() {
  this.$nextTick(() => {
    this.$emit('change', this.model);  // 触发父组件调用change方法,参数为this.model(注解1.5)的值
    this.isGroup && this.dispatch('ElRadioGroup', 'handleChange', this.model);  // 如果是在单选框组里,调用this.dispatch('ElRadioGroup', 'handleChange', this.model)
  });
}
6. label
<span class="el-radio__label" @keydown.stop>  // 注解6
  <slot></slot>  // 提供插槽
  <template v-if="!$slots.default">{{label}}</template> // 如果没有使用default插槽,显示调用时传进来的label值
</span>
上一篇下一篇

猜你喜欢

热点阅读