vue虚拟滚动下拉选择器

目的

为了解决 element-ui 中 el-select 组件在大数据量的情况下出现的性能问题(数据量太大,导致渲染过慢,或造成页面卡顿甚至于卡死) 。

原理

本组件时基于vue-virtual-scroll-list(一个基于vue2的虚拟滚动组件,通过不渲染可视区域以外的内容,显示虚拟的滚动条来提升页面性能)实现的下拉选中框,该下拉选择组件支持单选,多选,筛选等操作,若是选中了数据,再次打开会自动定位到数据的位置

一.安装

npm install vue-virtual-scroll-list --save

常用参数及方法

常用参数
参数名 类型 描述
data-key String|Function 从每个数据对象中的“数据源”获取唯一键。或者用每个“数据源”调用一个函数,并返回它们的唯一键。它的值在“数据源”中必须是唯一的,用于标识条目的大小。
data-sources Array[Object] 为列表构建的源数组,每个数组数据必须是一个对象,并具有唯一的键ID(data-key)属性。
data-component Component vue创建/声明的渲染项组件,它将使用’ data-sources ‘中的数据对象作为渲染道具,并命名为’source’。
keeps Number 你希望虚拟列表在真正的dom中保持呈现多少项,默认30个。
extra-props Object 分配给不在数据源中的项目组件的额外道具。注意:索引和源都被占用在内部。
常用方法

可以通过ref方式调用以下这些方法:

方法名 描述
reset 将所有状态重置为初始状态。
scrollToIndex(index) 手动设置滚动位置为指定的索引。
scrollToOffset(offset) 手动设置滚动位置到指定的偏移量。

上述是组件常用的参数和方法,其他参数及方法可 官方插件

二.开始封装element的select组件

在src/components中创建文件夹SelectV2

1.创建index.vue文件

<template>
  <div>
    <el-select
      :value="value"
      popper-class="virtualselect"
      filterable
      :filter-method="filterMethod"
      @visible-change="visibleChange"
      v-bind="$attrs"
      v-on="$listeners"
    >
      <vue-virtual-list
        ref="virtualList"
        class="virtualselect-list"
        :data-key="fields.value"
        :data-sources="dataSources"
        :data-component="itemComponent"
        :keeps="20"
        :extra-props="{
          label: fields.label,
          value: fields.value,
          rightLabel: fields.rightLabel
        }"
      ></vue-virtual-list>
    </el-select>
  </div>
</template>

<script>
import VueVirtualList from 'vue-virtual-scroll-list';
import itemComponent from './itemComponent';

export default {
  name: 'select-v2',
  components: {
    'vue-virtual-list': VueVirtualList
  },
  model: {
    prop: 'value',
    event: 'change'
  },
  props: {
    // 下例列表数据
    data: {
      type: Array,
      default: () => []
    },
    // 字段参数配置,默认为label,value
    // 可尝试传入rightLabel字段,使其在右边也展示内容
    fields: {
      type: Object,
      default() {
        return {
          label: 'label',
          value: 'value',
        };
      }
    },
    // 绑定的默认值
    value: {
      type: [String, Array],
      default: () => []
    }
  },
  mounted() {
    this.init();
  },
  watch: {
    'data'() {
      this.init();
    }
  },
  data() {
    return {
      // 内容组件
      itemComponent,
      // 下拉列表中的数据
      dataSources: [],
      // 用来定位滚动到某个位置
      start: 0
    };
  },
  methods: {
    init() {
      if (!this.value || this.value.length === 0) {
        // 初始化数据
        this.dataSources = this.data;
      } else {
        // 回显问题
        // 单选时,存储当前的index
        // 多选时,取选中的所有值中下标的最小值
        this.dataSources = JSON.parse(JSON.stringify(this.data));
        if (typeof this.value === 'string') {
          const value = this.value
          for (let i = 0; i < this.dataSources.length; i++) {
            const element = this.dataSources[i];
            if (element[this.fields.value] === value) {
              this.start = i
              break;
            }
          }
        } else if (Array.isArray(this.value)) {
          let start = []
          const valueSet = new Set(this.value)
          for (let i = 0; i < this.dataSources.length; i++) {
            const element = this.dataSources[i];
            if (valueSet.has(element[this.fields.value])) {
              start.push(i)
            }
          }
          this.start = Math.min(...start)
        }
      }
    },
    // 搜索
    filterMethod(query) {
      if (query !== '') {
        setTimeout(() => {
          this.$refs.virtualList.scrollToIndex(0);
          this.dataSources = [...this.data.filter((item) => {
            return this.fields.rightLabel
                ? item[this.fields.label]
                    .indexOf(query) > -1 ||
                item[this.fields.rightLabel]
                    .indexOf(query) > -1
                : item[this.fields.label]
                .indexOf(query) > -1;
          })];
        }, 100);
      } else {
        this.init();
      }
    },
    visibleChange(bool) {
      if (!bool) {
        this.$refs.virtualList.reset();
        this.init();
      } else {
        // 定位到选中的位置
        this.$nextTick(() => {
          this.$refs.virtualList.scrollToIndex(this.start);
        })
      }
    }
  }
};
</script>
<style lang="scss">
.virtualselect {
  // 设置最大高度
  &-list {
    max-height: 245px;
    overflow-y: auto;
  }

  .el-scrollbar .el-scrollbar__bar.is-vertical {
    width: 0 !important;
  }
}
</style>

2.同目录下创建itemComponent.vue文件

<template>
  <div>
    <el-option
        :key="label + value"
        :label="source[label]"
        :value="source[value]"
    >
      <span>{{ source[label] }}</span>
      <span v-if="rightLabel" style="float:right;color:#939393">{{
          source[rightLabel]
        }}</span>
    </el-option>
  </div>
</template>

<script>
export default {
  name: 'item-component',
  props: {
    // index of current item
    // 每一行的索引
    index: {
      type: Number
    },
    // 每一行的内容
    source: {
      type: Object,
      default() {
        return {};
      }
    },
    // 需要显示的名称
    label: {
      type: String
    },
    // 绑定的值
    value: {
      type: String
    },
    // 右侧显示绑定的值,为空则不显示
    rightLabel: {
      type: String,
      default: ''
    }
  },
  mounted() {}
};
</script>

三.使用用例

创建测试页面

<template>
  <div class="cw-select">
    <select-v2
        :data="list"
        v-model="value"
        placeholder="请选择下拉数据"
        clearable
        multiple
        @change="selectChange"
    ></select-v2>
  </div>
</template>

<script>
import SelectV2 from '@/components/SelectV2'
export default {
  name: 'demo',
  components: {
    'select-v2': SelectV2
  },
  data() {
    return {
      list: [],
      // 下拉框选择的默认值,可以时字符串和数组
      value: []
    };
  },
  mounted() {
    this.list = [];
    // 创建20000条测试数据
    for (let i = 0; i < 20000; i++) {
      this.list.push({ value: i, label: '测试' + i + '' });
    }
  },
  methods: {
    selectChange(val) {
      console.log('下拉框选择的值', val);
    }
  }
};
</script>

0/500
评论列表