本文继续带你看表单组件 radio,若是你没有读过另外一篇文章 Input,我建议你先看完那个再来,由于不少东西在那里面分析了。html
首先让咱们来了解一下 radio 在表单中的做用:前端
原生的 radio 想必你们都很熟悉,平时开发中也会常常用到,先看一下它经常使用的两个属性vue
name
单选按钮的名称value
单选按钮须要传给服务器的值这里重点关注一下value
,它在前端页面上并不会起到什么做用,甚至不会显示,可是最主要的就是能够经过它将单选按钮选择的值传递给服务器,好让后台程序知道用户选择了什么。为何要讲这个,别急,对后面的理解确定有帮助。node
既然 ElementUI 是基于 Vue 开发的,那么在 Vue 中是如何使用 radio 的呢?ios
移步官网查看表单输入绑定git
从官网中咱们能够看到实现 radio 的双向绑定也是使用了v-model
语法糖,它会根据控件类型自动选取正确的方法来更新元素。那么在 radio 组件中就等效于给value
属性和input
事件同时绑定一个响应式数据。当选中单选按钮时v-model
绑定的值一般就是value
的值github
<!-- 当选中时,`picked` 为字符串 "a" -->
<input type="radio" v-model="picked" value="a">
复制代码
<template>
<label class="el-radio">
<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>
</template>
复制代码
从这个结构中咱们能够看出整个 radio 组件包裹在 label 标签中,首先简单了解一下 label 标签,它是为 input 元素定义标记的,label 元素不会向用户呈现任何特殊效果,当用户选择该标签时,浏览器就会自动将焦点转到和标签相关的表单控件上,也就是说当咱们点击 label 时,其实就是点击了内部的 input 标签。这样作为用户点击按钮提供了便捷,只要在 label 范围内点击都能触发按钮的事件。编程
里层有两个span
标签,第一个表示的是前面的小圆圈,因为各浏览器对于标签的默认样式不统一,咱们又须要保证咱们的组件在任何地方运行都能保持一致的效果,一般会把原生 input 样式隐藏起来,经过一个span
或者div
来模拟,源码的具体样式后期再分析。第二个span
表示的是按钮后面的显示文字,默认传递的内容会在插槽中渲染,若是用户没有直接传递内容,那么就须要把用户传递的 label 值显示出来,这就是后面的template
作的事。json
接下来重点看一下 input 属性浏览器
<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" />
复制代码
这里面会涉及 radio 组件的属性和方法,因此我打算放在一块儿来分析。
有一些在个人另外一篇文章 Input里面分析过,这里就再也不赘述,建议传送过去看一下。
el-radio__original
类将 input 隐藏同时还能触发事件:value
是把父组件传递过来的 label 给原生value
属性:name
绑定原生name
这里重点要说的是v-model="model"
,这个怎么理解呢?不急,慢慢解释。
当咱们须要在多个单选按钮中里面来控制同一个值时,要使用v-model
双向绑定一个值,这个值是什么?看一个例子
<template>
<el-radio v-model="gender" label="0">男</el-radio>
<el-radio v-model="gender" label="1">女</el-radio>
</template>
复制代码
data() {
return {
gender: '0'
}
}
复制代码
使用组件发现v-model
绑定的是父组件的gender
值,可是子组件自己不可以直接使用v-model='value'
来双向绑定数据,由于这个value
来自于父组件,根据 Vue 的单项数据流,子组件通常状况下是不能直接更改父组件传递过来的数据的,因此须要定义一个本身的model
用来绑定 radio 组件的数据,可是这个model
也不是经过data
来写死的,由于它是取决于外界传进来的value
值,同时还要修改这个值。
从源码中能够看到,model
是一个计算属性,当它被当成一个双向数据绑定的值时,就不能是一个函数,而是一个对象,提供get
和set
方法。
model: {
get () {
// 若是在一个单选按钮组里就是按钮组的值
return this.isGroup ? this._radioGroup.value : this.value
},
set (val) {
// 若是是 radio 包裹在按钮组里,那么 model 的改变就须要触发父组件的 input 事件
if (this.isGroup) {
this.dispatch('ElRadioGroup', 'input', [val])
} else {
this.$emit('input', val)
}
// 若是当前的 model 等于组件的 label 就表示当前这个按钮被选中了
this.$refs.radio &&
(this.$refs.radio.checked = this.model === this.label)
}
}
复制代码
有关
dispatch
方法参考另外一篇文章 Input,里面有很是详细的分析emitter.js
文件的详细解释也以及上传到个人 Github 上了,欢迎 star!
当咱们须要获取model
值时会调用它的get
方法,get方法里面会判断 radio 是否是在一个单选按钮组里(具体见下文),若是在,那么 radio 自己是没有value
的,value
是经过radio-group
的v-model
传递过来的,如今咱们来看一下isGroup
这个计算属性:
// 判断 radio 标签是否在按钮组里
isGroup () {
let parent = this.$parent
while (parent) {
if (parent.$options.componentName !== 'ElRadioGroup') {
parent = parent.$parent
} else {
// 将按钮组添加到当前组件实例的 _radioGroup 属性上
// 并结束循环
this._radioGroup = parent
return true
}
}
return false
}
复制代码
在这个属性里经过循环遍历父组件,直到找到radio-group
组件把它赋值给_radioGroup
,而后咱们就可以在 radio
组件中使用radio-group
组件实例中的数据和方法了,这里为何不能直接用$parent
呢?是由于有考虑到组件嵌套,若是radio
组件的直接父组件不是radio-group
,那$parent
指向的就不是咱们须要的组件,因此这里一直遍历$parent
的$parent
就是为了将parent
指向正确的组件。
固然这里咱们还可使用「依赖注入」的方法去实现深层次的父子通讯,在radio-group
中定义须要注入的属性,将radio-group
组件的实例传过去:
provide () {
return {
radioGroup: this
}
}
复制代码
radio
组件使用inject
来接收,默认是一个空字符串:
inject: {
radioGroup: {
default: ''
}
}
复制代码
至于methods
里面的方法handleChange
是用来处理 radio 的 change 事件的,可是目前还不明白为何要使用nextTick()
,但愿大佬能分享一下。
handleChange () {
// 这里不太清楚为何使用 nextTick()
this.$nextTick(() => {
this.$emit('change', this.model)
// 若是存在按钮组
this.isGroup &&
this.dispatch('ElRadioGroup', 'handleChange', this.model)
})
}
复制代码
ElementUI 提供了单选按钮组来包裹一组互斥的按钮,使得咱们在使用的时候只须要将v-model
双向绑定在radio-group
上,而在radio
里面只须要传入label
便可。来看一下RadioGroup
组件的封装:
<template>
<component :is="_elTag" class="el-radio-group" role="radiogroup" @keydown="handleKeydown" >
<slot></slot>
</component>
</template>
复制代码
能够看出来结构仍是很简单的,就是一个component
包裹了一个插槽,使用:is
来决定须要将component
渲染成哪一个组件,_elTag
是一个计算属性
_elTag() {
return (this.$vnode.data || {}).tag || 'div';
}
复制代码
返回的是当前组件的虚拟 DOM 节点的标签,默认是div
。
接着往script
里面看,一开始就定义了一个对象,可是这个对象又不是普通的对象,它是被冻结起来的,这里就要详细说一下Object.freeze()
的做用了。
该方法用于冻结一个对象,被冻结的对象不能够修改,也就是对象身上不能添加或者删除属性,也不能修改「已有属性的可枚举性、可配置性、可写性以及原有的值」。另外它的原型对象也不容许修改,返回的是这个对象自己而不是副本。若是强行修改对象,通常状况下是静默失败的,也就是不会报错,可是在「严格模式」下会抛出TypeError
异常。
值得一提的是它是「浅冻结」,对于被冻结的对象中若是某个属性是一个「引用类型」的值,那么引用类型的值只要地址不发生变化它的属性值是能够更改的,要想实现「深冻结」就须要使用递归循环对象,具体实现参考 MDN Object.freeze()
// 冻结对象
const keyCode = Object.freeze({
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40
});
复制代码
源码是将键盘上的「上下左右」按键的键盘码做为一个对象冻结起来了,这样可以防止后续代码不当心修改了它的值。那么为何要使用键盘码呢?
在实际使用过程当中,能够发现使用方向键能够切换选中的按钮,这也是为了使用过程当中尽可能减小手离开键盘吧
// 左右上下按键 能够在 radio 组内切换不一样选项
handleKeydown(e) {
// 事件触发的元素
const target = e.target;
// 若是当前的不是 input 元素,那就是 label 标签
const className = target.nodeName === 'INPUT' ? '[type=radio]' : '[role=radio]';
const radios = this.$el.querySelectorAll(className);
const length = radios.length;
// 拿到事件触发元素在全部 radio 里的索引
const index = [].indexOf.call(radios, target);
const roleRadios = this.$el.querySelectorAll('[role=radio]');
switch (e.keyCode) {
case keyCode.LEFT:
case keyCode.UP:
e.stopPropagation();
e.preventDefault();
// 索引为 0 表示当前按钮的第一个触发了键盘事件
if (index === 0) {
// 选中最后一个
roleRadios[length - 1].click();
roleRadios[length - 1].focus();
} else {
// 往前选择
roleRadios[index - 1].click();
roleRadios[index - 1].focus();
}
break;
case keyCode.RIGHT:
case keyCode.DOWN:
// 若是当前是最后一个,那么接下来就选中第一个
if (index === (length - 1)) {
e.stopPropagation();
e.preventDefault();
roleRadios[0].click();
roleRadios[0].focus();
} else {
// 日后选择
roleRadios[index + 1].click();
roleRadios[index + 1].focus();
}
break;
default:
break;
}
}
复制代码
分析一下具体实现的步骤:
click
和focus
事件最后还有一个监听属性value
// 监听 value 若是发生变化就 form-item 组件触发 change 事件
watch: {
value(value) {
this.dispatch('ElFormItem', 'el.form.change', [this.value]);
}
}
复制代码
由于咱们的组件最终确定都是会放在 form 表单里面的,监听value
是为了当它的值发生变化时及时通知 form 表单更新数据。
最后来总结一下 radio 组件的封装:
v-model
进行数据双向绑定radio-group
包裹radio
使得在一组里面只能单一选择虽然提供的功能很简单,经过阅读和分析,咱们的编程能力必定会有所提升的,再本身动手封装一个 radio 组件你会更加了解一个组件的封装须要考虑哪些问题。
OK,下一篇再见。
【2020.3.15】超详细 ElementUI 源码分析 —— Input
【2020.3.16】超详细 ElementUI 源码分析 —— Layout