Element源码分析系列7-Select(下拉选择框)
简介
Element的下拉选择器⽰意图如下
确实做的很漂亮,交互体验⾮常好,html有原⽣的选择器 <select>,但是太丑了,⽽且各浏览器样式不统⼀,因此要做⼀个漂亮且实⽤的下拉选择器必须⾃⼰模拟全部⽅法和结构,Element的下拉选择器代码量⾮常⼤,仅 select.vue⼀个⽂件就快1000⾏,⽽且⾥⾯是由Element的其他组件组合⽽成,算上其他组件的话,⼜得加上1000⾏,最后是这个选择器引⽤了⾮常多的util以及第三⽅js,再加上这些⾄少得再加2000⾏,所以只能分析部分核⼼原理,下⾯是下拉选择器的import
import Emitter from 'element-ui/src/mixins/emitter';
import Focus from 'element-ui/src/mixins/focus';
import Locale from 'element-ui/src/mixins/locale';
import ElInput from 'element-ui/packages/input';
import ElSelectMenu from './select-dropdown.vue';
import ElOption from './option.vue';
import ElTag from 'element-ui/packages/tag';
import ElScrollbar from 'element-ui/packages/scrollbar';
import debounce from 'throttle-debounce/debounce';
import Clickoutside from 'element-ui/src/utils/clickoutside';
import { addClass, removeClass, hasClass } from 'element-ui/src/utils/dom';
import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event';
import { t } from 'element-ui/src/locale';
import scrollIntoView from 'element-ui/src/utils/scroll-into-view';
import { getValueByPath } from 'element-ui/src/utils/util';
import { valueEquals } from 'element-ui/src/utils/util';
import NavigationMixin from './navigation-mixin';
import { isKorean } from 'element-ui/src/utils/shared';
复制代码
不过这些import⾥⾯很多东西是值得学习的,官⽹代码
下拉选择器的html结构
还是先来分析这个下拉选择器的html结构,简化后的html代码如下
<template>
<div class="el-select" >
<div class="el-select__tags"
</div>
<el-input></el-input>
<transition>
<el-select-menu>
<el-select-menu>
</transtion>
</div>
</template>
html下拉菜单的制作方法复制代码
最外层⼀个div包裹所有⼦元素(相对定位),⾥⾯第⼀个div是展⽰下拉选择器的tag的包裹div,如下图,这个div绝对定位,然后通
过top:50%;transform:translateY(-50%)垂直居中于最外层的div内
然后第⼆个<el-input>是Element封装的输⼊组件,前⾯⽂章介绍过,这个输⼊框宽度和最外层的div⼀样,如下图,右侧的箭头按钮是放在其padding位置上
然后最后的 <transtion>不是组件,是Vue的过渡动画的标志,不会渲染出来,⾥⾯包裹着 <el-select-menu>这也是Element封装
的组件,表⽰弹出的下拉菜单,也是绝对定位, 所以整个下拉组件只有中间的input是相对定位,其他都是绝对定位,⽽且要善于复⽤⾃⼰已有的组件,⽽不是⼜重头写
部分功能源码分析
如果要写完所有功能,那⾄少得⼀周以上,所以只能写⼀部分
下拉框主体操作流程逻辑梳理
下⾯分析下下拉框主体操作流程以及其中的数据传递过程
⾸先看下下拉框的⽤法,官⽹代码如下
<el-select v-model="value" placeholder="请选择">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
复制代码
数据部分如下
<script>
export default {
data() {
return {
options: [{
value: '选项1',
label: '黄⾦糕'
}, {
value: '选项2',
label: '双⽪奶'
}]
value: ''
}
}
}
</script>
复制代码
可见最外层的<el-select>有⼀个v-model,这个是组件的v-model⽤法,具体参考官⽹,value初始为空,当选择了下拉菜单的某⼀项
后,value变成那⼀项的值。<el-select>标签内是⽤v-for循环出所有的options,<el-option>也是Element封装的组件,可以明确上⾯肯定绑定了click事件,options由label和value组成,分别代表该下拉项的显⽰⽂本和实际的值,⽽data中的options也提供了对应的key。
这⾥注意下<el-option>是作为slot插槽被插⼊到<el-select>中的,因此在<el-select>需要有<slot>来承载内容,如果组件没有包含⼀个 元素,则任何传⼊它的内容都会被抛弃。查看html代码,发现slot的位置如下
<el-select-menu
<el-scrollbar>
<el-option>
</el-option>
<slot></slot>
</el-scrollbar>
<p
...
</p>
</el-select-menu>
复制代码
slot被包含在<el-scrollbar>这个滚动条组件内,这个组件的实现很考验基本功,略复杂,代码,因此所有的option选项都会被放⼊滚动条组件内
当⽤户点击初始状态下的下拉框,触发toggleMenu显⽰出下拉菜单,toggleMenu如下
toggleMenu() {
if (!this.selectDisabled) {
if (uVisibleOnFocus) {
} else {
this.visible = !this.visible;
}
if (this.visible) {
(this.$refs.input || this.$ference).focus();
}
}
},
复制代码
由代码可知⾸先判断是否禁⽤,如果是在禁⽤状态下则不触发事件,接着判断uVisibleOnFocus,这⼜是⼲嘛的呢,仔细查看源码得知,当时多选状态下时,也就是下图中可以多个tag并排,这时组件⾥⾯的另⼀个输⼊框(下图光标处)会渲染出来,然后该输⼊框会聚焦,此时下拉菜单不需要隐藏(⽅便你查看已有的条⽬),所以这⾥进⾏了if判断。this.visible = !this.visible然后这句就是在切换下拉菜单的状态
下拉菜单显⽰出来后,点击某个option,会关闭下拉菜单且将这个值传递给⽗组件,先来看option组件的内容
<template>
<li
@mouseenter="hoverItem"
@click.stop="selectOptionClick"
class="el-select-dropdown__item"
v-show="visible"
:class="{
'selected': itemSelected,
'is-disabled': disabled || groupDisabled || limitReached,
'hover': hover
}">
<slot>
<span>{{ currentLabel }}</span>
</slot>
</li>
</template>
复制代码
很简单,由li元素封装⽽成,@mouseenter="hoverItem"这句话说明了当你⿏标hover在某项上时触发 hoverItem事件,这⾥你可能会问,为啥要在⿏标hover时做这件事?其实这⾥有这个操作:当你⿏标悬浮在某个option上时,按下enter键也能达到选中项的⽬的,当然单击也⾏,所以在mouseenter时就要更新被hover的option,来看hoverItem的内容
hoverItem() {
if (!this.disabled && !upDisabled) {
this.select.hoverIndex = this.select.options.indexOf(this);
}
},
复制代码
⿊⼈问号!这是在⼲嘛?仅仅是⼀条赋值语句,不慌,先看this.select是啥,搜索后发现select在如下位置
inject: ['select'],
复制代码
它既不是⼀个prop也不是data,是依赖注⼊,依赖注⼊的核⼼思想是让后代组件能够访问到祖先组件的内容,因为如果是⽗⼦组件则通过$parent就可以访问⽗组件,但是爷爷组件呢?所以有了依赖注⼊,依赖注⼊的使⽤很简单,在祖先组件内声明如下provide属性,value是祖先组件的⽅法或者属性
provide: function () {
return {
xxMethod: Method
}
}
复制代码
然后在后代组件内声明如下
inject: ['xxMethod']
复制代码
则在后代组件中可以使⽤xxMethod,回过头来看option组件的依赖注⼊select,它的位置在祖先组件(不是⽗组件)<el-select>中,也就是在本⽂的下拉选择器组件中,如下
provide() {
return {
'select': this
};
},
复制代码
它返回了this,this就是指这个下拉选择器组件的实例,因此就能通过this.select.hoverIndex下拉选择器上的hoverIndex属性,那么继续来分析this.select.hoverIndex = this.select.options.indexOf(this),这
句话的意思是按下回车后,将⿏标悬浮所在的option在options⾥的序号赋值给hoverIndex,意思就是到被悬浮的那个option在数组中的序号,然后其余的逻辑就在<el-select>⾥处理了。前⾯说⿏标hover时按下enter也能够选中,这是怎么实现的呢?可以猜到肯定在input上绑定了事件,源码⾥input上有这么⼀句
@prevent="selectOption"
复制代码
这⾥这么多修饰符闹哪样?native修饰符是必须的,官⽹说在组件⽤v-on只能监听⾃定义事件,要监听原⽣的事件必须⽤native修
饰,prevent是防⽌触发默认enter事件,⽐如按下enter提交了表单之类的,肯定不⾏。然后看selectOption⽅法
selectOption() {
if (!this.visible) {
} else {
if (this.options[this.hoverIndex]) {
this.handleOptionSelect(this.options[this.hoverIndex]);
}
}
},
复制代码
这⾥就⽤到了hoverIndex来更新选中的项,接下来看handleOptionSelect是如何更新所选的项的,这个⽅法传⼊了option实例
handleOptionSelect(option, byClick) {
if (this.multiple) {
const value = this.value.slice();
const optionIndex = ValueIndex(value, option.value);
if (optionIndex > -1) {
value.splice(optionIndex, 1);
} else if (this.multipleLimit <= 0 || value.length < this.multipleLimit) {
value.push(option.value);
}
this.$emit('input', value);
...
} else {
this.$emit('input', option.value);
this.visible = false;
}
...
},
复制代码
这⾥只保留核⼼逻辑,可以看出⾸先要判断是否是多选状态,因为多选状态下<el-select v-model="value">v-model的value是个数组,单选状态下是⼀个单独的值,如果是多选,⾸先获得value的副本,这⾥有必要搞清楚value是啥,其实value就是这个组件的⼀个prop,就是v-model语法糖拆分开来的产物,也就是上⾯的v-model中的value,也就是⽤户传⼊的data中的数据项,所以这个value变化了就会导致⽤户的传⼊的value变化。接着上⾯通过indexOf在value数组中查是否存在option选项,如果存在则splice去除掉,不存在则push进来,让后通过emit触发⽗组件的input事件改变value,同时触发⽗组件的change通知⽤户我的值改变啦!如果是单选状态,那就能简单了,直接emit即可。
当直接⿏标点击某个option时,触发@click.stop="selectOptionClick"中的selectOptionClick
selectOptionClick() {
if (this.disabled !== true && upDisabled !== true) {
this.dispatch('ElSelect', 'handleOptionClick', [this, true]);
}
},
复制代码
这个⽅法⾥⾯⽤了通⽤的dispatch⽅法在<el-select>上触发handleOptionClick事件,传⼊当前option实例,这个dispatch其实就是完成了⼦组件向祖先组件传递事件的逻辑,在<el-select>肯定有⼀个on⽅法接收该事件,如下
this.$on('handleOptionClick', this.handleOptionSelect)
复制代码
可以看出这个handleOptionSelect和上⾯说的是⼀个⽅法,因此点击某⼀个option和按enter最终都会
触发这个⽅法从⽽更新value
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论