美文网首页
elementUI源码分析-04-radio

elementUI源码分析-04-radio

作者: 范小饭_ | 来源:发表于2021-02-08 11:04 被阅读0次

一、基础回顾

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功能一样,只是样式的区别。

相关文章

网友评论

      本文标题:elementUI源码分析-04-radio

      本文链接:https://www.haomeiwen.com/subject/wdpxxltx.html