拜讀及分析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組件的屬性(顏色)
相關文章
相關標籤/搜索