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

一、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';  // 注解7
  export default {
    name: 'ElRadio',
    mixins: [Emitter],  // 注解7
    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: {
        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]);
  • this.$emit('input', val);寫法,就可以讓父組件<el-radio v-model="radio" label="1">備選項</el-radio>"里的radio自動更新,實現父子組件間傳值。
<el-radio v-model="radio" label="1">備選項</el-radio>

上訴代碼相當于:

<el-radio :checked="radio"  @change="val => { radio = val }" label="1">備選項</el-radio>
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"  // 當前是否被禁用
  1. 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
  • @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>
7. mixins

混入 (mixin) 提供了一種非常靈活的方式,來分發 Vue 組件中的可復用功能。一個混入對象可以包含任意組件選項。當組件使用混入對象時,所有混入對象的選項將被“混合”進入該組件本身的選項。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。