前言
在后台管理系统中,数据几千上万条还是比较常见的,但当 select 数据过多导致页面卡顿至卡死 也是有可能的。
思路
大家想,导致页面卡顿或者卡死的原因是什么呢,归根到底就是数据量太多,渲染 select 的DOM数 太多,导致渲染引擎被撑炸了,总之就是 需要展示的数据太多。
解决办法
一、减少数据展示——使用scroll事件 + 数组slice切割
那还不好解决么,直接给 select 绑定一个 滚动事件 上代码
<template>
<el-select
ref="defineSelectRef"
v-model="modelValue"
>
<el-option
v-for="item in options.slice(0,rangeNumber)"
:key="item[optionsValue]"
:label="item[optionsLabel]"
:value="item[optionsValue]"
@click="selectChangeTap(item)"
/>
</el-select>
</template>
<script setup lang="ts">
const options = ref<any[]>([]) // select 数据
const defineSelectRef = ref() // select RefDom
const rangeNumber = ref(10) // 给用户展示的数据个数
onMounted(() => {
// 获取下拉框的Dom 添加懒加载
const SELECT_DOM = defineSelectRef.value.popperPaneRef.getElementsByClassName("el-select-dropdown__wrap")
// 添加滚轮事件
SELECT_DOM[0].addEventListener('scroll', (el:any) => {
// 当滑动到底部 展示的数值加10
if (el.target.scrollHeight - el.target.scrollTop === el.target.clientHeight) {
rangeNumber.value += 10
}
})
})
</script>
这种简单粗暴,只适合用于 不需要搜索 和 级联 的,不过一般来说,这种大数据量的都会要求有搜索功能,那只能再封装了
二 、添加搜索功能
使用搜索功能,那就需要 用到俩个数组了,一个数组就不够用了,老样子,上代码说话
<template>
<el-select
ref="defineSelectRef"
v-model="modelValue"
popper-class="defineSelect"
style="width: 100%"
clearable
filterable
:filter-method="filterSelect"
:placeholder="props.placeholder"
@clear="clearItem"
@visible-change="visibleChange"
:loading="loading"
>
<template v-if="type === 'default' && options.length > 0">
<el-option
v-for="item in optionsDefault"
:key="item[optionsValue]"
:label="item[optionsLabel]"
:value="item[optionsValue]"
@click="selectChangeTap(item)"
/>
</template>
<template v-else>
<el-option
v-for="item in optionsFilter.slice(0,rangeNumber)"
:key="item[optionsValue]"
:label="item[optionsLabel]"
:value="item[optionsValue]"
@click="selectChangeTap(item)"
/>
</template>
<template #loading>
<svg class="circular" viewBox="0 0 50 50">
<circle class="path" cx="25" cy="25" r="20" fill="none" />
</svg>
</template>
</el-select>
</template>
<script setup lang="ts">
import type { formItemInput } from 'types/form'
import type { PropType } from 'vue'
import { ref, toRefs, onMounted, watch } from 'vue'
interface optionsSettingType{
optionsData:Array<any>
optionsLabel:{label:string, value:string}
getOptionsFunc:any
}
const props = defineProps({
modelValue: {
},
placeholder: {
type: String,
default: ''
},
})
const emits = defineEmits(['update:modelValue', 'selectItem'])
const defineSelectRef = ref()
const { modelValue } = toRefs(props) as any
const loading = ref(true)
const type = ref<'filter'|'default'>('default')
const options = ref<any[]>([]) // 初始化得到的
const optionsDefault = ref<any[]>([]) // 默认数组
const optionsFilter = ref<any[]>([]) // 过滤处理后的
const rangeNumber = ref(10)
const optionsValue = ref(props.optionsLabel.value || 'value')
const optionsLabel = ref(props.optionsLabel.label || 'label')
onMounted(() => {
initialize()
// 获取下拉框的Dom 添加懒加载
const SELECT_DOM = defineSelectRef.value.popperPaneRef.getElementsByClassName("el-select-dropdown__wrap")
// 添加滚轮事件
SELECT_DOM[0].addEventListener('scroll', (el:any) => {
// 当滑动到底部 展示的数值加10
if (el.target.scrollHeight - el.target.scrollTop === el.target.clientHeight) {
rangeNumber.value += 10
}
})
})
// -----------------------------监听数据 开始---------------------------------
watch(() => rangeNumber.value, (newValue) => {
optionsDefault.value = options.value.slice(0, newValue)
}, { immediate: true })
// -----------------------------监听数据 结束---------------------------------
/**
* 这里使用了 防抖 ... 为何不是用的库呐 因为 每个项目用的库都不一样 就随便手写了个
* 有没有大佬帮忙补充一 在此感谢
*/
let tiemFilter = 500 // 毫秒
let isFilter = false
let ifFilterTimeout:any
// 搜索功能
function filterSelect(value:string) {
loading.value = true
value = value.trim()
// 当没有值的时候展示默认数据
if (value) {
type.value = 'filter'
} else {
type.value = 'default'
}
if (isFilter) {
clearTimeout(ifFilterTimeout)
ifFilterTimeout = setTimeout(() => {
isFilter = false
filterSelectFun(value.toLowerCase())
}, tiemFilter)
} else {
isFilter = true
clearTimeout(ifFilterTimeout)
ifFilterTimeout = setTimeout(() => {
isFilter = false
filterSelectFun(value.toLowerCase())
}, tiemFilter)
}
}
function filterSelectFun(value:string) {
// 重置搜索的集合
optionsFilter.value = []
// 精确匹配的结果
const exactMatches:object[] = []
// 其他结果 这里使用二维数组 通过找到匹配项的 index 顺序排列
const otherMatches:Array<Array<object>> = []
options.value.forEach(item => {
// 获取到 label 的值
const label = item[optionsLabel.value].toLowerCase()
if (label === value) { // 当完全相等时 插入到精准匹配中
exactMatches.push(item)
} else {
let index = label.indexOf(value) // 模糊匹配
if (index !== -1) { // 如果匹配上了 则插入其他结果的数组中
otherMatches[index] = otherMatches[index] ? otherMatches[index] : []
otherMatches[index].push(item)
}
}
})
optionsFilter.value = exactMatches.concat(...otherMatches.filter(subArr => subArr.length !== 0))
loading.value = false
}
// 打开 || 关闭 下拉框时
function visibleChange(visible: boolean) {
visible ? '' : type.value = 'default'
}
// select 选中
function selectChangeTap(item:any) {}
// 清除
function clearItem() {}
</script>
搜索也加上了,基本上能解决大多数的场景了,但鄙人有幸,遇到的是ERP... 全是级联 淦
三、添加 级联逻辑
级联这个玩意就非常恶心的了,里面有很多的坑
这里就不讲解了,这是一个封装完整的 select
<template>
<el-select
ref="defineSelectRef"
v-model="modelValue"
popper-class="defineSelect"
style="width: 100%"
clearable
filterable
:filter-method="filterSelect"
:placeholder="props.placeholder"
@clear="clearItem"
@visible-change="visibleChange"
:loading="loading"
>
<template v-if="type === 'default' && options.length > 0">
<el-option
v-for="item in optionsDefault"
:key="item[optionsValue]"
:label="item[optionsLabel]"
:value="item[optionsValue]"
@click="selectChangeTap(item)"
/>
</template>
<template v-else>
<el-option
v-for="item in optionsFilter.slice(0,rangeNumber)"
:key="item[optionsValue]"
:label="item[optionsLabel]"
:value="item[optionsValue]"
@click="selectChangeTap(item)"
/>
</template>
<template #loading>
<svg class="circular" viewBox="0 0 50 50">
<circle class="path" cx="25" cy="25" r="20" fill="none" />
</svg>
</template>
</el-select>
</template>
<!--
modelValue——————双向绑定的值
placeholder——————提示语
type——————当前下拉框展示的内容 filter:展示筛选的值 default:展示默认请求到的值
-->
<script setup lang="ts">
import type { formItemInput } from 'types/form'
import type { PropType } from 'vue'
import { ref, toRefs, onMounted, watch } from 'vue'
interface optionsSettingType{
optionsData:Array<any>
optionsLabel:{label:string, value:string}
getOptionsFunc:any
}
const props = defineProps({
modelValue: {
},
placeholder: {
type: String,
default: ''
},
optionsSetting: {
default() {
return {
optionsData: [],
optionsLabel: {
label: 'label',
value: 'value'
},
getOptionsFunc: null
} as optionsSettingType
}
},
})
const emits = defineEmits(['update:modelValue', 'selectItem', 'itemEmit', 'linkageF'])
const defineSelectRef = ref()
const { modelValue } = toRefs(props) as any
const loading = ref(true)
const type = ref<'filter'|'default'>('default')
const options = ref<any[]>([]) // 初始化
const optionsDefault = ref<any[]>([]) // 默认数组
const optionsFilter = ref<any[]>([]) // 过滤处理后的
const rangeNumber = ref(10)
const optionsValue = ref(props.optionsSetting.optionsLabel.value || 'value')
const optionsLabel = ref(props.optionsSetting.optionsLabel.label || 'label')
onMounted(() => {
initialize()
// 获取下拉框的Dom 添加懒加载
const SELECT_DOM = defineSelectRef.value.popperPaneRef.getElementsByClassName("el-select-dropdown__wrap")
// 添加滚轮事件
SELECT_DOM[0].addEventListener('scroll', (el:any) => {
// 当滑动到底部 展示的数值加10
if (el.target.scrollHeight - el.target.scrollTop === el.target.clientHeight) {
rangeNumber.value += 10
}
})
})
// -----------------------------监听数据 开始---------------------------------
watch(modelValue, (newValue) => {
if (!newValue) return
// 判断 展示的数组中是否有这个数据
let isFind = optionsDefault.value.find(item => item[optionsValue.value] === newValue)
// 如果有 则 跳出
if (isFind) return
// 如果没有 则 在初始化数组中寻找 并插入
let item = options.value.find(item => item[optionsValue.value] === newValue)
console.log(newValue, '监听2', item, options.value, optionsValue.value)
if (!item) {
item = {
optionsValue: newValue,
optionsLabel: newValue
}
}
optionsDefault.value.push(item)
}, { immediate: true })
watch(() => rangeNumber.value, (newValue) => {
optionsDefault.value = options.value.slice(0, newValue)
}, { immediate: true })
// -----------------------------监听数据 结束---------------------------------
// 初始化
let isOptionsList:NodeJS.Timer
function initialize() {
/**
* 初始化的时候就被赋值了 导致没有数据
* 判断当前的 options 有没有数据 没有数据则请求还未完成
* 使用定时器 轮询查询有没有请求完毕
* 当请求完毕的时候 在options中寻找值 并 关闭定时器
*/
if (options.value.length === 0) {
isOptionsList = setInterval(() => {
if (options.value.length !== 0) {
let item = options.value.find(item => item[optionsValue.value] === modelValue.value)
item ? optionsDefault.value.push(item) : ''
clearInterval(isOptionsList)
}
}, 500)
}
/**
* 初始化请求值
* 根据 根据父类传进来的 数据 或者 请求API 获取到下拉列表的数据
*/
if (props.optionsSetting.optionsData && props.optionsSetting.optionsData.length > 0) {
options.value = props.optionsSetting.optionsData
optionsDefault.value = options.value.slice(0, rangeNumber.value)
loading.value = false
} else if (props.optionsSetting.getOptionsFunc) {
props.optionsSetting.getOptionsFunc().then((result:any) => {
options.value = result.data
optionsDefault.value = options.value.slice(0, rangeNumber.value)
loading.value = false
})
}
}
/**
* 这里使用了 防抖 ... 为何不是用的库呐 因为 每个项目用的库都不一样 就随便手写了个
* 有没有大佬帮忙补充一 在此感谢
*/
let tiemFilter = 500 // 毫秒
let isFilter = false
let ifFilterTimeout:any
// 搜索功能
function filterSelect(value:string) {
loading.value = true
value = value.trim()
// 当没有值的时候展示默认数据
if (value) {
type.value = 'filter'
} else {
type.value = 'default'
}
if (isFilter) {
clearTimeout(ifFilterTimeout)
ifFilterTimeout = setTimeout(() => {
isFilter = false
filterSelectFun(value.toLowerCase())
}, tiemFilter)
} else {
isFilter = true
clearTimeout(ifFilterTimeout)
ifFilterTimeout = setTimeout(() => {
isFilter = false
filterSelectFun(value.toLowerCase())
}, tiemFilter)
}
}
function filterSelectFun(value:string) {
// 重置搜索的集合
optionsFilter.value = []
// 精确匹配的结果
const exactMatches:object[] = []
// 其他结果 这里使用二维数组 通过找到匹配项的 index 顺序排列
const otherMatches:Array<Array<object>> = []
options.value.forEach(item => {
// 获取到 label 的值
const label = item[optionsLabel.value].toLowerCase()
if (label === value) { // 当完全相等时 插入到精准匹配中
exactMatches.push(item)
} else {
let index = label.indexOf(value) // 模糊匹配
if (index !== -1) { // 如果匹配上了 则插入其他结果的数组中
otherMatches[index] = otherMatches[index] ? otherMatches[index] : []
otherMatches[index].push(item)
}
}
})
optionsFilter.value = exactMatches.concat(...otherMatches.filter(subArr => subArr.length !== 0))
loading.value = false
}
// 打开 || 关闭 下拉框时
function visibleChange(visible: boolean) {
visible ? '' : type.value = 'default'
}
// select 选中
function selectChangeTap(item:any) {
emits('update:modelValue', item.value)
emits('itemEmit', {})
}
// 清除
function clearItem() {
type.value = 'default'
emits('update:modelValue', '')
emits('itemEmit', {})
}
</script>
<style lang="scss" scoped>
.el-select-dropdown__loading {
display: flex;
justify-content: center;
align-items: center;
height: 100px;
font-size: 20px;
}
.circular {
display: inline;
height: 30px;
width: 30px;
animation: loading-rotate 2s linear infinite;
}
.path {
animation: loading-dash 1.5s ease-in-out infinite;
stroke-dasharray: 90, 150;
stroke-dashoffset: 0;
stroke-width: 2;
stroke: var(--el-color-primary);
stroke-linecap: round;
}
.loading-path .dot1 {
transform: translate(3.75px, 3.75px);
fill: var(--el-color-primary);
animation: custom-spin-move 1s infinite linear alternate;
opacity: 0.3;
}
.loading-path .dot2 {
transform: translate(calc(100% - 3.75px), 3.75px);
fill: var(--el-color-primary);
animation: custom-spin-move 1s infinite linear alternate;
opacity: 0.3;
animation-delay: 0.4s;
}
.loading-path .dot3 {
transform: translate(3.75px, calc(100% - 3.75px));
fill: var(--el-color-primary);
animation: custom-spin-move 1s infinite linear alternate;
opacity: 0.3;
animation-delay: 1.2s;
}
.loading-path .dot4 {
transform: translate(calc(100% - 3.75px), calc(100% - 3.75px));
fill: var(--el-color-primary);
animation: custom-spin-move 1s infinite linear alternate;
opacity: 0.3;
animation-delay: 0.8s;
}
@keyframes loading-rotate {
to {
transform: rotate(360deg);
}
}
@keyframes loading-dash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -40px;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -120px;
}
}
@keyframes custom-spin-move {
to {
opacity: 1;
}
}
</style>
这是前端来进行处理,不过更合理的方式是通过后端,发起请求去处理比较好