Element源码分析系列8-Cascader(级联选择器)

更新时间:2023-06-20 23:53:39 阅读: 评论:0

Element源码分析系列8-Cascader(级联选择器)
简介
级联选择器,如下图,也是⼀种常⽤的组件,这个组件会⽐较复杂⼀点
Element中和该组件相关的⽂件有 main.vue和 menu.vue2个⽂件,前者代表输⼊框部分,后者代表下⽅的级联选择部分,以及附加的js⽂件popper.js以及vue.popper.js,⽤来处理弹出框逻辑,前⾯⽂章介绍过,这4个⽂件总代码量2000⾏左右,⾸先要明
确,Element中把弹出框的逻辑分离出去了,放在专门的popper.js中,因为许多组件都要⽤到该弹出框。该组件官⽹代码
级联选择器输⼊框的html结构
先来看main.vue中的html结构,main.vue代表输⼊框部分,简化后的html结构如下
<span class="el-cascader">
<el-input>
<template slot="suffix">
<i v-if></i>
<i v-el></i>
</template>
</el-input>
<span class="el-cascader__label">
...
</span>
</span>
复制代码
结构很简单,最外层⼀个span包裹所有元素,该span相对定位且inline-block,⾥⾯是⼀个<el-input>
输⼊框组件,该输⼊框⽤来搜索⽬标内容(内容就是级联选择器的data),然后<el-input>⾥⾯⼀个template作为插槽存放了2个i标签,注意这⾥的slot="suffix"这是将这2个i标签作为具名插槽的内容插⼊<el-input>中的对应位置,带表了下箭头和清空输⼊框的按钮。然后是⼀个span,这个span就是下图中输⼊框内的⽂字
注意这⾥输⼊框的⽂字不是直接作为value放在输⼊框内的,⽽是⼀个绝对定位的span放在输⼊框上,⼀般我们会直接把选中的⽂字作为输⼊框的value填充,但这⾥没有这么做,因为后⾯有个搜索功能,需要在输⼊框内输⼊⽂字
是不是没有发现下拉菜单的html结构?因为下拉菜单是挂载在document.body上的,通过popper.js来控制,所以结构被分离出去了
级联选择器输⼊框的代码分析
先来看最外层span的代码
<span
class="el-cascader"
:class="[
{
'is-opened': menuVisible,
'is-disabled': cascaderDisabled
},
cascaderSize ? 'el-cascader--' + cascaderSize : ''
]"
@click="handleClick"
@mouenter="inputHover = true"
@focus="inputHover = true"
@mouleave="inputHover = fal"
@blur="inputHover = fal"
ref="reference"
v-clickoutside="handleClickoutside"
@keydown="handleKeydown"
居里夫人的资料>
复制代码
作为组件最外层的span,其功能主要就是点击之后会弹出/隐藏下拉框,前⾯class部分就是控制该输⼊框是否禁⽤的样式,is-opened这个类很奇怪,源码⾥没有,⽽且审查元素也发现该类是空,menuVisible是组件内data中的变量,控制是否显⽰下拉菜单,⾃然可以想到,下⾯的@click="handleClick"中有控制该变量的代码,该⽅法如下
handleClick() {工会民主管理制度
if (this.cascaderDisabled) return;
this.$refs.input.focus();
if (this.filterable) {
return;
}
},
复制代码
⾸先判断组件是否禁⽤,如果禁⽤则直接返回,第⼆句this.$refs.input.focus()是获取到该组件内的<el-input>并让其获得焦点,focus是原⽣⽤法,注意这⾥默认状态下组件内的输⼊框是readonly只读的,只有在开启了搜索状态下才能获得焦点,⽽开启搜索由filterable这个prop 控制,⽤户传⼊,<el-input>上:readonly="readonly"这句话就是控制只读的,readonly是个计算属性,如下
readonly() {
const isIE = !this.$isServer && !isNaN(Number(document.documentMode));
return !this.filterable || (!isIE && !uVisible);
}
复制代码
这⾥⾸先判断是不是ie浏览器,⾸先判断是不是服务端渲染,如果是则直接返回fal,然后这句
话!isNaN(Number(document.documentMode)就可以很轻松的判断是否是ie,之前我记得⼀般是
⽤navigator.urAgent.indexOf("MSIE")>0来判断的,documentMode是⼀个ie特有属性
ie返回⼀个数字,其他浏览器返回undefined,则Numer(undefined)就是NaN,那为啥不直接⽤
document.documentMode!==undefined来判断呢这⾥不明⽩,难道是怕undefined不是真正的undefined?因为undefined可以被修改。继续看return逻辑,如果是开启搜索状态(filterable为true,那么⼀般情况下输⼊框readonly应该为fal,表⽰可以写⼊),注意这⾥还要继续判断 (!isIE && !uVisible),如果浏览器是ie,那么输⼊框可写,问题来了,为啥要判断ie呢?这⾥有点迷糊,我试了下ie和chrome,没看出啥问题来
继续回到handleClick中, if (this.filterable)这句话说明如果开启了搜索状态,则点击输⼊框后直接返回,不切换下拉菜单状态,这是合理的,因为搜索状态下需要让下拉菜单⼀直显⽰⽅便你查看,最后⼀句uVisible = !uVisible才是真正切换的语句
接着看span上的这4句
@mouenter="inputHover = true"
@focus="inputHover = true"
项目实施报告@mouleave="inputHover = fal"
@blur="inputHover = fal"
复制代码
这是控制是否显⽰输⼊框的叉按钮,⽤于清空输⼊框,如下图
mouenter和mouleave表⽰⿏标移⼊移出span时切换inputHover,注意不是mouover和mouout,因为这2者会在⼦元素上触发。但是focus和blur就奇怪了,因为这是在普通html元素span
上绑定的,⼀般来说只在input上做,span默认没有tabindex,因此按tab⽆法使得其获得焦点,除⾮加⼀个tabindex属性,但是官⽹⼜没有说明有这个属性,所以span到底是如何获得焦点的?仔细查看span元素的属性后,如下图
发现它有⼀个tabindex,但是为-1,-1的意思就是通过tab键⽆法访问到,这⾥我有2点不明⽩,⼀是这个tabindex属性是如何加上去的,⼆是span的 @keydown="handleKeydown"这⼀句,通过打印发现当组件内的input获得焦点时,这个span上的keydown会被触发。
@keydown="handleKeydown"最后⼀句这⾥也很奇怪,给span绑定了⼀个keydown⽅法,只有在span获得焦点时按键才触发该⽅法,仔细观察后发现原来是span⾥⾯的input获得焦点触发focus⽅法, 然后冒泡到⽗span上触发⽗span的focus,这时候按键就能够触发⽗span的keydown
再来看<el-input>的代码
<el-input
ref="input"
:readonly="readonly"
:placeholder="currentLabels.length ? undefined : placeholder"
v-model="inputValue"我就是我
@input="debouncedInputChange"
@focus="handleFocus"
@blur="handleBlur"
送人游岭南
@compositionstart.native="handleComposition"
@compositionend.native="handleComposition"
:validate-event="fal"
:size="size"
:disabled="cascaderDisabled"
:class="{ 'is-focus': menuVisible }"
>
恋爱先生下载复制代码
⾸先要明确这个输⼊框起到的作⽤仅仅承载是搜索功能时⽤户输⼊的⽂字,v-model="inputValue"这句话指定了输⼊框绑定的值,当⽤户键⼊字符时,该值被更新,inputValue是组件内的data中的属性,@input="debouncedInputChange"这句话声明了input事件绑定的函数,从名字看来这⾥⽤到了防抖,简⽽⾔之,这⾥的防抖就是⽤户输⼊⽂字时停顿了多久才触发debouncedInputChange,因为搜索功能会调⽤ajax,因此是异步的,需要控制向服务器的请求频率,如果不设置,则输⼊⼀个字符触发⼀次,明显太⾼频,来看⼀下debouncedInputChange
this.debouncedInputChange = debounce(this.debounce, value => {
const before = this.beforeFilter(value);
if (before && before.then) {
__IS__FLAT__OPTIONS: true,
label: this.t('el.cascader.loading'),explore的名词
value: '',
disabled: true
}];
before
.then(() => {
this.$nextTick(() => {
this.handleInputChange(value);
});
});
} el if (before !== fal) {
this.$nextTick(() => {
this.handleInputChange(value);
});
}
});
},
复制代码
这⾥的debounce是⼀个⾼阶函数,⼀个完整的防抖函数实现,具体可参考npm,第⼀个参数是防抖时间,第⼆个参数就是指定的回调函数,返回⼀个新的函数作为input事件绑定的函数。这个回调函数的参数是value,就是输⼊框新输⼊的值,该函数内第⼀句const before = this.beforeFilter(value)的beforeFilter是⼀个函数
beforeFilter: {
type: Function,
default: () => (() => {})
},
复制代码
这个函数是⼀个函数,before是其返回值,该函数是由⽤户⾃定义传⼊的,⽬的是作为搜索功能筛选之前的钩⼦,参数为输⼊的值,若返回fal 或者返回 Promi 且被 reject,则停⽌筛选。
接着if (before && before.then)如果该函数的返回值为true且拥有then⽅法,说明是个promi,⾸先修改menu.options为加载状态, 然后在then⾥⾯执⾏this.handleInputChange(value)进⾏真正的操作 ,el if那⼀段说明不是promi且返回值为true,则直接执⾏handleInputChange⽅法,这⾥为啥要⽤nextTick,暂时还不明⽩
<el-input>后⾯的@compositionstart.native="handleComposition"监听了⼀个原⽣的事件,注意这是在<el-input>组件上给根元素监听的原⽣事件⽽不是给原⽣html元素监听事件,那么必须⽤native修饰符
然后注意到mounted⽅法⾥有这么⼀句话
mounted() {
this.flatOptions = this.flattenOptions(this.options);
}
复制代码
这就是在进⾏经典的数组展平操作,this.options是⽤户传⼊的数据数组,⽤来渲染下拉菜单,⽽数组的每个值都是⼀个对象,有
value,label,children,⽽children就是嵌套的⼦数组,相当于⼆级菜单以及多级菜单,那么为啥要展平呢?原因是搜索功能需要遍历所有数据项,因此展平的数组更容易遍历,下⾯是代码
flattenOptions(options, ancestor = []) {
let flatOptions = [];
options.forEach((option) => {
const optionsStack = at(option);
if (!option[this.childrenKey]) {
flatOptions.push(optionsStack);
} el {
if (this.changeOnSelect) {
flatOptions.push(optionsStack);
}
flatOptions = at(this.flattenOptions(option[this.childrenKey], optionsStack));
}
});
return flatOptions;
},
复制代码
原理就是递归操作,判断有没有children项存在,如果有,则递归调⽤⾃⼰,并concat到flatOptions 并返回,否则直接push,这⾥该⽅法的第⼆个参数是⽤来保存多级菜单的,然后到搜索的代码⾥看下,核⼼搜索逻辑如下
let filteredFlatOptions = flatOptions.filter(optionsStack => {
return optionsStack.some(option => new RegExp(escapeRegexpString(value), 'i')
.test(option[this.labelKey]));
});
复制代码
这就是对展开的数组进⾏filter操作,⽤正则表达式进⾏匹配,value就是⽤户输⼊的要查询的值,这⾥optionStack是数组,如果⾥⾯任何⼀项满⾜,都返回true表⽰找到,通过some⾼阶函数最终获得filteredFlatOptions搜索的结果
级联选择器下拉菜单分析
通过查看main.vue的的代码发现html部分并没有下拉菜单这个结构,其实下拉菜单是挂载在body上的,那⾃然会问,输⼊框部分和下拉菜单部分是如何联系在⼀起的?查看源码发现⼀个initMenu的⽅法,该⽅法在第⼀次showMenu时会被调⽤,代码如下
initMenu() {
this.popperElm = u.$el;
dlan},
复制代码
注意第⼀句话u = new Vue(ElCascaderMenu).$mount()这表明把ElCascaderMenu作为选项对象,然后new了⼀个Vue的实例出来,这个实例就是下拉菜单实例,ElCascaderMenu就是菜单组件,⽽$mount()没有传递参数,表⽰在⽂档之外渲染,但是没有挂载到dom,具体的挂载操作在vue-popper.js中进⾏,这⾥⽤u保存了下拉菜单的实例,因此对于⽤户操作下拉菜单,都能通过
也很重要,它将下拉菜单的根dom元素赋值给了popperElm,popperElm⼜是哪⾥来的呢?是这样来的
const popperMixin = {
props: {
placement: {
type: String,
default: 'bottom-start'
},
appendToBody: Popper.props.appendToBody,
arrowOfft: Popper.props.arrowOfft,
offt: Popper.props.offt,
boundariesPadding: Popper.props.boundariesPadding,
popperOptions: Popper.props.popperOptions
},
methods: hods,
data: Popper.data,
beforeDestroy: Popper.beforeDestroy
};
复制代码
通过popperMixin将vue-popper.js⾥⾯的⽅法,data等混⼊输⼊框这个部分,这样做的⽬的是能够在这个组件⾥操作popper组件的相关内容。initMenu中最后⼏句就是在监听下拉菜单⽤$emit触发的各种事件
到现在为⽌还是没有看到下拉菜单是如何挂载到body上的,initMenu⾥没有,我们继续看,当点击输⼊框时弹出下拉菜单,触发showMenu,进⼊showMenu

本文发布于:2023-06-20 23:53:39,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/fan/89/1047603.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:下拉菜单   搜索   函数   组件   作为
相关文章
留言与评论(共有 0 条评论)
   
验证码:
推荐文章
排行榜
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图