前言✨
该组件是element的el-select组件的二次封装,解决痛点如下:
- 支持模糊搜索和首字母搜索(
element
自带的只支持文字搜索) - 性能优化,某些场景下
options
数组可能达到数千条甚至上万条,服务端接口没有做分页的情况下,dom
渲染数量过多,导致页面和下拉框极其卡顿,还会导致beforeDestroy
周期执行时间过长,离开页面的动作变得迟缓,所以我采用了截取的方法,最多展示maxNum
(默认100)条,下拉框内滚动每次加5条,大幅度提升渲染速度。并且执行搜索操作时会对全盘数据搜索而不是仅在前100条内搜索。 - 开发中经常遇到下拉框选中后获取选中参数的同级所有参数,而
element
自带的change
事件的value
只能获取到双向绑定的value
,所以网上一般都是采用循环的方式让选中的value
和原数组的value
值相等才能获取到,这使得代码量增加切性能变差。而本组件提供了optionClick
方法可以直接点击获取选中参数的所有同级参数。 - 组件内部增加了
v-on="$listeners"
,包含了父作用域中的 (不含 .native 修饰器的)v-on
事件监听器,在组件中就可以自己增加el-select的事件了
安装及使用
1.首先安装pinyin-match插件
$ npm install pinyin-match --save
2.封装组件方便后期使用
<template>
<el-select
v-bind:value="value"
v-bind="$attrs"
v-el-select-loadmore="loadMore(maxNum)"
v-on="$listeners"
:placeholder="$attrs.placeholder"
:filter-method="handleFilter"
@visible-change="visibleChange"
@focus="clearSelect('focus')"
@clear="clearSelect('clear')"
filterable
clearable
size="small"
>
<el-option
v-for="(item, index) in optionsList.slice(firstNum, maxNum)"
:key="item[props.value] + index"
v-bind="$attrs"
:label="item[props.label]"
:value="item[props.value]"
@click.native="optionClick(item)"
>
</el-option>
</el-select>
</template>
<script>
import PinyinMatch from "pinyin-match";
import { throttle } from "@/utils/tools"; // 封装的函数节流
export default {
name: "SearchSelect",
model: {
prop: "value",
event: "input",
},
props: {
// 需要绑定的值 等于 v-model
value: {
type: [String, Number, Array, Object],
default: "",
},
// 需要循环的数组 必传
options: {
type: Array,
default() {
return [];
},
required: true,
},
// el-option参数 必传
props: {
type: Object,
default() {
return {
value: "value",
label: "label",
};
},
required: true,
},
},
data() {
return {
optionsList: [],
copyOptionsList: [],
firstNum: 0,
maxNum: 100,
};
},
directives: {
"el-select-loadmore": (el, binding, vnode) => {
const DROPDOWN_DOM = el.querySelector(
".el-select-dropdown .el-select-dropdown__wrap"
);
if (DROPDOWN_DOM) {
DROPDOWN_DOM.addEventListener(
"scroll",
throttle(function () {
// this.scrollTop - 1 是为了兼容部分浏览器
const condition =
this.scrollHeight - this.scrollTop - 1 <= this.clientHeight;
if (condition) {
binding.value();
}
}),
200
);
}
},
},
watch: {
// 监听赋值并copy一份
options: {
handler(val) {
this.optionsList = val;
this.copyOptionsList = JSON.parse(JSON.stringify(val));
this.showValueMethod();
},
deep: true,
},
value: {
handler(val) {
this.showValueMethod();
},
},
},
created() {
this.optionsList = this.options;
this.copyOptionsList = JSON.parse(JSON.stringify(this.options));
this.showValueMethod();
},
methods: {
/**
* @Description: 下拉框支持模糊搜索
* @Author: JayShen
* @param {*} val
*/
handleFilter(val) {
try {
if (val) {
this.firstNum = 0;
this.maxNum = 100;
this.optionsList = this.copyOptionsList;
this.optionsList = this.optionsList.filter((item) =>
PinyinMatch.match(item[this.props.label], val)
);
} else {
this.optionsList = this.copyOptionsList;
}
} catch (error) {
console.error("模糊音下拉框:", error);
}
},
/**
* @Description: clear、focus事件还原数组
* @Author: JayShen
* @param {*}
*/
clearSelect(type) {
if (type === "clear") {
this.firstNum = 0;
this.maxNum = 100;
}
this.optionsList = this.copyOptionsList;
},
/**
* @Description: 滚动增加最大条数
* @Author: JayShen
* @param {*} n
*/
loadMore(n) {
return () => (this.maxNum += 5);
},
/**
* @Description: 触发下拉框展示
* @Author: JayShen
* @param {*} flag
*/
visibleChange(flag) {
if (flag) {
this.handleFilter();
}
},
/**
* @Description: 解决数据回显问题
* @Author: JayShen
* @param {*}
*/
showValueMethod() {
if (
this.value &&
this.optionsList &&
this.optionsList.length > this.maxNum
) {
for (let i = 0; i < this.optionsList.length; i++) {
if (this.optionsList[i][this.props.value] === this.value) {
if (i > this.maxNum) {
// 如果value位于数组后面部分,就截取前面的值,增加体验感
if (this.optionsList.length < i + this.maxNum) {
this.firstNum = i - this.maxNum;
} else {
this.firstNum = i - 5;
}
this.maxNum = i + this.maxNum;
}
break;
}
}
}
},
/**
* @Description: option点击事件
* @Author: JayShen
* @param {*} item 当前选中的参数
*/
optionClick(item) {
this.$emit("optionClick", item);
},
},
};
</script>
3.在页面中使用
<SearchSelect
v-model="id"
:options="list"
:props="{
label: 'name',
value: 'id'
}"
@optionClick="optionClick"
placeholder="placeholder"
/>
4.函数节流
export const throttle = (fn, t = 200) => {
let last
let timer
return function () {
const args = arguments
const now = +new Date()
if (last && now - last < t) {
clearTimeout(timer)
timer = setTimeout(() => {
last = now
fn.apply(this, args)
}, t)
} else {
last = now
fn.apply(this, args)
}
}