拜读及分析Element源码-radio组件篇

element-ui 单选框radio组件源码分析,也是很常用的一个

单选框组件分为3部分

  1. radio-group: 单选组,适用于多个互斥的选项中选择的场景
  2. radio: 单选
  3. radio-button: 按钮样式的单选

2可以单独使用,也可与1组合使用,3和1要组合使用

radio-group

结构

很简单相当于是一个父容器,并且提供了键盘上下左右选中的方法

1
2
3
4
5
6
7
<div
class="el-radio-group"
role="radiogroup"
@keydown="handleKeydown"
>
<slot></slot>
</div>

slot接收的内容就是radio或radio-button了

script部分

1. 导入mixins

1
import Emitter from 'element-ui/src/mixins/emitter';

这是其实就是用到emitter.js里的dispatch 方法(向上找到指定组件并发布指定事件及传递值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 接收组件名,事件名,参数
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时用到

1
2
3
4
5
6
watch: {
// 监听选中值,向上找到from-item组件发布el.form.change(应该是用于表单验证)
value(value) {
this.dispatch('ElFormItem', 'el.form.change', [this.value]);
}
}

2.声明 冻结上下左右的keyCode组成的对象

1
2
3
4
5
6
const keyCode = Object.freeze({
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40
});

Object.freeze() 方法可以冻结一个对象,冻结指的是不能向这个对象添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性。该方法返回被冻结的对象。

3.若form-item组件注入属性影响size(默认为空)

1
2
3
4
5
inject: {
elFormItem: {
default: ''
}
}

size在computed里

1
2
3
4
5
6
7
8
9
// 最终大小
computed: {
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
radioGroupSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
}
}

4.生命周期及watch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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,控制整体样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 <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)组成(筛选框)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- 单选框 -->
<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显示(筛选框对应的内容)

1
2
3
4
5
6
7
8
<!-- 单选文字 -->
<!-- 阻止冒泡 -->
<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方法

1
2
// 用到mixins中的dispatch方法,向上寻找对应的组件并发布事件
import Emitter from 'element-ui/src/mixins/emitter';

运用在input的change事件中

1
2
3
4
5
6
7
8
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

1
2
3
4
5
6
7
8
9
// form注入
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
}

同上,接收form组件注入属性,影响size及disabled。 (computed中可以看到)

3.computed

  • 是否被radio-group包裹

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 向上找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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 实现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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    _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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<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基本上一样,来看下有区别的地方

选中时的填充色和边框色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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组件的属性(颜色)
------ 本文结束------
0%