美文网首页
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