花了快半个月时间来看radio组件,真的是发现本身基础很薄弱,不少东西都不知道,仍是要多学习才是。html
我发现直接把代码放出来的效果并非很好,由于要结合着讲解来才行。所以从本篇开始,我会放出我github的地址,我模仿的代码都会放在该地址下,有兴趣看的同窗能够点连接。vue
先来看一下radio组件的主体结构吧node
<template>
<label class="el-radio">
<span class="el-radio__input">
<span class="el-radio__inner"></span>
<input class="el-radio__original">
</span>
<!-- keydown.stop 阻止事件继续冒泡 -->
<span class="el-radio__label" @keydown.stop>
<slot></slot>
<!-- 若是没有设置radio显示的值 则显示label值 -->
<template v-if="!$slots.default">{{label}}</template>
</span>
</label>
</template>
复制代码
一个 template
的代码结构差很少就是这个样子,使用 lable
标签将这个文件包裹起来,扩大了点击范围,保证了点击文字和图标都可以起到点击的效果。git
咱们能够看到 radio
组件并无使用原生的radio
标签,这是由于 原生标签的 radio
在不一样的浏览器下的样式是并不相同,所以这里是将 radio
隐藏起来,本身写一个 radio
代替原生,统一各个浏览器样式问题。 在这里要说明的是,由于咱们须要用到 原生的radio
来获取焦点并触发 change
事件, 所以咱们不能将原生的 radio
设置为 dispaly:none
或者 visibility:hidden
。Element
是如何作的呢?github
opacity:0
将
radio
的透明度设置为
0
,而且绝对定位 使其脱离文档流,不会占据空间。这既隐藏了
radio
元素又不占据 空间,而且可以获取到焦点。是一个好方法,值得参考。
接下来我把 radio
的主体 template
放出来说解一下其中的属性浏览器
<template>
<label
class="el-radio"
:class="[ // radio大小仅在border为true时有效 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>
<!-- keydown.stop 阻止事件继续冒泡 -->
<span class="el-radio__label" @keydown.stop>
<slot></slot>
<!-- 若是没有设置radio显示的值 则显示label值 -->
<template v-if="!$slots.default">{{label}}</template>
</span>
</label>
</template>
复制代码
role = 'radio'
:aria-checked="model===label"
:aria-disabled="isDisabled"
//这三行是为了给不方便人士使用时提供功能的。好比当他们使用屏幕阅读器的时候, role的做用是告诉阅读这是一个 radio, aria-checked是描述这个 radio是否被选择, aria-disabled是告诉阅读器这个按钮不可读。
tabIndex="tabIndex" // 设置是否能够经过键盘上的 tab键 进行选择, -1 表明不可选, 0 表明可选
复制代码
label
标签上的 tabIndex
是经过计算获得的,bash
isGroup() {
let parent = this.$parent
while (parent) {
if (parent.$options.componentName !== 'ElTestRadioGroup') {
parent = parent.$parent
} else {
// eslint-disable-next-line
this._radioGroup = parent
return true
}
}
return false
},
// 是否禁用
isDisabled() {
return this.isGroup
? this._radioGroup.disabled || this.disabled || (this.elTestForm || {}).disabled
: this.disabled || (this.elTestForm || {}).disabled
},
tabIndex() {
return (this.isDisabled || (this.isGroup && this.model !== this.label)) ? -1 : 0
}
复制代码
首先这里经过遍历 查询当前radio
是否来被包裹于一个 radio-group
组件当中,根据是否包裹于 radio-group
将 isGroup
的值设置为 true
、false
。 再根据 isGroup
来判断 isDisabled
。ide
当 isGroup
为 true
时, isDisabled
的值首先取决于 radioGroup
是否禁用,而后是radio
是否禁用, 最后有可能 radio
是位于一个 form
表单当中,取决于 form
的禁用状态(看来form
会是一个 大难点哦)。 当为 false
时,取决于 isGroup
函数
一样的 tabIndex
值取决于 禁用状态 , 而且当 radio
位于 radioGroup
时, 而且选中的是当前 radio
, 则保证使用 tab
键操做的时候,不会再次选择,优化了体验。学习
在这几块的计算判断当中,值得注意的是它并无使用if else
判断,而是使用了 与或的短路原则,值得学习一下。
&& 的判断是同真为真,一假为假,则运算若是左边的表达式值为 false,那么就不会再执行右边的表达式了,若是左表达式为 true,就会继续执行右表达式 || 的判断是一真为真,同假为假,则运算若是左表达式值为 true,那么就不用执行右边的表达式了,若是左表达式为 false,就会继续执行右表达式;
template
标签上还值得学习的一点是vue
的事件修饰符。事件修饰符。vue
为事件v-on
添加了如下修饰符
vue还支持按键修饰符、鼠标键值修饰符、键值修饰符。详细的解释能够看这篇文章,写的很详细。按键修饰符
介绍了按键修饰符,那么这句代码也就 不难理解了
@keydown.space.stop.prevent="model = isDisabled ? model : label"
复制代码
当tab
选中当前 radio
在键盘上敲击空格键的时候(space
即空格键 ),阻止了原生事件发生(发生了什么原生事件?这个不太知道),并执行代码
model = isDisabled ? model : label // 键盘选中radio
复制代码
不知道大家看了 radio
组件有没有一个疑惑,就是没有一个 click事件,却在点击的时候触发了 model
值的改变? 由于这块有重写v-model
, 我打印了 model
的 set
和 handleChange
model: {
get() {
},
set(val) {
console.warn(val)
}
},
handleChange() {
console.log('change')
}
复制代码
在点击事件触发后执行顺序 为 set->handleChange
。
在这个地方咱们不得不说起一下 v-model
的实现原理
v-mode
l的本质是一个语法糖(几乎说烂的词),其本质是 v-bind
v-on
的组合,如下两种状况是相等的
<input v-model="test"></input>
<input v-bind:value="test" v-on:input = "test = $event.target.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)
}
复制代码
你可能会发现 为何添加的是 checked
属性 和change
事件
其实v-model
在不一样的 HTML
标签上会监控不一样属性,抛出不一样事件
tex
t和textarea
元素使用 value
属性和 input
事件checkbox
和radio
元素使用 checked
属性和 change
事件select
将 value
做为prop
并将change
做为事件v-model
使用 value
属性 和 input
事件 咱们能够看到源码上函数genRadioModel
确实是给 input
组件添加了 checked
属性 和change
事件。 源码上对于input
标签的不一样类型也是作不一样的处理(我这里放出部分代码),详细的过程有兴趣的同窗能够去看VUE
的源码 或者 去看 vue.js技术揭秘 好书值得一看!!!强推if (el.component) {
genComponentModel(el, value, modifiers)
// component v-model doesn't need extra runtime return false } else if (tag === 'select') { genSelect(el, value, modifiers) } else if (tag === 'input' && type === 'checkbox') { genCheckboxModel(el, value, modifiers) } else if (tag === 'input' && type === 'radio') { genRadioModel(el, value, modifiers) } else if (tag === 'input' || tag === 'textarea') { genDefaultModel(el, value, modifiers) } else if (!config.isReservedTag(tag)) { genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime
return false
} else if (process.env.NODE_ENV !== 'production') {
warn(
`<${el.tag} v-model="${value}">: ` +
`v-model is not supported on this element type. ` +
'If you are working with contenteditable, it\'s recommended to ' + 'wrap a library dedicated for that purpose inside a custom component.', el.rawAttrsMap['v-model'] ) } 复制代码
因此原生代码时 <input type="radio" v-model="value">
通过处理后 按照 源码的流程,将radio
其实处理为 <input v-bind:checked="value" v-on:change="value = $event.target.checked">
所以当咱们引入 radio
组件并使用的时候,咱们的代码是这样
<el-radio v-model="radio"></el-radio>
等价于
<el-radio v-bind:value="radio" v-on:input="radio = #event.tagret.value"></el-radio>
复制代码
在代码中的原生 input
标签
<input
...
v-model="model"
type="radio"
....
>
复制代码
则转化为
<input v-bind: checked="model" v-on: change="model=$event.target.checked">
复制代码
当 原生 radio
被点击时, model
的值发生改变,触发 set
model: {
get() {
// 若是是以el-radio-group包裹 则取group的value值
return this.isGroup ? this._radioGroup.value : this.value
},
set(val) {
if (this.isGroup) {
this.dispatch('ElTestRadioGroup', 'input', [val])
} else {
this.$emit('input', val)
}
this.$refs.radio && (this.$refs.radio.checked = this.model === this.label)
}
},
复制代码
而后调用 this.$emit('input')
将值传递到 咱们自定义的radio
组件上,进而改变咱们绑定的 radio
值。 利用了 on/emit
监听传递事件。
此时若是是radio-group
时,会触发 dispatch
事件,在非group
时,触发 input事件。
<input
...
@focus="focus = true"
@blur="focus=false"
@change="handleChange"
...
>
复制代码
最后就是在鼠标焦点聚焦 radio
以及移开时 触发 focus
以及 blur
事件, 当model
的值发生改变时会触发 handleChange
事件
handleChange() {
this.$nextTick(() => {
this.$emit('change', this.model)
// 根据是不是group调用 分发dispatch事件
this.isGroup && this.dispatch('ElTestRadioGroup', 'handleChange', this.model)
})
}
复制代码
this.$emit('change', this.model)
则是若是组件有自定义的 change
事件时,将会触发自定义的change
事件。
如何对vue源码中打断点呢? 在运行时,vue执行的是node_modules/vue/dist/vue.runtime.ems.js, 可是dis`是打包后的文件,我如何对 src/...里面 分开的单个文件打断点并查看?
好比说这样一个文件,model.js,用来处理 v-model指令的,我怎么打断点而后在运行时查看呢?求教
其实 radio
其实还引用了 mixins
属性(混入), 由于在单个的 radio
中并未触发到 混入文件 emitter.js
中的函数,所以我会放在下一篇 radio-group
时来根据事件触发的顺序讲一下 混入函数的触发过程。
感受能够开始阅读vue
源码了,不少东西仍是要和源码结合才能看懂诶,一个radio
看了快半个月,大部分都花在源码上了。望诸君一块儿加油。有问题但愿你们指出来!