问题
当我们使用el-select选择器下拉数据很大的时候,会出现页面卡顿,甚至卡死的情况,用户体验很不好。我目前采取的方案是使用虚拟列表的方式去处理这个问题。
实现效果
数据获取完毕:
点击输入框:我们可以看到 2 万条数据只展示了 30 条。
我们滚动找到 kaimo-666,选择它
我们再次点击输入框,我们以及定位到了 kaimo-666 这个位置
另外拓展了点击项目跟输入框数据改变的事件
源码地址
我基于 vue-virtual-scroll-list
跟 element-ui
实现了下拉虚拟列表,解决下拉选择框数据量大时卡顿问题。
代码地址:https://github.com/kaimo313/select-virtual-list
什么是虚拟列表
虚拟列表是按需显示的一种技术,可以根据用户的滚动,不必渲染所有列表项,而只是渲染可视区域内的一部分列表元素的技术。
虚拟列表原理:
如图所示,当列表中有成千上万个列表项的时候,我们如果采用虚拟列表来优化。就需要只渲染可视区域( viewport )内的 item8 到 item15 这8个列表项。由于列表中一直都只是渲染8个列表元素,这也就保证了列表的性能。
代码实现
这里我们使用 vue-virtual-scroll-list 轮子,一个 vue 组件支持大量数据列表,具有高滚动性能。
1、安装 vue-virtual-scroll-list 跟 element-ui
npm i element-ui vue-virtual-scroll-list --save
里面的一些参数可以参考文档:https://github.com/tangbc/vue-virtual-scroll-list/blob/master/README.md
Required props:
Commonly used:
Public methods:You can call these methods via ref:
比如:定位到选择的项目时,我就使用了下面两个方法。
另外事件的发射参考了:https://tangbc.github.io/vue-virtual-scroll-list/#/keep-state
2、编写 select-virtual-list 组件
这里我们使用 el-popover 跟 el-input 加上 vue-virtual-scroll-list 去实现自定义虚拟选择器组件 select-virtual-list。
新建文件 src\components\SelectVirtualList\index.vue
<template> <el-popover v-model="visibleVirtualList" popper-class="select-virtual-list-popover" trigger="click" placement="bottom-start" :width="width"> <virtual-list v-if="visibleVirtualList" ref="virtualListRef" class="virtual-list" :data-key="'id'" :keeps="keeps" :data-sources="dataList" :data-component="itemComponent" :extra-props="{ curId }" :estimate-size="estimateSize" :item-class="'list-item-custom-class'" ></virtual-list> <el-input slot="reference" v-model="curValue" :style="`width:${width}px;`" :size="size" :placeholder="placeholder" @input="handleInput" ></el-input> </el-popover> </template> <script> import VirtualList from 'vue-virtual-scroll-list'; import VirtualItem from './VirtualItem.vue'; export default { name: 'SelectVirtualList', props: { width: { type: Number, default: 250 }, size: { type: String, default: "small" }, placeholder: { type: String, default: "请选择" }, dataList: { type: Array, default: () => { return []; } }, // 虚拟列表在真实 dom 中保持渲染的项目数量 keeps: { type: Number, default: 30 }, // 每个项目的估计大小,如果更接近平均大小,则滚动条长度看起来更准确。 建议分配自己计算的平均值。 estimateSize: { type: Number, default: 32 }, // input输入触发方法 virtualInputCall: Function, // 点击每个项目触发方法 virtualClickItemCall: Function }, components: { VirtualList }, watch: { visibleVirtualList(n) { if(n) { // 当展示虚拟列表时,需要定位到选择的位置 this.$nextTick(() => { let temp = this.curIndex ? this.curIndex : 0; // 方法一:手动设置滚动位置到指定索引。 this.$refs.virtualListRef.scrollToIndex(temp - 1); // 方法二:手动将滚动位置设置为指定的偏移量。 // this.$refs.virtualListRef.scrollToOffset(this.estimateSize * (temp - 1)); }) } } }, data () { return { curId: "", // 当前选择的 id curValue: "", // 当前选择的值 curIndex: null, // 当前选择的索引 visibleVirtualList: false, // 是否显示虚拟列表 itemComponent: VirtualItem, // 由 vue 创建/声明的渲染项组件,它将使用 data-sources 中的数据对象作为渲染道具并命名为:source。 }; }, created() { // 监听点击子组件 this.$on('clickVirtualItem', (item) => { this.curId = item.id; this.curValue = item.name; this.curIndex = item.index; this.visibleVirtualList = false; console.log("item--->", item) this.virtualClickItemCall && this.virtualClickItemCall(item); }) }, methods: { // 输入框改变 handleInput(val) { console.log("val--->", val); if(!val) { this.curId = ""; this.curIndex = null; } this.virtualInputCall && this.virtualInputCall(val); } } }; </script> <style lang='scss'> .el-popover.el-popper.select-virtual-list-popover { height: 300px; padding: 0; border: 1px solid #E4E7ED; border-radius: 4px; background-color: #FFFFFF; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); box-sizing: border-box; .virtual-list { width: 100%; height: calc(100% - 20px); padding: 10px 0; overflow-y: auto; } } ::-webkit-scrollbar { width: 8px; height: 8px; background-color: #fff; } ::-webkit-scrollbar-thumb { background-color: #aaa !important; border-radius: 10px !important; } ::-webkit-scrollbar-track { background-color: transparent !important; border-radius: 10px !important; -webkit-box-shadow: none !important; } </style>
新建子组件文件 src\components\SelectVirtualList\VirtualItem.vue
<template> <div :class="['virtual-item', {'is-selected': curId === source.id}]" @click="handleClick"> <span>{{source.name}}</span> </div> </template> <script> export default { name: 'VirtualItem', props: { curId: { type: String, default: "" }, source: { type: Object, default () { return {} } }, }, methods: { dispatch(componentName, eventName, ...rest) { let parent = this.$parent || this.$root; let name = parent.$options.name; while (parent && (!name || name !== componentName)) { parent = parent.$parent; if (parent) { name = parent.$options.name; } } if (parent) { parent.$emit.apply(parent, [eventName].concat(rest)); } }, handleClick() { // 通知 SelectVirtualList 组件,点击了项目 this.dispatch('SelectVirtualList', 'clickVirtualItem', this.source); } } } </script> <style lang="scss" scoped> .virtual-item { font-size: 14px; padding: 0 20px; position: relative; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #606266; height: 32px; line-height: 32px; box-sizing: border-box; cursor: pointer; &:hover { background-color: #eee; } &.is-selected { color: #409EFF; background-color: #dbeeff; } } </style>
3、编写测试 demo
新建文件:src\views\demo.vue
<template> <select-virtual-list :width="250" size="small" placeholder="请选择" :dataList="dataList" :keeps="30" :estimateSize="32" :virtualInputCall="virtualInputCall" :virtualClickItemCall="virtualClickItemCall" ></select-virtual-list> </template> <script> import SelectVirtualList from '../components/SelectVirtualList/index.vue'; // 获取模拟数据 import { getList } from '../utils/list.js'; export default { data () { return { dataList: [], } }, components: { SelectVirtualList }, created() { // 2 秒返回 2 万条数据 console.log("dataList--->开始获取数据"); getList(20000, 2000).then(res => { this.dataList = res; console.log("dataList--->数据获取完毕", res); }) }, methods: { // 输入回调 virtualInputCall(val) { console.log("virtualInputCall---->", val); // ... }, // 点击项目回调 virtualClickItemCall(item) { console.log("virtualClickItemCall---->", item); // ... } } } </script>
添加模拟接口方法:src\utils\list.js
/** * @description 获取模拟数据 * @param {Number} num 需要获取数据的数量 * @param {Number} time 需要延迟的毫秒数 */ export function getList(num = 10000, time) { return new Promise((resolve, reject) => { setTimeout(() => { const tempArr = [] let count = num; while (count--) { const index = num - count; tempArr.push({ id: `${index}$${Math.random().toString(16).substr(9)}`, index, name: `kaimo-${index}`, value: index }) } resolve(tempArr); }, time); }) }