开篇
花了快半个月时间来看radio组件,真的是发现自己基础很薄弱,很多东西都不知道,还是要多学习才是。
我发现直接把代码放出来的效果并不是很好,因为要结合着讲解来才行。因此从本篇开始,我会放出我github的地址,我模仿的代码都会放在该地址下,有兴趣看的同学可以点链接。
radio的主体结构
先来看一下radio组件的主体结构吧
<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
标签将这个文件包裹起来,扩大了点击范围,保证了点击文字和图标都能够起到点击的效果。
我们可以看到 radio
组件并没有使用原生的radio
标签,这是因为 原生标签的 radio
在不同的浏览器下的样式是并不相同,因此这里是将 radio
隐藏起来,自己写一个 radio
代替原生,统一各个浏览器样式问题。 在这里要说明的是,因为我们需要用到 原生的radio
来获取焦点并触发 change
事件, 因此我们不能将原生的 radio
设置为 dispaly:none
或者 visibility:hidden
。Element
是如何做的呢?
opacity:0
将 radio
的透明度设置为0
,并且绝对定位 使其脱离文档流,不会占据空间。这既隐藏了 radio
元素又不占据 空间,并且能够获取到焦点。是一个好方法,值得参考。
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
是通过计算得到的,
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
。
当 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
添加了以下修饰符
疑问一 passive究竟怎么使用的诶?
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
看了快半个月,大部分都花在源码上了。望诸君一起加油。有问题希望大家指出来!