單選框這個組件看似簡單,實則知識點衆多,較爲複雜,若是寫一個html的原生單選框,那確實很簡單,可是封裝一個完整的單選組件就不那麼簡單了,接下來咱們先介紹Vue的單選框的一些原理,而後再分析Element的單選框實現css
原生單選框很簡單,若是咱們要實現一個男女性別的單選按鈕組,代碼只需以下幾句html
<input type="radio" name="sex" value="male" checked>男</input>
<input type="radio" name="sex" value="female">女</input>
複製代碼
上面的男的單選按鈕添加了checked
屬性,表示被選中,value
屬性表示單選按鈕的值,能夠給每一個input添加onchange
和onclick
事件來經過點擊獲取其值,也能夠經過一個按鈕點擊後遍歷全部單選的input按鈕,獲取checked
屬性爲true
的那一項,而後再獲取其value
注意如何讓一組單選互斥,也就是說同一時刻只能有一個單選被選中,name
屬性就是這個做用, 經過把一些單選按鈕的name
設置爲同一個值,就達到了互斥的效果
而Vue的單選框則有所不一樣,代碼以下vue
v-model
便可達到互斥效果,
v-model
的值是data裏面的數據,進行了雙向綁定,因而可知並無經過
name
屬性來達到互斥,那麼時怎麼實現的呢?首先先來了解下v-model的本質,v-model本質上是語法糖
官網說的很清楚,這就至關於進行了一個雙向綁定,對input輸入框的input事件進行監聽,當鍵盤敲下時就實時改變searchText的值,同時修改searchText的值,輸入框的value也跟着變化。那麼底層是怎麼處理互斥的呢?經過查看v-model相關源碼
function genRadioModel ( el: ASTElement, value: string, modifiers: ?ASTModifiers ) {
const number = modifiers && modifiers.number
let valueBinding = getBindingAttr(el, 'value') || 'null'
valueBinding = number ? `_n(${valueBinding})` : valueBinding
addProp(el, 'checked', `_q(${value},${valueBinding})`)
addHandler(el, 'change', genAssignmentCode(value, valueBinding), null, true)
}
複製代碼
上述代碼是處理單選框model的代碼,genRadioModel
參數中的value
就是input的value的值,而valueBinding
的值就是v-model中的v-bind:value的值git
<input type="radio" id="jack" value="Jack" v-model="name">
複製代碼
若是示例如上,那麼addProp
這個方法就會把checked
屬性的值_q('Jack',name)
放入屬性列表,這裏_q是looseEqual
方法的簡寫,表示寬鬆比較(若是是對象,則經過JSON.stringify轉成字符串比較,不然直接String()轉換比較)2個值是否相同,這樣這裏的邏輯就明確了,若是單選框的value的值和v-model的值相同,那麼就加上一個checked
屬性,表示該單選被選中,天然而然其餘單選框value的值和v-model的值不一樣,因此就不是選中狀態,沒有checked屬性,因此達到了互斥效果github
整個單選組件的源碼不算太長,可是裏面知識點不少,先上源碼,官網代碼點此element-ui
<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
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: {
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);
}
}
},
_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 ? 0 : -1) : 0) : -1;
}
},
methods: {
handleChange() {
this.$nextTick(() => {
this.$emit('change', this.model);
this.isGroup && this.dispatch('ElRadioGroup', 'handleChange', this.model);
});
}
}
};
</script>
複製代碼
首先分析template部分,分析一個組件首先得搞清楚組件的html結構,上面的代碼結構簡化後以下數組
<label ...>
<span class='el-radio__input'>
<span class='el-radio__inner'></span>
<input type='radio' .../>
</span>
<span class='el-radio__label'>
<slot></slot>
<template v-if="!$slots.default">{{label}}</template>
</span>
</label>
複製代碼
因而可知,整個組件是一個外層label套2個span,咱們知道原生的radio標籤很醜,樣式在各個瀏覽器不統一,因此必須本身實現全部radio按鈕的樣式,通常作法是隱藏真正的input,本身用div或者span模擬input標籤,這裏的label放在最外層的做用是擴大鼠標點擊範圍,不管是點擊在文字仍是input上都可以觸發響應,固然以下經過for屬性綁定input的id屬性也能夠實現瀏覽器
<input id='t' type='radio'>
<label for='t'>點此</label>
複製代碼
前者被稱爲隱式連接,後者是顯示連接,很明顯前者不須要id,確定前者好,label裏面2個內聯的span水平排列,根據下圖bash
能夠猜到,第一個span表明模擬的圓形按鈕,第二個span表明文字部分,而第一個span裏面又有一個span和input,這個span就是模擬的圓圈,然後面的input纔是真正的radio按鈕,不過被隱藏了,那麼是怎麼隱藏的呢?查看css以下 真正的input透明度爲0,且是絕對定位脫離文檔流,所以不佔空間且咱們看不到,注意不是display:none
或者
visibility:hidden
,若是是none或者hidden的話則沒法觸發鼠標點擊了,
只有opacity:0
才能達到目的,這是個須要注意的地方
接下來看label中的第二個span,這個span就是咱們填充的文本app
<span class='el-radio__label'>
<slot></slot>
<template v-if="!$slots.default">{{label}}</template>
</span>
複製代碼
這個span裏作了處理,slot默認渲染咱們在<el-radio>
和</el-radio>
間的文本,注意template,若是咱們什麼都不填,好比咱們這麼寫
<el-radio label='1'></el-radio>
複製代碼
最終文本就渲染成其label的值
template經過$slot.default
進行判斷是否存在子元素從而決定是否渲染,注意template本身自己不會被渲染出來,只是起一個佔位符的做用
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"
>
複製代碼
首先第一句class="el-radio"
代表了label的基礎類class,裏面有什麼呢?
@include b(radio) {
color: $--radio-color;
font-weight: $--radio-font-weight;
line-height: 1;
position: relative;
cursor: pointer;
display: inline-block;
white-space: nowrap;
outline: none;
font-size: $--font-size-base;
複製代碼
無非就是規定了一些很基礎的css樣式,鼠標樣式,不換行,無輪廓,字體大小顏色等 而後第二句:class
代表了動態綁定的類,其中有是否禁用,是否得到焦點,是否有邊框,是否選中等。首先看是否禁用類is-disabled
,部分scss代碼以下
.el-radio__inner {
background-color: $--radio-disabled-input-fill;
border-color: $--radio-disabled-input-border-color;
cursor: not-allowed;
&::after {
cursor: not-allowed;
background-color: $--radio-disabled-icon-color;
}
複製代碼
可見禁用類就是修改了背景色和邊框色以及鼠標樣式變爲禁止符號,固然這只是樣式上的禁止,功能上的禁止是如何實現的呢?功能上的禁用是經過設置input的disabled屬性來實現,下面源碼中的真正的input的:disabled="isDisabled"
一句話就實現了單選按鈕禁止點擊
<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"
>
複製代碼
isDisabled
是計算屬性,代碼以下
isDisabled() {
return this.isGroup
? this._radioGroup.disabled || this.disabled || (this.elForm || {}).disabled
: this.disabled || (this.elForm || {}).disabled;
},
複製代碼
這裏首先經過isGroup
來判斷本身是不是在單選組裏,單選組也是一個Element組件,代碼以下,經過將一系列單選按鈕放在一塊兒造成一個框組來進行操做,這裏只需設置一個v-model在最外層便可
<el-radio-group v-model="radio2">
<el-radio :label="3">備選項</el-radio>
<el-radio :label="6">備選項</el-radio>
<el-radio :label="9">備選項</el-radio>
</el-radio-group>
複製代碼
那麼isGroup
是啥呢,看代碼,它是一個計算屬性,首先獲取當前組件的父級組件,而後檢查其組件名是不是ElRadioGroup
即單選框組,若是不是就繼續檢查父級的父級,這裏的知識在前面文章介紹過。這個方法會找到距離本身最近的父級ElRadioGroup
組件
isGroup() {
let parent = this.$parent;
while (parent) {
if (parent.$options.componentName !== 'ElRadioGroup') {
parent = parent.$parent;
} else {
this._radioGroup = parent;
return true;
}
}
return false;
},
複製代碼
回過頭來看禁用的邏輯,當本身是被包含在單選框組組件內時,則禁用與否就等於單選框組的禁用與否,這很正常,畢竟整個框組都禁用了,本身也就被禁用了,若是隻是單獨的單選框組件,則禁用就是本身的disabled
這個prop
禁用邏輯結束,而後是{ 'is-focus': focus }
,這句話表明label標籤是否得到is-focus
類,經過focus控制,而focus在上面input的@foucus
和@blur
中進行處理,也就是input是否得到焦點,接下來的is-bordered
經過用戶傳入的border屬性進行控制是否單選框有邊框,後面的is-checked
類表明了當前單選按鈕被選中的樣式,經過model===label
來控制,model是個計算屬性
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);
}
}
},
複製代碼
上面定義了getter和setter,getter首先判斷本身是不是在單選框組組件內,若是是舊返回單選框組的value,不然就是本身的value,而label
則是用戶傳入的一個屬性,表明單選組件本身表明的值,這裏的一個難點是this.value
究竟是啥,查看源碼得知this.value
是一個prop
,可是官網上單選組件根本沒有這個value供用戶定義,這實際上是在組件上使用v-model
的作法,官網介紹以下
v-bind:value
這個prop,
所以在單選組件內得聲明一個叫value的prop,這樣就能夠取到用戶定義的v-model的值,從而加以利用,而
set
方法裏面則必須經過
this.$emit('input', val)
觸發父組件上的oninput事件傳遞出新值,
dispatch
後面咱們再討論
role="radio"
:aria-checked="model === label"
:aria-disabled="isDisabled"
複製代碼
這幾句都是用來爲不方便的人士提供的功能,好比屏幕閱讀器,role的做用是描述一個非標準的tag的實際做用。好比用div作button,那麼設置div 的 role="button",輔助工具就能夠認出這其實是個button。 aria的意思是Accessible Rich Internet Application,aria-*的做用就是描述這個tag在可視化的情境中的具體信息。好比:
<div role="checkbox" aria-checked="checked"></div>
複製代碼
輔助工具就會知道,這個div其實是個checkbox的角色,爲選中狀態,而後是
:tabindex="tabIndex"
@keydown.space.stop.prevent="model = isDisabled ? model : label"
複製代碼
其中tabindex規定了按下tab鍵該元素獲取焦點的順序,一樣是個計算屬性
tabIndex() {
return !this.isDisabled ? (this.isGroup ? (this.model === this.label ? 0 : -1) : 0) : -1;
}
複製代碼
若是爲禁用狀態,tabindex爲-1,則沒法使用tab鍵使該元素獲取焦點,若是不是禁用狀態下,若是該單選按鈕是在單選框組組件內且是選中狀態則能夠經過tab鍵獲取焦點,不然沒法經過tab鍵獲取焦點, 當 tabindex > 0 的元素都切換以後,纔會切換到 tabindex = 0 的元素,而且按出現的前後次序進行切換,這裏的邏輯就是tab只能訪問到選中狀態下的單選按鈕
後面這句@keydown.space.stop.prevent="model = isDisabled ? model : label"
不清楚是幹啥的,我去掉了也能夠正常使用組件,這裏說明按下空格鍵會改變model的值???
注意js部分的mixin:[Emitter]
,首先介紹混入,混入 (mixins) 是一種分發 Vue 組件中可複用功能的很是靈活的方式。混入對象能夠包含任意組件選項。當組件使用混入對象時,全部混入對象的選項將被混入該組件自己的選項。這裏將Emitter
混入進了該組件,也就是說全部該組件都擁有Emitter
中的方法,混入是一個數組,咱們進入emitter.js中看看混入了啥?
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);
}
}
};
複製代碼
很明顯,這裏將methods
進行了混入,添加了dispatch
和broadcast
方法,那麼爲啥不直接在組件的methods裏寫這2個方法呢?緣由在於這樣作會增大代碼量,因爲不少地方都會用到的公用方法,用混入的方法能夠減小代碼量,實現代碼重用,好比有10個組件都要用這2個方法,那麼用混入每一個組件就只寫一行代碼,簡單不少。
混入的methods將會和組件本來的methods合併,若是衝突,則保留組件的methods裏的方法,而後咱們來研究dispatch
方法,該方法實現了向最近的特定父級組件發送事件的邏輯,第一個參數是父級組件的名稱,第二個是事件名稱,第三個參數是事件參數,是一個數組或者單獨的值,邏輯也很簡單:不斷地取到本身的父組件,判斷是不是目標組件,若是不是繼續去其父組件判斷,若是是則在父組件上調用$emit
觸發事件,注意這裏的
parent.$emit.apply(parent, [eventName].concat(params));
複製代碼
不能寫成
parent.$emit(eventName,...params)
複製代碼
必須用apply定$emit
的調用目標對象,由於是在父組件上觸發該事件而不是在dispatch裏,這裏你可能會說parent.$emit
不就是在父組件上調用麼?其實不是,parent.$emit
僅僅是拿到了emit這個方法而已,並無說明在哪裏調用! 這裏要特別注意
而後咱們看看到底哪裏使用了dispatch
方法,答案就是單選組件的methods裏
methods: {
handleChange() {
this.$nextTick(() => {
this.$emit('change', this.model);
this.isGroup && this.dispatch('ElRadioGroup', 'handleChange', this.model);
});
}
}
複製代碼
這裏的handleChange是在單選組件內的input上綁定的,在單選按鈕失去焦點時觸發
<input @change="handleChange" .../>
複製代碼
當點擊不一樣的單選按鈕時會觸發該按鈕的原生onchange事件,這裏又向父級拋出了一個change事件,這是由於單選組件須要一個@change
來講明綁定值變化時觸發的事件,同時將this.model
的值傳遞出去讓用戶拿到該值,以下代碼
<el-radio v-model="v" label='1' @change="radioChange"></el-radio>
複製代碼
而後若是該單選組件是在單選組組件內,則會像單選組組件發送一個handleChange事件告訴父組件:個人值變化啦!不然怎麼通知父組件本身的值!
最後是這個$nextTick
,這個就很微妙了,試着把nextTick去掉,發現單選組件點擊新的組件後,打印出來的值是舊組件的值,這就有問題了,$nextTick
的做用是將回調延遲到下次 DOM 更新循環以後執行,可是這裏爲啥加了nextTick後就能獲取新點擊的單選組件的值了???不明白,但願有大佬能解釋下~