本文繼續帶你看錶單組件 radio,若是你沒有讀過另外一篇文章 Input,我建議你先看完那個再來,由於不少東西在那裏面分析了。html
首先讓咱們來了解一下 radio 在表單中的做用:前端
原生的 radio 想必你們都很熟悉,平時開發中也會常常用到,先看一下它經常使用的兩個屬性vue
name
單選按鈕的名稱value
單選按鈕須要傳給服務器的值這裏重點關注一下value
,它在前端頁面上並不會起到什麼做用,甚至不會顯示,可是最主要的就是能夠經過它將單選按鈕選擇的值傳遞給服務器,好讓後臺程序知道用戶選擇了什麼。爲何要講這個,別急,對後面的理解確定有幫助。node
既然 ElementUI 是基於 Vue 開發的,那麼在 Vue 中是如何使用 radio 的呢?ios
移步官網查看表單輸入綁定git
從官網中咱們能夠看到實現 radio 的雙向綁定也是使用了v-model
語法糖,它會根據控件類型自動選取正確的方法來更新元素。那麼在 radio 組件中就等效於給value
屬性和input
事件同時綁定一個響應式數據。當選中單選按鈕時v-model
綁定的值一般就是value
的值github
<!-- 當選中時,`picked` 爲字符串 "a" -->
<input type="radio" v-model="picked" value="a">
複製代碼
<template>
<label class="el-radio">
<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>
</template>
複製代碼
從這個結構中咱們能夠看出整個 radio 組件包裹在 label 標籤中,首先簡單瞭解一下 label 標籤,它是爲 input 元素定義標記的,label 元素不會向用戶呈現任何特殊效果,當用戶選擇該標籤時,瀏覽器就會自動將焦點轉到和標籤相關的表單控件上,也就是說當咱們點擊 label 時,其實就是點擊了內部的 input 標籤。這樣作爲用戶點擊按鈕提供了便捷,只要在 label 範圍內點擊都能觸發按鈕的事件。編程
裏層有兩個span
標籤,第一個表示的是前面的小圓圈,因爲各瀏覽器對於標籤的默認樣式不統一,咱們又須要保證咱們的組件在任何地方運行都能保持一致的效果,一般會把原生 input 樣式隱藏起來,經過一個span
或者div
來模擬,源碼的具體樣式後期再分析。第二個span
表示的是按鈕後面的顯示文字,默認傳遞的內容會在插槽中渲染,若是用戶沒有直接傳遞內容,那麼就須要把用戶傳遞的 label 值顯示出來,這就是後面的template
作的事。json
接下來重點看一下 input 屬性瀏覽器
<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" />
複製代碼
這裏面會涉及 radio 組件的屬性和方法,因此我打算放在一塊兒來分析。
有一些在個人另外一篇文章 Input裏面分析過,這裏就再也不贅述,建議傳送過去看一下。
el-radio__original
類將 input 隱藏同時還能觸發事件:value
是把父組件傳遞過來的 label 給原生value
屬性:name
綁定原生name
這裏重點要說的是v-model="model"
,這個怎麼理解呢?不急,慢慢解釋。
當咱們須要在多個單選按鈕中裏面來控制同一個值時,要使用v-model
雙向綁定一個值,這個值是什麼?看一個例子
<template>
<el-radio v-model="gender" label="0">男</el-radio>
<el-radio v-model="gender" label="1">女</el-radio>
</template>
複製代碼
data() {
return {
gender: '0'
}
}
複製代碼
使用組件發現v-model
綁定的是父組件的gender
值,可是子組件自己不可以直接使用v-model='value'
來雙向綁定數據,由於這個value
來自於父組件,根據 Vue 的單項數據流,子組件通常狀況下是不能直接更改父組件傳遞過來的數據的,因此須要定義一個本身的model
用來綁定 radio 組件的數據,可是這個model
也不是經過data
來寫死的,由於它是取決於外界傳進來的value
值,同時還要修改這個值。
從源碼中能夠看到,model
是一個計算屬性,當它被當成一個雙向數據綁定的值時,就不能是一個函數,而是一個對象,提供get
和set
方法。
model: {
get () {
// 若是在一個單選按鈕組裏就是按鈕組的值
return this.isGroup ? this._radioGroup.value : this.value
},
set (val) {
// 若是是 radio 包裹在按鈕組裏,那麼 model 的改變就須要觸發父組件的 input 事件
if (this.isGroup) {
this.dispatch('ElRadioGroup', 'input', [val])
} else {
this.$emit('input', val)
}
// 若是當前的 model 等於組件的 label 就表示當前這個按鈕被選中了
this.$refs.radio &&
(this.$refs.radio.checked = this.model === this.label)
}
}
複製代碼
有關
dispatch
方法參考另外一篇文章 Input,裏面有很是詳細的分析emitter.js
文件的詳細解釋也以及上傳到個人 Github 上了,歡迎 star!
當咱們須要獲取model
值時會調用它的get
方法,get方法裏面會判斷 radio 是否是在一個單選按鈕組裏(具體見下文),若是在,那麼 radio 自己是沒有value
的,value
是經過radio-group
的v-model
傳遞過來的,如今咱們來看一下isGroup
這個計算屬性:
// 判斷 radio 標籤是否在按鈕組裏
isGroup () {
let parent = this.$parent
while (parent) {
if (parent.$options.componentName !== 'ElRadioGroup') {
parent = parent.$parent
} else {
// 將按鈕組添加到當前組件實例的 _radioGroup 屬性上
// 並結束循環
this._radioGroup = parent
return true
}
}
return false
}
複製代碼
在這個屬性裏經過循環遍歷父組件,直到找到radio-group
組件把它賦值給_radioGroup
,而後咱們就可以在 radio
組件中使用radio-group
組件實例中的數據和方法了,這裏爲何不能直接用$parent
呢?是由於有考慮到組件嵌套,若是radio
組件的直接父組件不是radio-group
,那$parent
指向的就不是咱們須要的組件,因此這裏一直遍歷$parent
的$parent
就是爲了將parent
指向正確的組件。
固然這裏咱們還可使用「依賴注入」的方法去實現深層次的父子通訊,在radio-group
中定義須要注入的屬性,將radio-group
組件的實例傳過去:
provide () {
return {
radioGroup: this
}
}
複製代碼
radio
組件使用inject
來接收,默認是一個空字符串:
inject: {
radioGroup: {
default: ''
}
}
複製代碼
至於methods
裏面的方法handleChange
是用來處理 radio 的 change 事件的,可是目前還不明白爲何要使用nextTick()
,但願大佬能分享一下。
handleChange () {
// 這裏不太清楚爲何使用 nextTick()
this.$nextTick(() => {
this.$emit('change', this.model)
// 若是存在按鈕組
this.isGroup &&
this.dispatch('ElRadioGroup', 'handleChange', this.model)
})
}
複製代碼
ElementUI 提供了單選按鈕組來包裹一組互斥的按鈕,使得咱們在使用的時候只須要將v-model
雙向綁定在radio-group
上,而在radio
裏面只須要傳入label
便可。來看一下RadioGroup
組件的封裝:
<template>
<component :is="_elTag" class="el-radio-group" role="radiogroup" @keydown="handleKeydown" >
<slot></slot>
</component>
</template>
複製代碼
能夠看出來結構仍是很簡單的,就是一個component
包裹了一個插槽,使用:is
來決定須要將component
渲染成哪一個組件,_elTag
是一個計算屬性
_elTag() {
return (this.$vnode.data || {}).tag || 'div';
}
複製代碼
返回的是當前組件的虛擬 DOM 節點的標籤,默認是div
。
接着往script
裏面看,一開始就定義了一個對象,可是這個對象又不是普通的對象,它是被凍結起來的,這裏就要詳細說一下Object.freeze()
的做用了。
該方法用於凍結一個對象,被凍結的對象不能夠修改,也就是對象身上不能添加或者刪除屬性,也不能修改「已有屬性的可枚舉性、可配置性、可寫性以及原有的值」。另外它的原型對象也不容許修改,返回的是這個對象自己而不是副本。若是強行修改對象,通常狀況下是靜默失敗的,也就是不會報錯,可是在「嚴格模式」下會拋出TypeError
異常。
值得一提的是它是「淺凍結」,對於被凍結的對象中若是某個屬性是一個「引用類型」的值,那麼引用類型的值只要地址不發生變化它的屬性值是能夠更改的,要想實現「深凍結」就須要使用遞歸循環對象,具體實現參考 MDN Object.freeze()
// 凍結對象
const keyCode = Object.freeze({
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40
});
複製代碼
源碼是將鍵盤上的「上下左右」按鍵的鍵盤碼做爲一個對象凍結起來了,這樣可以防止後續代碼不當心修改了它的值。那麼爲何要使用鍵盤碼呢?
在實際使用過程當中,能夠發現使用方向鍵能夠切換選中的按鈕,這也是爲了使用過程當中儘可能減小手離開鍵盤吧
// 左右上下按鍵 能夠在 radio 組內切換不一樣選項
handleKeydown(e) {
// 事件觸發的元素
const target = e.target;
// 若是當前的不是 input 元素,那就是 label 標籤
const className = target.nodeName === 'INPUT' ? '[type=radio]' : '[role=radio]';
const radios = this.$el.querySelectorAll(className);
const length = radios.length;
// 拿到事件觸發元素在全部 radio 裏的索引
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();
// 索引爲 0 表示當前按鈕的第一個觸發了鍵盤事件
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;
}
}
複製代碼
分析一下具體實現的步驟:
click
和focus
事件最後還有一個監聽屬性value
// 監聽 value 若是發生變化就 form-item 組件觸發 change 事件
watch: {
value(value) {
this.dispatch('ElFormItem', 'el.form.change', [this.value]);
}
}
複製代碼
由於咱們的組件最終確定都是會放在 form 表單裏面的,監聽value
是爲了當它的值發生變化時及時通知 form 表單更新數據。
最後來總結一下 radio 組件的封裝:
v-model
進行數據雙向綁定radio-group
包裹radio
使得在一組裏面只能單一選擇雖然提供的功能很簡單,經過閱讀和分析,咱們的編程能力必定會有所提升的,再本身動手封裝一個 radio 組件你會更加了解一個組件的封裝須要考慮哪些問題。
OK,下一篇再見。
【2020.3.15】超詳細 ElementUI 源碼分析 —— Input
【2020.3.16】超詳細 ElementUI 源碼分析 —— Layout