拜讀及分析Element原始碼-radio元件篇
element-ui 單選框radio元件原始碼分析,也是很常用的一個
單選框元件分為3部分
- radio-group:單選組,適用於多個互斥的選項中選擇的場景
- radio: 單選
- radio-button: 按鈕樣式的單選
2可以單獨使用,也可與1組合使用,3和1要組合使用
radio-group
結構
很簡單相當於是一個父容器,並且提供了鍵盤上下左右選中的方法
<div class="el-radio-group" role="radiogroup" @keydown="handleKeydown" > <slot></slot> </div> 複製程式碼
slot接收的內容就是radio或radio-button了
script部分
1. 匯入mixins
import Emitter from 'element-ui/src/mixins/emitter'; 複製程式碼
這是其實就是用到emitter.js裡的dispatch 方法(向上找到指定元件併發布指定事件及傳遞值)
// 接收元件名,事件名,引數 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時用到
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(執行相同)
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元件的屬性(顏色)