最近遇到一个问题,在开发编辑页面时候,遇到select
大数据量,一个下拉选择器有两万个备选,页面上会不定出现3-18个select
,预计可能会渲染30万
多个option
备选,这么多dom加载,直接把浏览器干无响应了。
第一个构想就是
虚拟select
搜索一番没有合适的轮子,vxe-table 比较合适,但是为了一个问题单独引入,不合适,
只能先基于elementUI
的select
选择器再度封装解决问题,节流版select
第一版,节流select
此版本的select规避页面一打开,就加载所有option
导致页面卡死,无法使用为目标
<throttling-select v-model="value" :list="options" :placeholder="placeholder" selkey="key" sellabel="label"></throttling-select>
在vue组件挂载的时候,判断当前select
的option
数量是否超过1000个,小于则直接渲染,大于则不渲染只启用搜索渲染,借助于el-select
的远程搜索功能实现
<template>
<el-select
@visible-change="popChange"
v-model="selectValue"
filterable
remote
:placeholder="placeholder"
:remote-method="remoteMethod"
:loading="loading"
style="width: calc(98% - 20px)"
>
<el-option
v-for="item in options"
:key="item[selkey]"
:label="item[sellabel]"
:value="item[selkey]"
>
</el-option>
</el-select>
</template>
<script>
export default {
mixins: [],
filters: {},
components: {},
model: {
prop: 'value', //绑定的值,通过父组件传递
event: 'update' //自定义名
},
props: {
value: {
type: [String, Number],
default: ''
},
list: {//选项值
type: Array,
default: () => []
},
placeholder: {
type: [String, Number],
default: '选项多,加载慢,建议搜索'
},
selkey: {
type: [String, Number],
default: 'key'
},
sellabel: {
type: [String, Number],
default: 'label'
}
},
data() {
return {
options: [],
selectValue: '',
loading: false
}
},
computed: {},
watch: {
selectValue(val) {
console.log(val)
this.$emit('update', val)
}
},
created() {
this.selectValue = this.value
},
mounted() {
},
destroyed() {
},
methods: {
popChange(e) {
console.log(e)
if (e) {
this.loading = true
this.options = this.list.length > 1000 ? [] : [...this.list]
this.loading = false
}
},
remoteMethod(query) {
console.log(query)
if (query !== '') {
this.loading = true
this.options = []
const reg = new RegExp(query.toLowerCase())
setTimeout(() => {
this.options = this.list.filter(item => {
return reg.test(item[this.selkey].toLowerCase())
})
this.loading = false
}, 50)
} else {
this.options = []
}
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped></style>
第二版:虚拟select滚动
基于vue-virtual-scroller
插件二次开发,好处是支持大数据,复用dom元素,拖动流畅
虽然作者 已经封装过virtual-selector
插件,但是根本还是在virtual-scroller
上做的二次封装,只完成了最基本的select
功能,定制化不高.
索性自己重新封装,满足自由度。样式嫌丑
或者功能不满足
请自己改virtualSelector.vue
文件
安装
npm install -save vue-virtual-scroller
创建
virtualSelector.vue
文件
//virtualSelector.vue
//开发插件
<template>
<div
class="virtual-selector"
:id="vsId"
>
<span
class="virtual-selector__label"
:class="{none: !label}"
>{{ label }}</span>
<div class="virtual-selector__input-wrapper">
<input
class="virtual-selector__input"
:placeholder="placeholder"
v-model="selected[option.itemNameKey]"
@keyup="handleKeyup"
@input="handleInput"
@focus="handleFocus($event)"
@select="handleInputSelect($event)"
/>
<i class="virtual-selector__arrow">
<svg
viewBox="64 64 896 896"
data-icon="down"
width="1em"
height="1em"
fill="currentColor"
aria-hidden="true"
focusable="false"
class=""
>
<path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"></path>
</svg>
</i>
<div
v-if="loading"
class="virtual-selector__loading"
>
<slot name="loading"></slot>
</div>
<recycle-scroller
v-if="flist.length > 0"
class="virtual-selector__scroller virtual-selector__dropdown"
:items="flist"
:item-size="itemSize"
:key-field="option.itemNameKey"
v-slot="{ item }"
>
<div
class="virtual-selector__dropdown-item"
:class="{
'virtual-selector__dropdown-item--selected':
item[option.itemValueKey] === selected[option.itemValueKey],
}"
@click="handleItemSelect($event, item)"
>
<slot
v-if="$scopedSlots.item"
name="item"
:item="item"
></slot>
<slot v-else>{{ item[option.itemNameKey] }}</slot>
</div>
</recycle-scroller>
</div>
</div>
</template>
<script>
function debounce(fn, delay) {
let timer;
return function () {
const context = this;
const args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
};
}
import { RecycleScroller } from "vue-virtual-scroller";
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
const defaultItemPageSize = 8;
const defaultItemGap = 0;
const dropdownActiveClassName = "virtual-selector__input-wrapper--active";
export default {
name: "VirtualSelector",
components: { RecycleScroller },
props: {
loading: {
type: Boolean,
default: false,
},
label: {
type: String,
default: "",
},
placeholder: {
type: String,
default: "",
},
value: {
type: Object,
default: () => {},
},
list: {
type: Array,
required: true,
default: () => [],
},
/**
* option: {
* itemNameKey: string,
* itemValueKey: string,
* itemPageSize: number
* itemGap: number
* }
*/
option: {
type: Object,
required: true,
default: () => {},
},
},
data() {
return {
id: new Date().getTime(),
flist: [],
selected: {},
itemSize: 32 + ((this.option && this.option.itemGap) || defaultItemGap),
};
},
computed: {
vsId() {
return `virtual-selector-${this.id}`;
},
},
watch: {
list: {
immediate: true,
handler() {
this.init();
},
},
},
methods: {
init() {
if (!this.list || this.list.length == 0) return;
if (
!this.option ||
!this.option.itemNameKey ||
!this.option.itemValueKey
) {
throw new Error(
'请指定列表配置选项"itemNameKey"或"itemValueKey"'
);
}
this.flist = [...this.list];
this.value instanceof Object &&
(this.selected = {
[this.option.itemNameKey]: this.value[this.option.itemNameKey],
[this.option.itemValueKey]: this.value[this.option.itemValueKey],
});
this.$nextTick(() => {
document
.getElementById(this.vsId)
.querySelector(".virtual-selector__scroller").style.maxHeight =
(this.option.itemPageSize || defaultItemPageSize) * this.itemSize +
4 +
"px";
});
},
mount() {
document.addEventListener("click", this.handleGlobalClick, false);
},
unmount() {
document.removeEventListener("click", this.handleGlobalClick, false);
},
handleKeyup: debounce(function () {
const input = this.selected[this.option.itemNameKey];
this.option.itemNameKey !== this.option.itemValueKey &&
(this.selected[this.option.itemValueKey] = "");
if (!input) {
this.flist = [...this.list];
} else {
this.flist = this.list.filter((item) => {
if (
item[this.option.itemNameKey].toLowerCase() === input.toLowerCase()
) {
this.selected[this.option.itemValueKey] =
item[this.option.itemValueKey];
this.$nextTick(() => {
this.$emit("select", {
id: this.vsId,
select: { ...this.selected },
});
});
}
return item[this.option.itemNameKey]
.toString()
.toLowerCase()
.includes(input.toLowerCase());
});
}
this.$emit("search", {
id: this.vsId,
search: { [this.option.itemNameKey]: input },
});
}, 300),
handleInput() {
this.$emit("input", {
[this.option.itemNameKey]: this.selected[this.option.itemNameKey],
});
},
handleFocus(e) {
e.target.offsetParent.classList.toggle(dropdownActiveClassName);
this.$emit("focus", {
id: this.vsId,
focus: { event: e },
});
},
handleInputSelect(e) {
e.target.offsetParent.classList.add(dropdownActiveClassName);
},
handleItemSelect(e, item) {
this.selected = {
...item,
[this.option.itemNameKey]: e.target.offsetParent.innerText,
};
this.$emit("select", {
id: this.vsId,
select: { ...this.selected },
});
},
handleGlobalClick(e) {
if (e.target.className === "virtual-selector__input") return;
Array.from(document.querySelectorAll(".virtual-selector")).forEach(
(el) => {
el.querySelector(".virtual-selector__input-wrapper").classList.remove(
dropdownActiveClassName
);
}
);
},
},
mounted: function () {
this.mount();
},
beforeDestroy: function () {
this.unmount();
},
};
</script>
<style scoped>
.virtual-selector {
display: inline-flex;
align-items: center;
box-sizing: border-box;
}
.virtual-selector__label {
line-height: 1.5;
font-size: 14px;
white-space: nowrap;
color: #333;
}
.virtual-selector__label::after {
position: relative;
top: -0.5px;
content: ":";
margin: 0 8px 0 2px;
}
.virtual-selector__label.none::after {
display: none;
}
.virtual-selector__input-wrapper {
position: relative;
flex: 1;
}
.virtual-selector__input {
display: block;
width: 100%;
height: 32px;
padding: 0 34px 0 11px;
border: 1px solid #409eff;
border-radius: 5px;
background-color: #fff;
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
box-sizing: border-box;
outline: none;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
cursor: text;
}
.virtual-selector__input::placeholder,
.virtual-selector__input::-webkit-input-placeholder {
color: rgba(0, 0, 0, 0.65);
}
.virtual-selector__input:hover {
border-color: #0f48b3;
}
.virtual-selector__arrow {
position: absolute;
top: 10px;
right: 11px;
display: flex;
align-items: center;
justify-content: center;
width: 12px;
height: 12px;
transition: transform 0.3s, -webkit-transform 0.3s;
pointer-events: none;
}
.virtual-selector__arrow svg {
color: rgba(0, 0, 0, 0.25);
}
.virtual-selector__loading {
position: absolute;
top: 1px;
left: 1px;
width: calc(100% - 2px);
height: 30px;
line-height: 30px;
font-size: 12px;
text-align: center;
color: rgba(0, 0, 0, 0.65);
background-color: #fff;
}
.virtual-selector__input-wrapper--active input {
border-color: #0f48b3;
/*box-shadow: 0 0 0 2px rgba(15, 72, 179, 0.2);*/
}
.virtual-selector__input-wrapper--active .virtual-selector__arrow {
transform: rotate(180deg);
}
.virtual-selector__input-wrapper--active .virtual-selector__dropdown {
display: block;
}
.virtual-selector__dropdown {
display: none;
position: absolute;
min-width: 100%;
padding: 4px 0;
margin: 5px 0 0;
border-radius: 2px;
box-sizing: border-box;
line-height: 1.5;
list-style: none;
font-size: 14px;
background-color: #fff;
color: rgba(0, 0, 0, 0.65);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
outline: none;
z-index: 1050;
overflow-y: auto;
transform: translateZ(0px);
}
.virtual-selector__scroller {
max-height: 252px;
}
.virtual-selector__dropdown-item {
display: block;
padding: 5px 12px;
line-height: 22px;
font-weight: 400;
font-size: 14px;
color: rgba(0, 0, 0, 0.65);
text-align: left;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
cursor: pointer;
transition: background 0.3s ease;
}
.virtual-selector__dropdown-item:hover {
background-color: #dae7f2;
}
.virtual-selector__dropdown-item--selected {
font-weight: 600;
background-color: #fafafa;
}
</style>
抛出插件
VirtualSelector.vue
插件
//创建index.js
//写入
import VirtualSelector from '@/components/virtualSelector/Selector/virtualSelector'
const VirSelector = {
install(Vue) {
Vue.component('virtual-selector', VirtualSelector)
Vue.component('VirtualSelector', VirtualSelector)
}
}
export default VirSelector
挂载全局插件
VirSelector
//main.js
import VirSelector from '@/components/virtualSelector/index'
Vue.use(VirSelector)
封装使用vue-virtual-selector
//vue-virtual-selector.vue
//可以拿去用了
<template>
<virtual-selector
:loading="loading"
label=""
:placeholder="placeholder"
v-model="selectedvalue"
:list="list"
:option="listOption"
@focus="handleFocus"
@search="handleSearch"
@select="handleSelect">
<div slot="loading">loading...</div>
<div slot="item" slot-scope="{ item }">
<span>{{ item[listOption.itemNameKey] }}</span>
</div>
</virtual-selector>
</template>
<script>
export default {
name: 'VirSelector',
mixins: [],
filters: {},
model: {
prop: 'value', //绑定的值,通过父组件传递
event: 'update' //自定义名
},
components: {},
props: {
value: {
required: true,
type: [String, Number],
default: ''
},
list: {
required: true,
type: Array,
default: () => []
},
listOption: {
required: false,
type: Object,
default: () => {
return {
itemNameKey: 'label',
itemValueKey: 'key',
itemPageSize: 8,
itemGap: 5
}
}
},
placeholder: {
required: false,
type: [String, Number],
default: ''
}
},
data() {
return {
loading: false,
selectedvalue: {}
}
},
computed: {},
watch: {},
created() {
//如果value没值,有带选项,提示不存在
if (!this.value && this.list.length && !this.placeholder) {
//给value一个默认值
this.selectedvalue = this.list[0]
this.$emit('update', this.selectedvalue[this.listOption.itemValueKey])
} else {
//如果v-model有值,则匹配
this.selectedvalue = this.list.find(item => {
return item[this.listOption.itemNameKey] === this.value || item[this.listOption.itemValueKey] === this.value
})
}
},
mounted() {
},
destroyed() {
},
methods: {
//点击
handleFocus({ id, focus }) {
console.log('focus : ', { id, focus })
},
//搜索
handleSearch({ id, search }) {
console.log('search : ', { id, search })
this.$emit('update', search[this.listOption.itemValueKey])
this.selectedvalue = search
},
//选择
handleSelect({ id, select }) {
console.log('select : ', { id, select })
this.$emit('update', select[this.listOption.itemValueKey])
this.selectedvalue = select
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped></style>
完成了一个独立插件