!!!解决select数据量过多页面卡顿+级联处理

前言

        在后台管理系统中,数据几千上万条还是比较常见的,但当 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>

这是前端来进行处理,不过更合理的方式是通过后端,发起请求去处理比较好

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值