elementUI源码分析-04-radio

一、基础回顾

el-radio是单选组件,是对原生<input type="radio">的封装。

先来回顾原生的radio单选,比如做一个单选题的单选按钮

<input type="radio"  name ="fruit" value="apple" checked>大苹果
<input type="radio"  name ="fruit" value="banana">大香蕉

checked:表示被选中的
value: 表示单选选项的值
name: 定义 input 元素的名称,具有相同name可以达到互斥的效果,表示同一时刻只能有一个按钮被选中

获取value的值,可以通过click、onchange等事件遍历元素,checked=true的那一项的value值,就是在最后选中的value值。

有时候还会input和label配合使用,label 元素不会向用户呈现任何特殊效果,label的for属性应当与相关元素的 id 属性相同,input配合label使用可以点击label的内容,聚焦到input

    <input type="radio"  name ="fruit" id="apple" checked value="apple">
    <label for="apple">大苹果</label>


    <input type="radio"  name ="fruit" id="banana" value="banana">
    <label for="banana">大香蕉</label>

因为原生单选在不同浏览器下的默认显示效果不一样,所以通常情况下,我们都会采用障眼法覆盖其原生的样式。

    label{
    line-height: 24px;
    height: 24px;
    display: inline-block;
    margin-left: 5px;
    margin-right:15px;
    color: #777;
    }
    .radio_type{
    width: 20px;
    height:20px;
    appearance: none;
    -moz-appearance:none; /* Firefox */
        -webkit-appearance:none; /* Safari 和 Chrome */
    position: relative;
    }
    .radio_type:before{
    content: '';
    width: 20px;
    height: 20px;
    border: 2px solid #EDD19D;
    display: inline-block;
    border-radius: 50%;
    vertical-align: middle;
    }
    .radio_type:checked:before{
    content: '';
    width: 20px;
    height: 20px;
    border: 2px solid #EDD19D;
    display: inline-block;
    border-radius: 50%;
    vertical-align: middle;
    }
    .radio_type:checked:after{
    content: '';
    width: 12px;
    height: 12px;
    text-align: center;
    background:#EDD19D;
    border-radius: 50%;
    display: block;
    position: absolute;
    top: 6px;
    left: 6px;
    }
    .radio_type:checked+label{
    color: #EDD19D;
    }
image.png

二、el-radio用法与源码

el-radio

基础的引用方式如下:

<template>
  <el-radio v-model="radio" label="1">备选项</el-radio>
  <el-radio v-model="radio" label="2">备选项</el-radio>
</template>

<script>
  export default {
    data () {
      return {
        radio: '1'
      };
    }
  }
</script>

接受的参数如下

参数 说明 类型 可选值 默认值
value / v-model 绑定值 string / number / boolean
label Radio 的 value string / number / boolean
disabled 是否禁用 boolean false
border 是否显示边框 boolean false
size Radio 的尺寸,仅在 border 为真时有效 string medium / small / mini
name 原生 name 属性 string

源码如下

<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
        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"
      >
    </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: {
        // 判断当前组件的父组件是否是ElRadioGroup(单选框组)
        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);
                }
                this.$refs.radio && (this.$refs.radio.checked = this.model === this.label);
            }
        },
        _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)) ? -1 : 0;
        }
    },

    methods: {
      handleChange() {
        this.$nextTick(() => {
          this.$emit('change', this.model);
          this.isGroup && this.dispatch('ElRadioGroup', 'handleChange', this.model);
        });
      }
    }
  };
</script>

name:用于给原生input元素的设置name属性,用于达到相同name互斥的效果

border: 是否为选项添加边框,如果为true,则设置is-bordered类名为元素添加边框

.el-radio.is-bordered {
    padding: 12px 20px 0 10px;
    border-radius: 4px;
    border: 1px solid #dcdfe6;
    box-sizing: border-box;
    height: 40px;
}

size:与border配合使用时才生效,用来设置选项按钮大小

disabled:给原生input元素设置disable属性,使其禁用。

label:Radio 的 value值

value / v-model:用来实现双向数据绑定的。

三、功能点解密

样式设置

el-redio样式

el-radio也是覆盖了radio原有的样式。label下面嵌套了2个span,第一个span是图标部分,第二个span是文字部分,

第一个span中又嵌套了一个span和input,里面的span是模拟的圆形按钮,input是真正的radio标签,el-input隐藏了原有的input样式,然后用span标签去模拟input标签。

隐藏原生input的样式,将其设置opacity为0,使其在页面中不可见,但是可以点击

.el-radio__original {
    opacity: 0;
    outline: none;
    position: absolute;
    z-index: -1;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    margin: 0;

设置span的样式,模拟input

// 空心,未选中转台的按钮
.el-radio__inner {
    border: 1px solid #dcdfe6;
    border-radius: 100%;
    width: 14px;
    height: 14px;
    background-color: #fff;
    position: relative;
    cursor: pointer;
    display: inline-block;
    box-sizing: border-box;
}
空心 未选中转台的按钮
// 选中状态下的按钮
el-radio__input.is-checked .el-radio__inner {
    border-color: #409eff;
    background: #409eff;
}
.el-radio__inner {
    border: 1px solid #dcdfe6;
    border-radius: 100%;
    width: 14px;
    height: 14px;
    background-color: #fff;
    position: relative;
    cursor: pointer;
    display: inline-block;
    box-sizing: border-box;
}
//  用伪类,模拟中间小圆点
.el-radio__input.is-checked .el-radio__inner:after {
    transform: translate(-50%,-50%) scale(1);
}
.el-radio__inner:after {
    width: 4px;
    height: 4px;
    border-radius: 100%;
    background-color: #fff;
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%,-50%) scale(0);
    transition: transform .15s ease-in;
}

v-model/value

单独引用el-radio组件的时候,都会使用v-model去绑定data下面的值来实现双向数据绑定,也就是说有时候即使我们不设置name属性,也可以达到互斥的效果,所以我们去看下v-model/value是如何实现的呢?

其实v-model/value只是v-bind和v-on的语法糖,在使用v-model绑定数据以后,既绑定了数据,又添加了事件监听,这个事件就是input事件。

官方文档给出:

<input v-model="something">

这不过是以下示例的语法糖:

<input
  v-bind:value="something"
  v-on:input="something = $event.target.value">

这就相当于对input元素的input事件进行监听,来实现value值的绑定,至于如何实现互斥,其实就是如果单选框的value值和v-model值相同,那么就给当前input元素添加一个checked属性,表示被选中,其他不相等的,就不添加checked属性,就实现了互斥的效果。

在el-radio的源码中,对input设置的是v-model="model",并且对model设置了getter和setter,然后emit一个input事件,在官网中可以看到解释.

允许一个自定义组件在使用 v-model 时定制 prop 和 event。默认情况下,一个组件上的 v-model 会把 value 用作 prop 且把 input 用作 event。

<my-checkbox v-model="foo" value="some value"></my-checkbox>

上述代码相当于

<my-checkbox
  :checked="foo"
  @change="val => { foo = val }"
  value="some value">
</my-checkbox>

tabindex和:aria-&

tabIndex和aria-* 都是属于无障碍学习的一些设置。

html中的tabIndex属性可以设置键盘中的TAB键在控件中的移动顺序,即焦点的顺序。几乎所有浏览器均 tabindex 属性,除了 Safari。

tabindex有三个值:0 ,-1, 以及X(X里32767是界点)。

当tabindex>=1时,该元素可以用tab键获取焦点,数字越小,越先定位到。

tabIndex=0 ,将排列在所有tabIndex>=1的控件之后。默认情况下tabIndex=0

当tabindex=-1时,该元素用tab键获取不到焦点,但是可以通过js获取.

支持 tabindex 属性的元素:<a>, <area>, <button>, <input>, <object>, <select> 以及 <textarea>。

可以用以下代码,使用tab键感受以下。

  <a href="0.com" tabindex="0">0</a>
  <a href="-1.com" tabindex="-1">-1</a>
  <a href="1.com" tabindex="1">1</a>
  <a href="2.com" tabindex="2">2</a>
  <a href="3.com" tabindex="3">3</a> 

role、aria-checked、aria-disabled,这些是为屏幕阅读器准备的,aria由一套属性组成,属性分为role以及对应的states和properties,aria将html元素分为六种role,每种有对应的states和properties,用以模拟一些tag,更详细的可点次查看《aria初探(一)》

@keydown.space.stop.prevent="model = isDisabled ? model : label" 这句也很巧妙,查了才知道,原来是为了tab切换不同选项时,按空格可以快速选择目标项。

mixin

mixin用来封装vue组件的可复用功能,一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。

当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。

  • 数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
  • 值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。

这里是混入了一个Emitter,也就是说该组件拥有Emitter中的方法。

function broadcast(componentName, eventName, params) {
  this.$children.forEach(child => {
    var name = child.$options.componentName;

    if (name === componentName) {
      child.$emit.apply(child, [eventName].concat(params));
    } else {
      broadcast.apply(child, [componentName, eventName].concat([params]));
    }
  });
}
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);
    }
  }
};

Emitter的源码中,是在methods混入了dispatch、broadcast两个方法,说明在其他的组件中也会多次使用这个两个方法。

dispatch

dispatch接受三个参数、componentName组件名,eventName事件名,params事件参数、

dispatch主要作用就是找到距离自己最近的目标父组件,然后调用目标组件的 目标事件,并传递参数。

这里调用目标事件使用的是parent.$emit.apply(parent, [eventName].concat(params));, 你可能会有疑问,为什么这么调用,不直接parent.$emit(eventName,...params)

首先,vm.$emit( event, arg )的作用是触发当前实例上的事件,apply主要作用是改变this指向,那么那个调用方式就是用parent对象去调用parent对象的eventName。

即parent拿到parent的$emit方法,再传递对应的事件参数。

broadcast

broadcast也接受三个参数、componentName组件名,eventName事件名,params事件参数、

broadcast主要作用是像后代组件传值,会遍历所有后代组件,如果是目标组件,就调用目标子组件的目标方法,并传递参数。

与dispatch类似。

四、button-group、radio-button

el-radio中很多地方都计算了isGroup,这是因为ele还提供了一个el-radio-group组件,适用于在多个互斥的选项中选择的场景,所以在设置class或者,触发input事件时都先判断是否是isGroup,如果isGroup,那么就采用isGroup的值或者事件。

radio-button和el-radio功能一样,只是样式的区别。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容