elementUI源碼分析-04-radio

一、基礎(chǔ)回顧

el-radio是單選組件,是對原生<input type="radio">的封裝。

先來回顧原生的radio單選,比如做一個單選題的單選按鈕

<input type="radio"  name ="fruit" value="apple" checked>大蘋果
<input type="radio"  name ="fruit" value="banana">大香蕉

checked:表示被選中的
value: 表示單選選項的值
name: 定義 input 元素的名稱,具有相同name可以達到互斥的效果,表示同一時刻只能有一個按鈕被選中

獲取value的值,可以通過click、onchange等事件遍歷元素,checked=true的那一項的value值,就是在最后選中的value值。

有時候還會input和label配合使用,label 元素不會向用戶呈現(xiàn)任何特殊效果,label的for屬性應(yīng)當(dāng)與相關(guān)元素的 id 屬性相同,input配合label使用可以點擊label的內(nèi)容,聚焦到input

    <input type="radio"  name ="fruit" id="apple" checked value="apple">
    <label for="apple">大蘋果</label>


    <input type="radio"  name ="fruit" id="banana" value="banana">
    <label for="banana">大香蕉</label>

因為原生單選在不同瀏覽器下的默認顯示效果不一樣,所以通常情況下,我們都會采用障眼法覆蓋其原生的樣式。

    label{
    line-height: 24px;
    height: 24px;
    display: inline-block;
    margin-left: 5px;
    margin-right:15px;
    color: #777;
    }
    .radio_type{
    width: 20px;
    height:20px;
    appearance: none;
    -moz-appearance:none; /* Firefox */
        -webkit-appearance:none; /* Safari 和 Chrome */
    position: relative;
    }
    .radio_type:before{
    content: '';
    width: 20px;
    height: 20px;
    border: 2px solid #EDD19D;
    display: inline-block;
    border-radius: 50%;
    vertical-align: middle;
    }
    .radio_type:checked:before{
    content: '';
    width: 20px;
    height: 20px;
    border: 2px solid #EDD19D;
    display: inline-block;
    border-radius: 50%;
    vertical-align: middle;
    }
    .radio_type:checked:after{
    content: '';
    width: 12px;
    height: 12px;
    text-align: center;
    background:#EDD19D;
    border-radius: 50%;
    display: block;
    position: absolute;
    top: 6px;
    left: 6px;
    }
    .radio_type:checked+label{
    color: #EDD19D;
    }
image.png

二、el-radio用法與源碼

el-radio

基礎(chǔ)的引用方式如下:

<template>
  <el-radio v-model="radio" label="1">備選項</el-radio>
  <el-radio v-model="radio" label="2">備選項</el-radio>
</template>

<script>
  export default {
    data () {
      return {
        radio: '1'
      };
    }
  }
</script>

接受的參數(shù)如下

參數(shù) 說明 類型 可選值 默認值
value / v-model 綁定值 string / number / boolean
label Radio 的 value string / number / boolean
disabled 是否禁用 boolean false
border 是否顯示邊框 boolean false
size Radio 的尺寸,僅在 border 為真時有效 string medium / small / mini
name 原生 name 屬性 string

源碼如下

<template>
  <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"
  >
    <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"
        @focus="focus = true"
        @blur="focus = false"
        @change="handleChange"
        :name="name"
        :disabled="isDisabled"
        tabindex="-1"
      >
    </span>
    <span class="el-radio__label" @keydown.stop>
      <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: {
        // 判斷當(dāng)前組件的父組件是否是ElRadioGroup(單選框組)
        isGroup() {
            let parent = this.$parent;
            while (parent) {
                if (parent.$options.componentName !== 'ElRadioGroup') {
                    parent = parent.$parent;
                } else {
                    this._radioGroup = parent;
                    return true;
                }
            }
            return false;
        },
        model: {
            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() {
            return (this.elFormItem || {}).elFormItemSize;
        },
        radioSize() {
            const temRadioSize = this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
            return this.isGroup
                ? this._radioGroup.radioGroupSize || temRadioSize
                : temRadioSize;
        },
        isDisabled() {
            return this.isGroup
            ? this._radioGroup.disabled || this.disabled || (this.elForm || {}).disabled
            : this.disabled || (this.elForm || {}).disabled;
        },
        tabIndex() {
            return (this.isDisabled || (this.isGroup && this.model !== this.label)) ? -1 : 0;
        }
    },

    methods: {
      handleChange() {
        this.$nextTick(() => {
          this.$emit('change', this.model);
          this.isGroup && this.dispatch('ElRadioGroup', 'handleChange', this.model);
        });
      }
    }
  };
</script>

name:用于給原生input元素的設(shè)置name屬性,用于達到相同name互斥的效果

border: 是否為選項添加邊框,如果為true,則設(shè)置is-bordered類名為元素添加邊框

.el-radio.is-bordered {
    padding: 12px 20px 0 10px;
    border-radius: 4px;
    border: 1px solid #dcdfe6;
    box-sizing: border-box;
    height: 40px;
}

size:與border配合使用時才生效,用來設(shè)置選項按鈕大小

disabled:給原生input元素設(shè)置disable屬性,使其禁用。

label:Radio 的 value值

value / v-model:用來實現(xiàn)雙向數(shù)據(jù)綁定的。

三、功能點解密

樣式設(shè)置

el-redio樣式

el-radio也是覆蓋了radio原有的樣式。label下面嵌套了2個span,第一個span是圖標(biāo)部分,第二個span是文字部分,

第一個span中又嵌套了一個span和input,里面的span是模擬的圓形按鈕,input是真正的radio標(biāo)簽,el-input隱藏了原有的input樣式,然后用span標(biāo)簽去模擬input標(biāo)簽。

隱藏原生input的樣式,將其設(shè)置opacity為0,使其在頁面中不可見,但是可以點擊

.el-radio__original {
    opacity: 0;
    outline: none;
    position: absolute;
    z-index: -1;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    margin: 0;

設(shè)置span的樣式,模擬input

// 空心,未選中轉(zhuǎn)臺的按鈕
.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;
}
空心 未選中轉(zhuǎn)臺的按鈕
// 選中狀態(tài)下的按鈕
el-radio__input.is-checked .el-radio__inner {
    border-color: #409eff;
    background: #409eff;
}
.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__input.is-checked .el-radio__inner:after {
    transform: translate(-50%,-50%) scale(1);
}
.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;
}

v-model/value

單獨引用el-radio組件的時候,都會使用v-model去綁定data下面的值來實現(xiàn)雙向數(shù)據(jù)綁定,也就是說有時候即使我們不設(shè)置name屬性,也可以達到互斥的效果,所以我們?nèi)タ聪?code>v-model/value是如何實現(xiàn)的呢?

其實v-model/value只是v-bind和v-on的語法糖,在使用v-model綁定數(shù)據(jù)以后,既綁定了數(shù)據(jù),又添加了事件監(jiān)聽,這個事件就是input事件。

官方文檔給出:

<input v-model="something">

這不過是以下示例的語法糖:

<input
  v-bind:value="something"
  v-on:input="something = $event.target.value">

這就相當(dāng)于對input元素的input事件進行監(jiān)聽,來實現(xiàn)value值的綁定,至于如何實現(xiàn)互斥,其實就是如果單選框的value值和v-model值相同,那么就給當(dāng)前input元素添加一個checked屬性,表示被選中,其他不相等的,就不添加checked屬性,就實現(xiàn)了互斥的效果。

在el-radio的源碼中,對input設(shè)置的是v-model="model",并且對model設(shè)置了getter和setter,然后emit一個input事件,在官網(wǎng)中可以看到解釋.

允許一個自定義組件在使用 v-model 時定制 prop 和 event。默認情況下,一個組件上的 v-model 會把 value 用作 prop 且把 input 用作 event。

<my-checkbox v-model="foo" value="some value"></my-checkbox>

上述代碼相當(dāng)于

<my-checkbox
  :checked="foo"
  @change="val => { foo = val }"
  value="some value">
</my-checkbox>

tabindex和:aria-&

tabIndex和aria-* 都是屬于無障礙學(xué)習(xí)的一些設(shè)置。

html中的tabIndex屬性可以設(shè)置鍵盤中的TAB鍵在控件中的移動順序,即焦點的順序。幾乎所有瀏覽器均 tabindex 屬性,除了 Safari。

tabindex有三個值:0 ,-1, 以及X(X里32767是界點)。

當(dāng)tabindex>=1時,該元素可以用tab鍵獲取焦點,數(shù)字越小,越先定位到。

tabIndex=0 ,將排列在所有tabIndex>=1的控件之后。默認情況下tabIndex=0

當(dāng)tabindex=-1時,該元素用tab鍵獲取不到焦點,但是可以通過js獲取.

支持 tabindex 屬性的元素:<a>, <area>, <button>, <input>, <object>, <select> 以及 <textarea>。

可以用以下代碼,使用tab鍵感受以下。

  <a href="0.com" tabindex="0">0</a>
  <a href="-1.com" tabindex="-1">-1</a>
  <a href="1.com" tabindex="1">1</a>
  <a href="2.com" tabindex="2">2</a>
  <a href="3.com" tabindex="3">3</a> 

role、aria-checked、aria-disabled,這些是為屏幕閱讀器準(zhǔn)備的,aria由一套屬性組成,屬性分為role以及對應(yīng)的states和properties,aria將html元素分為六種role,每種有對應(yīng)的states和properties,用以模擬一些tag,更詳細的可點次查看《aria初探(一)》

@keydown.space.stop.prevent="model = isDisabled ? model : label" 這句也很巧妙,查了才知道,原來是為了tab切換不同選項時,按空格可以快速選擇目標(biāo)項。

mixin

mixin用來封裝vue組件的可復(fù)用功能,一個混入對象可以包含任意組件選項。當(dāng)組件使用混入對象時,所有混入對象的選項將被“混合”進入該組件本身的選項。

當(dāng)組件和混入對象含有同名選項時,這些選項將以恰當(dāng)?shù)姆绞竭M行“合并”。

  • 數(shù)據(jù)對象在內(nèi)部會進行遞歸合并,并在發(fā)生沖突時以組件數(shù)據(jù)優(yōu)先。
  • 值為對象的選項,例如 methods、components 和 directives,將被合并為同一個對象。兩個對象鍵名沖突時,取組件對象的鍵值對。

這里是混入了一個Emitter,也就是說該組件擁有Emitter中的方法。

function broadcast(componentName, eventName, params) {
  this.$children.forEach(child => {
    var name = child.$options.componentName;

    if (name === componentName) {
      child.$emit.apply(child, [eventName].concat(params));
    } else {
      broadcast.apply(child, [componentName, eventName].concat([params]));
    }
  });
}
export default {
  methods: {
    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));
      }
    },
    broadcast(componentName, eventName, params) {
      broadcast.call(this, componentName, eventName, params);
    }
  }
};

Emitter的源碼中,是在methods混入了dispatch、broadcast兩個方法,說明在其他的組件中也會多次使用這個兩個方法。

dispatch

dispatch接受三個參數(shù)、componentName組件名,eventName事件名,params事件參數(shù)、

dispatch主要作用就是找到距離自己最近的目標(biāo)父組件,然后調(diào)用目標(biāo)組件的 目標(biāo)事件,并傳遞參數(shù)。

這里調(diào)用目標(biāo)事件使用的是parent.$emit.apply(parent, [eventName].concat(params));, 你可能會有疑問,為什么這么調(diào)用,不直接parent.$emit(eventName,...params)?

首先,vm.$emit( event, arg )的作用是觸發(fā)當(dāng)前實例上的事件,apply主要作用是改變this指向,那么那個調(diào)用方式就是用parent對象去調(diào)用parent對象的eventName。

即parent拿到parent的$emit方法,再傳遞對應(yīng)的事件參數(shù)。

broadcast

broadcast也接受三個參數(shù)、componentName組件名,eventName事件名,params事件參數(shù)、

broadcast主要作用是像后代組件傳值,會遍歷所有后代組件,如果是目標(biāo)組件,就調(diào)用目標(biāo)子組件的目標(biāo)方法,并傳遞參數(shù)。

與dispatch類似。

四、button-group、radio-button

el-radio中很多地方都計算了isGroup,這是因為ele還提供了一個el-radio-group組件,適用于在多個互斥的選項中選擇的場景,所以在設(shè)置class或者,觸發(fā)input事件時都先判斷是否是isGroup,如果isGroup,那么就采用isGroup的值或者事件。

radio-button和el-radio功能一樣,只是樣式的區(qū)別。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內(nèi)容