内容简介:单选框这个组件看似简单,实则知识点众多,较为复杂,如果写一个html的原生单选框,那确实很简单,但是封装一个完整的单选组件就不那么简单了,接下来我们先介绍Vue的单选框的一些原理,然后再分析Element的单选框实现原生单选框很简单,如果我们要实现一个男女性别的单选按钮组,代码只需如下几句上面的男的单选按钮添加了
单选框这个组件看似简单,实则知识点众多,较为复杂,如果写一个html的原生单选框,那确实很简单,但是封装一个完整的单选组件就不那么简单了,接下来我们先介绍Vue的单选框的一些原理,然后再分析Element的单选框实现
原生单选 Vs Vue单选
原生单选框很简单,如果我们要实现一个男女性别的单选按钮组,代码只需如下几句
<input type="radio" name="sex" value="male" checked>男</input> <input type="radio" name="sex" value="female">女</input> 复制代码
上面的男的单选按钮添加了 checked
属性,表示被选中, value
属性表示单选按钮的值,可以给每个input添加 onchange
和 onclick
事件来通过点击获取其值,也可以通过一个按钮点击后遍历所有单选的input按钮,获取 checked
属性为 true
的那一项,然后再获取其 value
注意如何让一组单选互斥,也就是说同一时刻只能有一个单选被选中, name
属性就是这个作用, 通过把一些单选按钮的 name
设置为同一个值,就达到了互斥的效果
而Vue的单选框则有所不同,代码如下
它只需要一个v-model
即可达到互斥效果,
v-model
的值是data里面的数据,进行了双向绑定,由此可见并没有通过
name
属性来达到互斥,那么时怎么实现的呢?首先先来了解下v-model的本质,v-model本质上是语法糖
官网说的很清楚,这就相当于进行了一个双向绑定,对input输入框的input事件进行监听,当键盘敲下时就实时改变searchText的值,同时修改searchText的值,输入框的value也跟着变化。那么底层是怎么处理互斥的呢?通过查看v-model相关源码
function genRadioModel ( el: ASTElement, value: string, modifiers: ?ASTModifiers ) { const number = modifiers && modifiers.number let valueBinding = getBindingAttr(el, 'value') || 'null' valueBinding = number ? `_n(${valueBinding})` : valueBinding addProp(el, 'checked', `_q(${value},${valueBinding})`) addHandler(el, 'change', genAssignmentCode(value, valueBinding), null, true) } 复制代码
上述代码是处理单选框model的代码, genRadioModel
参数中的 value
就是input的value的值,而 valueBinding
的值就是v-model中的v-bind:value的值
<input type="radio" id="jack" value="Jack" v-model="name"> 复制代码
如果示例如上,那么 addProp
这个方法就会把 checked
属性的值 _q('Jack',name)
放入属性列表,这里_q是 looseEqual
方法的简写,表示宽松比较(如果是对象,则通过JSON.stringify转成字符串比较,否则直接String()转换比较)2个值是否相同,这样这里的逻辑就明确了,如果单选框的value的值和v-model的值相同,那么就加上一个 checked
属性,表示该单选被选中,自然而然其他单选框value的值和v-model的值不同,所以就不是选中状态,没有checked属性,所以达到了互斥效果
源码分析
整个单选组件的源码不算太长,但是里面知识点很多,先上源码,官网代码 点此
<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 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: { 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); } } }, _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 ? 0 : -1) : 0) : -1; } }, methods: { handleChange() { this.$nextTick(() => { this.$emit('change', this.model); this.isGroup && this.dispatch('ElRadioGroup', 'handleChange', this.model); }); } } }; </script> 复制代码
首先分析template部分,分析一个组件首先得搞清楚组件的html结构,上面的代码结构简化后如下
<label ...> <span class='el-radio__input'> <span class='el-radio__inner'></span> <input type='radio' .../> </span> <span class='el-radio__label'> <slot></slot> <template v-if="!$slots.default">{{label}}</template> </span> </label> 复制代码
由此可见,整个组件是一个外层label套2个span,我们知道原生的radio标签很丑,样式在各个浏览器不统一,所以必须自己实现所有radio按钮的样式,一般做法是隐藏真正的input,自己用div或者span模拟input标签,这里的label放在最外层的作用是扩大鼠标点击范围,无论是点击在文字还是input上都能够触发响应,当然如下通过for属性绑定input的id属性也可以实现
<input id='t' type='radio'> <label for='t'>点此</label> 复制代码
前者被称为隐式链接,后者是显示链接,很明显前者不需要id,肯定前者好,label里面2个内联的span水平排列,根据下图
可以猜到,第一个span代表模拟的圆形按钮,第二个span代表文字部分,而第一个span里面又有一个span和input,这个span就是模拟的圆圈,而后面的input才是真正的radio按钮,不过被隐藏了,那么是怎么隐藏的呢?查看css如下
真正的input透明度为0,且是绝对定位脱离文档流,因此不占空间且我们看不到,注意不是display:none
或者
visibility:hidden
,如果是none或者hidden的话则无法触发鼠标点击了,
只有 opacity:0
才能达到目的,这是个需要注意的地方
接下来看label中的第二个span,这个span就是我们填充的文本
<span class='el-radio__label'> <slot></slot> <template v-if="!$slots.default">{{label}}</template> </span> 复制代码
这个span里做了处理,slot默认渲染我们在 <el-radio>
和 </el-radio>
间的文本,注意template,如果我们什么都不填,比如我们这么写
<el-radio label='1'></el-radio> 复制代码
最终文本就渲染成其label的值
template通过$slot.default
进行判断是否存在子元素从而决定是否渲染,注意template自己本身不会被渲染出来,只是起一个占位符的作用
label标签分析
label标签有一大堆属性,我们依次来看
<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" > 复制代码
首先第一句 class="el-radio"
表明了label的基础类class,里面有什么呢?
@include b(radio) { color: $--radio-color; font-weight: $--radio-font-weight; line-height: 1; position: relative; cursor: pointer; display: inline-block; white-space: nowrap; outline: none; font-size: $--font-size-base; 复制代码
无非就是规定了一些很基础的css样式,鼠标样式,不换行,无轮廓,字体大小颜色等 然后第二句 :class
表明了动态绑定的类,其中有是否禁用,是否获得焦点,是否有边框,是否选中等。首先看是否禁用类 is-disabled
,部分scss代码如下
.el-radio__inner { background-color: $--radio-disabled-input-fill; border-color: $--radio-disabled-input-border-color; cursor: not-allowed; &::after { cursor: not-allowed; background-color: $--radio-disabled-icon-color; } 复制代码
可见禁用类就是修改了背景色和边框色以及鼠标样式变为禁止符号,当然这只是样式上的禁止,功能上的禁止是如何实现的呢?功能上的禁用是通过设置input的disabled属性来实现,下面源码中的真正的input的 :disabled="isDisabled"
一句话就实现了单选按钮禁止点击
<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" > 复制代码
isDisabled
是计算属性,代码如下
isDisabled() { return this.isGroup ? this._radioGroup.disabled || this.disabled || (this.elForm || {}).disabled : this.disabled || (this.elForm || {}).disabled; }, 复制代码
这里首先通过 isGroup
来判断自己是否是在单选组里,单选组也是一个Element组件,代码如下,通过将一系列单选按钮放在一起形成一个框组来进行操作,这里只需设置一个v-model在最外层即可
<el-radio-group v-model="radio2"> <el-radio :label="3">备选项</el-radio> <el-radio :label="6">备选项</el-radio> <el-radio :label="9">备选项</el-radio> </el-radio-group> 复制代码
那么 isGroup
是啥呢,看代码,它是一个计算属性,首先获取当前组件的父级组件,然后检查其组件名是否是 ElRadioGroup
即单选框组,如果不是就继续检查父级的父级,这里的知识在前面文章介绍过。这个方法会找到距离自己最近的父级 ElRadioGroup
组件
isGroup() { let parent = this.$parent; while (parent) { if (parent.$options.componentName !== 'ElRadioGroup') { parent = parent.$parent; } else { this._radioGroup = parent; return true; } } return false; }, 复制代码
回过头来看禁用的逻辑,当自己是被包含在 单选框组 组件内时,则禁用与否就等于单选框组的禁用与否,这很正常,毕竟整个框组都禁用了,自己也就被禁用了,如果只是单独的单选框组件,则禁用就是自己的 disabled
这个 prop
禁用逻辑结束,然后是 { 'is-focus': focus }
,这句话代表label标签是否获得 is-focus
类,通过focus控制,而focus在上面input的 @foucus
和 @blur
中进行处理,也就是input是否获得焦点,接下来的 is-bordered
通过用户传入的border属性进行控制是否单选框有边框,后面的 is-checked
类代表了当前单选按钮被选中的样式,通过 model===label
来控制,model是个计算属性
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); } } }, 复制代码
上面定义了getter和setter,getter首先判断自己是否是在单选框组组件内,如果是旧返回单选框组的value,否则就是自己的value,而 label
则是用户传入的一个属性,代表单选组件自己代表的值,这里的一个难点是 this.value
到底是啥,查看源码得知 this.value
是一个 prop
,但是官网上单选组件根本没有这个value供用户定义,这其实是在组件上使用 v-model
的做法,官网介绍如下
v-bind:value
这个prop,
因此在单选组件内得声明一个叫value的prop,这样就可以取到用户定义的v-model的值 ,从而加以利用,而
set
方法里面则必须通过
this.$emit('input', val)
触发父组件上的oninput事件传递出新值,
dispatch
后面我们再讨论
然后是这几句
role="radio" :aria-checked="model === label" :aria-disabled="isDisabled" 复制代码
这几句都是用来为不方便的人士提供的功能,比如屏幕阅读器,role的作用是描述一个非标准的tag的实际作用。比如用div做button,那么设置div 的 role="button",辅助 工具 就可以认出这实际上是个button。 aria的意思是Accessible Rich Internet Application,aria-*的作用就是描述这个tag在可视化的情境中的具体信息。比如:
<div role="checkbox" aria-checked="checked"></div> 复制代码
辅助工具就会知道,这个div实际上是个checkbox的角色,为选中状态,然后是
:tabindex="tabIndex" @keydown.space.stop.prevent="model = isDisabled ? model : label" 复制代码
其中tabindex规定了按下tab键该元素获取焦点的顺序,同样是个计算属性
tabIndex() { return !this.isDisabled ? (this.isGroup ? (this.model === this.label ? 0 : -1) : 0) : -1; } 复制代码
如果为禁用状态,tabindex为-1,则无法使用tab键使该元素获取焦点,如果不是禁用状态下,如果该单选按钮是在单选框组组件内且是选中状态则可以通过tab键获取焦点,否则无法通过tab键获取焦点, 当 tabindex > 0 的元素都切换之后,才会切换到 tabindex = 0 的元素,并且按出现的先后次序进行切换, 这里的逻辑就是tab只能访问到选中状态下的单选按钮
后面这句 @keydown.space.stop.prevent="model = isDisabled ? model : label"
不清楚是干啥的,我去掉了也可以正常使用组件,这里说明按下空格键会改变model的值???
混入选项
注意js部分的 mixin:[Emitter]
,首先介绍混入,混入 (mixins) 是一种分发 Vue 组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。这里将 Emitter
混入进了该组件,也就是说所有该组件都拥有 Emitter
中的方法,混入是一个数组,我们进入emitter.js中看看混入了啥?
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); } } }; 复制代码
很明显,这里将 methods
进行了混入,添加了 dispatch
和 broadcast
方法,那么为啥不直接在组件的methods里写这2个方法呢?原因在于这样做会增大代码量,由于很多地方都会用到的公用方法,用混入的方法可以减少代码量,实现代码重用,比如有10个组件都要用这2个方法,那么用混入每个组件就只写一行代码,简单很多。
混入的methods将会和组件原本的methods合并,如果冲突,则保留组件的methods里的方法,然后我们来研究 dispatch
方法,该方法实现了向 最近的特定父级组件发送事件的逻辑 ,第一个参数是父级组件的名称,第二个是事件名称,第三个参数是事件参数,是一个数组或者单独的值,逻辑也很简单:不断地取到自己的父组件,判断是否是目标组件,如果不是继续去其父组件判断,如果是则在父组件上调用 $emit
触发事件,注意这里的
parent.$emit.apply(parent, [eventName].concat(params)); 复制代码
不能写成
parent.$emit(eventName,...params) 复制代码
必须用apply定 $emit
的调用目标对象,因为是在父组件上触发该事件而不是在dispatch里,这里你可能会说 parent.$emit
不就是在父组件上调用么?其实不是, parent.$emit
仅仅是拿到了emit这个方法而已,并没有说明在哪里调用! 这里要特别注意
然后我们看看到底哪里使用了 dispatch
方法,答案就是单选组件的methods里
methods: { handleChange() { this.$nextTick(() => { this.$emit('change', this.model); this.isGroup && this.dispatch('ElRadioGroup', 'handleChange', this.model); }); } } 复制代码
这里的handleChange是在单选组件内的input上绑定的,在单选按钮失去焦点时触发
<input @change="handleChange" .../> 复制代码
当点击不同的单选按钮时会触发该按钮的原生onchange事件,这里又向父级抛出了一个change事件,这是因为单选组件需要一个 @change
来说明绑定值变化时触发的事件,同时将 this.model
的值传递出去让用户拿到该值,如下代码
<el-radio v-model="v" label='1' @change="radioChange"></el-radio> 复制代码
然后如果该单选组件是在单选组组件内,则会像单选组组件发送一个handleChange事件告诉父组件:我的值变化啦!否则怎么通知父组件自己的值!
最后是这个 $nextTick
,这个就很微妙了,试着把nextTick去掉,发现单选组件点击新的组件后,打印出来的值是旧组件的值,这就有问题了, $nextTick
的作用 是将回调延迟到下次 DOM 更新循环之后执行 ,但是这里为啥加了nextTick后就能获取新点击的单选组件的值了???不明白,希望有大佬能解释下~
以上所述就是小编给大家介绍的《Element源码分析系列4-Radio(单选框)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 以太坊源码分析(36)ethdb源码分析
- [源码分析] kubelet源码分析(一)之 NewKubeletCommand
- libmodbus源码分析(3)从机(服务端)功能源码分析
- [源码分析] nfs-client-provisioner源码分析
- [源码分析] kubelet源码分析(三)之 Pod的创建
- Spring事务源码分析专题(一)JdbcTemplate使用及源码分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。