Element-UI radio 单选框源码
radio
模板部分:
<template>
<label
//label 元素不会向用户呈现任何特殊效果。不过,它为鼠标用户改进了可用性。如果您在 label 元素内点击文本,就会触发此控件。就是说,当用户选择该标签时,浏览器就会自动将焦点转到和标签相关的表单控件上。
class="el-radio"
:class="[
border && radioSize ? 'el-radio--' + radioSize : '', //border 与 radioSize 同时存在则有该样式
{ 'is-disabled': isDisabled }, //是否禁用
{ 'is-focus': focus }, //是否选中
{ 'is-bordered': border }, //是否有边框
{ 'is-checked': model === label } // model计算属性见下面 script
]"
role="radio" // role aria-checked aria-disabled 用于屏幕阅读器的,帮助残障人士更好的访问网站
:aria-checked="model === label"
:aria-disabled="isDisabled"
:tabindex="tabIndex" // 设置是否可以通过键盘上的 tab键 进行选择, -1 代表不可选, 0 代表可选
@keydown.space.stop.prevent="model = isDisabled ? model : label"
// keydown.space 空格 .stop 停止冒泡 .prevent 阻止默认行为 .stop.prevent 串联修饰符
//当tab选中当前 radio 在键盘上敲击空格键的时候(space即空格键 ),阻止了原生事件发生
>
<span class="el-radio__input"
:class="{
'is-disabled': isDisabled,
'is-checked': model === label
}"
>
<span class="el-radio__inner"></span> // 采用 css 伪类实现选中效果,代替 input
<input
ref="radio" // 注册 ref
class="el-radio__original" //设置了 opacity: 0 对其隐藏
: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>
// 要是没有插槽内容则显示 label
<template v-if="!$slots.default">{{label}}</template>
</span>
</label>
</template>
问题一:为啥不用原生 input 而是自己模拟
1、原生标签
radio
不同浏览器样式不同,所以自己写来代替2、需要用到原生的
radio
来获取焦点来触发change
事件,所以element
是 采用了 绝对定位老脱离文档流,以及设置透明度为0,而不是采用dispaly:none
或者visibility:hidden
,因为这样就无法点击了
radio
script 部分:
export default {
name: 'ElRadio', // name 三个好处 见其他源码文章
mixins: [Emitter], // mixins 下文单独拎出来与之相关部分 也就是 自己实现 dispatch
inject: { //与 Form 表单相关,表单之内的不在本文说,包括下文的一些属性的判断
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
componentName: 'ElRadio', //用于当前 Vue 实例的初始化选项。需要在选项中包含自定义 property 时会有用处:
/* 比如:
new Vue({
customOption: 'foo',
created: function () {
console.log(this.$options.customOption) // => 'foo'
}
})
*/
props: { // 属性传值
value: {},
label: {},
disabled: Boolean,
name: String,
border: Boolean,
size: String
},
data() {
return {
focus: false
};
},
computed: {
isGroup() { // 是否为 radio-group 组件实例 ,是则为 true , 否则为 false
let parent = this.$parent;
// 向上循环找它爸爸,一直找到为止 也就是 下面的 parent.$options.componentName === 'ElRadioGroup' 的时候
while (parent) {
if (parent.$options.componentName !== 'ElRadioGroup') {
parent = parent.$parent;
} else {
this._radioGroup = parent; // 父实例,如果当前实例有的话
return true;
}
}
return false;
},
model: { // 计算属性的 读取 get 和设置 set
get() { //如果是 radio-group 则取 radio-group 上面的值,否则则取 radio 本身的值
return this.isGroup ? this._radioGroup.value : this.value;
},
set(val) {
if (this.isGroup) {//自己实现 dispatch 事件 Emitter
// 被radio-group组件包裹 radio-group组件发布input事件数组形式暴露值
this.dispatch('ElRadioGroup', 'input', [val]);
} else {
this.$emit('input', val); //$emit 分发事件
}
this.$refs.radio && (this.$refs.radio.checked = this.model === this.label);
}
},
_elFormItemSize() { // FormItem
return (this.elFormItem || {}).elFormItemSize;
},
radioSize() {//radio 尺寸
// 自身尺寸优先级最高
const temRadioSize = this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
//如果为 radio-group 则取 radio-group 上面的尺寸,radio-group 没有设置大小则取自身
return this.isGroup
? this._radioGroup.radioGroupSize || temRadioSize
: temRadioSize;
},
isDisabled() {
//判断是否为 radio-group 有则取 radio-group 的值 反之取 radio
return this.isGroup
? this._radioGroup.disabled || this.disabled || (this.elForm || {}).disabled
: this.disabled || (this.elForm || {}).disabled;
},
tabIndex() {
//tabindex 属性规定元素的 tab 键控制次序(当 tab 键用于导航时)
return (this.isDisabled || (this.isGroup && this.model !== this.label)) ? -1 : 0;
}
},
methods: {
handleChange() {
this.$nextTick(() => {
// 派发事件
this.$emit('change', this.model);
// 如果被radio-group组件嵌套,向上找到radio-group组件发布handleChange事件暴露model
this.isGroup && this.dispatch('ElRadioGroup', 'handleChange', this.model);
});
}
}
};
混入Emitter.js
(这里只说和radio 相关的 dispatch
)
Vue.mixin( mixin )全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。
import Emitter from 'element-ui/src/mixins/emitter';
export default {
mixins: [Emitter],
}
Emitter 中的 dispatch
export default {
methods: {
dispatch(componentName, eventName, params) {
// this.$root 当前组件树的根 Vue 实例。如果当前实例没有父实例,此实例将会是其自己。
var parent = this.$parent || this.$root;
//组件名
var name = parent.$options.componentName;
// 当 parent 存在的时候, name 有值则走 name 是否等于 函数传递进来的 componentName
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
// 当 parent 存在的时候 ,name 则再去该 parent 上面的值
if (parent) {
name = parent.$options.componentName;
}
}
// 当上面循环不满足时,及找到符合组件名称的父级后,发布其事件
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
}
};
调用方法: this.isGroup && this.dispatch('ElRadioGroup', 'handleChange', this.model);
必须用apply定
$emit
的调用目标对象,因为是在父组件上触发该事件而不是在dispatch里,这里你可能会说parent.$emit
不就是在父组件上调用么?其实不是,parent.$emit
仅仅是拿到了emit这个方法而已,并没有说明在哪里调用!